Compare commits
2867 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 | |||
43ef527810 | |||
a6032a1387 | |||
23179cac53 | |||
954c3319bb | |||
eb6ec0275d | |||
890f8bc2d5 | |||
26b8dd9cda | |||
c07af8491c | |||
10d4da9fbf | |||
95e861099f | |||
6dc51dfab3 | |||
ddb0af1dcb | |||
b6e9043d91 | |||
7c553ee10d | |||
2a6b14adc6 | |||
159a5f60d0 | |||
08f03b3118 | |||
f137960f96 | |||
e5ab46727a | |||
8d2679b76e | |||
b0b6b994ed | |||
bb163132fe | |||
439bed2f24 | |||
5a233d8878 | |||
20cdbb6185 | |||
b3c4208e44 | |||
8623037224 | |||
e8d3d3d741 | |||
cc700f0ba0 | |||
801a5cce17 | |||
2a3ad58c18 | |||
969744f4cb | |||
40ebbdebeb | |||
eb64886dcf | |||
81fc9d57bd | |||
66b480f837 | |||
7e6a84ce19 | |||
68e7642827 | |||
b37c7ccc8a | |||
b7c9ba8ffd | |||
c8a31c4b6a | |||
fb6f450bfe | |||
9a97d86ff9 | |||
a5e3ecb38b | |||
9b22d94600 | |||
a42ddb959b | |||
b4e94d3c00 | |||
aad89a2001 | |||
07e4f99f80 | |||
715562c750 | |||
ef4f4d8374 | |||
e15f13646c | |||
8677c17f29 | |||
21ee42b5a4 | |||
d20964ec94 | |||
3155f5da66 | |||
50e38b6a10 | |||
72c31776d7 | |||
7456750828 | |||
b8a08f0789 | |||
28990a12da | |||
7e5abe8589 | |||
1d57f4b4dc | |||
9f875160ea | |||
785ed095bc | |||
359e93a1ba | |||
ee6322aae4 | |||
98d3d1967a | |||
01c3b148e6 | |||
77d3a5190d | |||
93c1c56279 | |||
8340608045 | |||
5502a74994 | |||
c5ef4c973b | |||
dbae909903 | |||
74317a1f3c | |||
569e933f02 | |||
46c40d713a | |||
f3602219f3 | |||
c6b52acf2f | |||
7260589136 | |||
b2e9ddaa4f | |||
4338b3ecb7 | |||
0f81d9f146 | |||
9980b10a83 | |||
6bfd388827 | |||
8203ed687b | |||
f394e06e4d | |||
8db35a81da | |||
2e0ebfcf58 | |||
51d2ff85e0 | |||
8b54426b89 | |||
fa346c5b1f | |||
d87788980d | |||
1e4319498c | |||
48501407fc | |||
01cbdee2ca | |||
b70c0af0a9 | |||
dcaee9cb7f | |||
1d8b6c7792 | |||
c98dbe84c6 | |||
1785916d32 | |||
c6cf564842 | |||
380b739017 | |||
c3f3c55303 | |||
2c01018529 | |||
dd2edf3013 | |||
7f3cdf9fac | |||
f75a91fbc1 | |||
f151086bb1 | |||
7c833e6f10 | |||
885e8314e8 | |||
3bdd955ab6 | |||
9499587bad | |||
0879ddba7c | |||
106a1dd4c7 | |||
5580248bcd | |||
c9c10b8fba | |||
ed254922e9 | |||
4b7d1e2cec | |||
775ae57258 | |||
96e4cd79ee | |||
bb70f7df4f | |||
602de2e407 | |||
225ced989c | |||
ab53698f34 | |||
02f2124126 | |||
66623e3324 | |||
4cc886fd04 | |||
a10b490245 | |||
b947b8a00d | |||
60fa240a4d | |||
e05cd87b7c | |||
8e41c905ed | |||
e21f607ab0 | |||
34af5abb82 | |||
fc1cdb5dc9 | |||
069a0cc980 | |||
78363727e1 | |||
23b16746f6 | |||
6edf9c3d15 | |||
2e371d2078 | |||
b70b478e25 | |||
97ad132086 | |||
2c5c54bf46 | |||
c0ca078b43 | |||
53e94d04f6 | |||
dd198f9f06 | |||
43f455f4d0 | |||
d9132ea5a5 | |||
d5c7e2547b | |||
13b87de06c | |||
83bdbb7a7e | |||
89320b8d51 | |||
5fa9c3879c | |||
c0cd76d40e | |||
01f60f8013 | |||
91ad0712af | |||
2cb1b36725 | |||
67ce66765f | |||
c8cc453942 | |||
3f835f152f | |||
35e8a853e0 | |||
d084a4bf4a | |||
2a6b79e097 | |||
5d786c922f | |||
26bd5dd534 | |||
b4df0ce52c | |||
386a8d7cd7 | |||
5221fb3479 | |||
2871f63f25 | |||
4c282d0559 | |||
4ac14dc074 | |||
fcdbb04ea6 | |||
14f283cf49 | |||
efc459663a | |||
d459aaf943 | |||
921c7d5441 | |||
7d7e648968 | |||
96fc779ec8 | |||
9605e17e96 | |||
111c1675f9 | |||
d547ec3126 | |||
0ce6a7be7f | |||
1e5eae69e9 | |||
89d9208f44 | |||
3e80aaf242 | |||
86c3159ded | |||
230e05abc0 | |||
ff2e6e6432 | |||
dc00be9be6 | |||
77249d8a58 | |||
b9ee0afe7f | |||
db23ab0bc2 | |||
13dc47533c | |||
0091a888bc | |||
724b585004 | |||
c0d127e4f4 | |||
1638577d98 | |||
951c202fd0 | |||
a578b86715 | |||
b6b384854e | |||
dac937060d | |||
c628183773 | |||
eaacaa6966 | |||
9a09e280c9 | |||
70ac07d861 | |||
dabe372360 | |||
d8e3fdeb14 | |||
a147084458 | |||
b25e171412 | |||
60aaa03202 | |||
3392ab8385 | |||
f172b94467 | |||
ca549fe8d8 | |||
0b6f56a774 | |||
e5bcff12cb | |||
2754900f7a | |||
54f43d3331 | |||
4900200c06 | |||
6d82a74db4 | |||
2d7f21b021 | |||
4292c0c642 | |||
9dca515a42 | |||
b577cd9829 | |||
704682cbbe | |||
858f0d77c0 | |||
31ef6063f2 | |||
f3bd81c55b | |||
24bb4902c2 | |||
ca7821cfad | |||
a73b25580e | |||
82e179730e | |||
b5d5002061 | |||
2ab2c9e05e | |||
b1e9ba3ac6 | |||
5822ed0185 | |||
17b295788d | |||
6cd5b958c7 | |||
df92cc5f73 | |||
03106cdee3 | |||
c4638daf1b | |||
e2f5eb79ad | |||
b6c8e9bca0 | |||
573839019e | |||
815bf10ae0 | |||
7c257d97a7 | |||
59f8814660 | |||
1a6993099f | |||
f62ded3c8e | |||
4eca6366cc | |||
51a45b355d | |||
e5765cb902 | |||
6e922d2033 | |||
5d1695ada8 | |||
6e3ce29067 | |||
2b9bbc4cef | |||
9557145f72 | |||
249877b9c5 | |||
ff2321fde5 | |||
1edd6e4193 | |||
c7d47f128e | |||
14cb07efb2 | |||
a51724208c | |||
050e2b2b99 | |||
3706f15f7e | |||
bdde9162d9 | |||
99b4933536 | |||
c5caf7e0da | |||
bcc5308cfb | |||
9fb902db5c | |||
7461e38034 | |||
dccd85a151 | |||
910604fdad | |||
508cb9158e | |||
915c500d32 | |||
60bd6621c8 | |||
b5f6262763 | |||
2b8c4422de | |||
a686432c65 | |||
449e625877 | |||
1ac07dafde | |||
3878e28b56 | |||
cf6bc8666b | |||
f76063eb40 | |||
ed52c5824d | |||
9333400322 | |||
3689cfcc0d | |||
b73eceb535 | |||
5dc3453fc9 | |||
cef1139a4b | |||
ac96959947 | |||
4d73d877ba | |||
9f1186302e | |||
319dcc0d15 | |||
e99fdb8561 | |||
f37a342a63 | |||
09a039894d | |||
3efbb1a9fd | |||
920ee62ee3 | |||
1ace44fe31 | |||
a60f05415b | |||
42c9d39e02 | |||
a8186f1ed9 | |||
c2ff515a17 | |||
960c3ba558 | |||
454a9cd01c | |||
7d42ce1c87 | |||
57f6f980cf | |||
8cba3aae2c | |||
01b32f78ed | |||
b6066dfd5f | |||
3ad554ed59 | |||
6aacc6361b | |||
638e4e6410 | |||
aa9b7cccc7 | |||
41739c8528 | |||
89b32dc7fc | |||
44aec23251 | |||
12fd6160c5 | |||
8819abc418 | |||
96b627095c | |||
239f98aa9c | |||
f5d0511662 | |||
75582d2a26 | |||
dba004f924 | |||
5423a07c47 | |||
aba725372e | |||
a61aa9dd5d | |||
74349b20ce | |||
09ab9a1c8f | |||
abfe5789a3 | |||
67ebac496d | |||
60a2bf173b | |||
4e03f07864 | |||
aef1709d7f | |||
2f590f7be2 | |||
d5fa6ca89a | |||
8eaaffb25a | |||
28c5e2bab2 | |||
e212039f2c | |||
99b0b67f77 | |||
6ec9ba3c01 | |||
d7960a7dcf | |||
2a6e9af9c9 | |||
327e4d1f90 | |||
fffadd7b9e | |||
aaaf0d2e77 | |||
9f9a9b8c90 | |||
1f6edb3c0c | |||
142efb4f99 | |||
532655d2d5 | |||
287edabd90 | |||
7aaedbe2ce | |||
4cae1c673c | |||
8e01d836a9 | |||
f6dc8f0741 | |||
3a976d08d2 | |||
50e83b1eb5 | |||
61fbbb0b09 | |||
9e70e5c12e | |||
69d9b64468 | |||
0620d29880 | |||
b52dc74d9b | |||
a46aef2e7e | |||
736806a53d | |||
f1475e5cdf | |||
d04724c70a | |||
bacaadc16d | |||
c51dd235f0 | |||
92f2c9857e | |||
3998cc7f8b | |||
c126d080bc | |||
bc05f1714d | |||
e98becb94b | |||
250b94c8b5 | |||
47f03f6833 | |||
6e7ae48f78 | |||
526dbcc0e7 | |||
1abc5a5643 | |||
c81c350136 | |||
f90dc8bc7e | |||
072e22d072 | |||
59807c1bb0 | |||
7c19e1f1f7 | |||
3b9f915f57 | |||
d85cc530d4 | |||
2bb27c7642 | |||
e90e003204 | |||
b1e58e1add | |||
0fd836314a | |||
0bc3f08cc1 | |||
a78af5080a | |||
074e465284 | |||
bc8165d0ae | |||
ba8561d75a | |||
b2d381ba4b | |||
d39353d332 | |||
ee916af48e | |||
da1dc0309b | |||
30f4e7d833 | |||
cf3a86fb9b | |||
e1633f43f4 | |||
5b64cfc23c | |||
19709cf085 | |||
b8bb6c4f02 | |||
b7a543f8cb | |||
04b4e19563 | |||
ffb27fc66d | |||
8b5f7eefda | |||
c750bf4ee8 | |||
aa74019ef6 | |||
9be6d9f75f | |||
81ebb9b552 | |||
5e13b8c41f | |||
dd1ed948ec | |||
8b93f701cf | |||
2f0084de5b | |||
2ef9828625 | |||
89db8983a7 | |||
287dd9bd31 | |||
9a92054c1a | |||
4189036213 | |||
2c0a427ba5 | |||
77b488d624 | |||
5249e05746 | |||
1e7a0dd7a6 | |||
fd67f2402a | |||
60a65ede2f | |||
1fa659ce61 | |||
0ab903dbc7 | |||
70b0a04793 | |||
c0df9aa939 | |||
60a1886942 | |||
1ebf97871b | |||
72e321aa32 | |||
b0f602b565 | |||
84c774503d | |||
9bbc7cc651 | |||
458083fb6d | |||
8dcfc840b4 | |||
9d06a3a6ad | |||
86cd08b954 | |||
144c3cc082 | |||
802cef41a6 | |||
e128e8f0a9 | |||
8a25b93ab2 | |||
7a040935e9 | |||
2015882688 | |||
379301eb9d | |||
5d86b05cdb | |||
73c99d3157 | |||
acba197c94 | |||
2441d8ed8a | |||
9c123f37c8 | |||
b48dbd99cf | |||
25c8599d8f | |||
3453a17c15 | |||
6e95dacd3a | |||
a286e252e9 | |||
a8997e92c3 | |||
89137153a0 | |||
e3382de8e0 | |||
1a48681591 | |||
8f006f0009 | |||
77e32aad2a | |||
8d365dae53 | |||
01fb89674c | |||
e3144adc61 | |||
c9fb0ca6ae | |||
82d7e1371e | |||
e1341dfdba | |||
7f917311d8 | |||
2bfb856f07 | |||
702f52f1c9 | |||
7ba8649940 | |||
485ca28a29 | |||
33460afaf2 | |||
2421ac2c11 | |||
f0cdb0b80b | |||
2af953927e | |||
dcb9fbd0f7 | |||
5bc1f6479d | |||
f3e4bca468 | |||
54645f5cff | |||
a7f3e00821 | |||
108c281b0c | |||
58892cbb56 | |||
dae1053ca8 | |||
83a9778c30 | |||
c52157bfb9 | |||
62bf846d5f | |||
148f7fa316 | |||
f488327885 | |||
593b929254 | |||
9218e97315 | |||
beb0e8bd77 | |||
cace66e9f8 | |||
ef850c71fd | |||
aa8dc1919f | |||
c7c9b19853 | |||
68c26e0f5b | |||
6bcdf286ef | |||
d9345396e8 | |||
4c423900d4 | |||
504419b26d | |||
6e058eafed | |||
08fc9d8631 | |||
e8a11991a0 | |||
3e6d679838 | |||
4dad859c4d | |||
ef9c933ca8 | |||
0461190a67 | |||
06b3211b08 | |||
2033a9ce0c | |||
fca18d9209 | |||
4f99088449 | |||
b1da684008 | |||
89fb6de2d5 | |||
b665bae3bb | |||
0b5a7544ca | |||
183826ca0d | |||
e507aace6b | |||
43c93ef0b4 | |||
093e51e092 | |||
17e1655eaf | |||
6b570f2b9a | |||
f239d105a7 | |||
776d8378e3 | |||
dd770cd7c6 | |||
4b3de54e18 | |||
5741cd1b2b | |||
b780d7136e | |||
3c28a05746 | |||
57ac5badba | |||
e873eb5123 | |||
c1a63edd71 | |||
37a060c4db | |||
157e4ac485 | |||
ba4d9675a8 | |||
e011fb094c | |||
f55a934939 | |||
96a88fe865 | |||
6cdb83d730 | |||
95f06df45d | |||
ec52b357d5 | |||
d84546cd7d | |||
4eee4156da | |||
70f475d13e | |||
3a50a61b12 | |||
a217f617d8 | |||
fdfcd78f02 | |||
56d6d8001a | |||
c3ee8e10e6 | |||
2f42732deb | |||
956b8260e7 | |||
b7954f87e0 | |||
540ce55dc2 | |||
d71a2c98d1 | |||
cb9cec676d | |||
9f2755bc7f | |||
fbe5a1f477 | |||
338642094d | |||
a3270d10f0 | |||
4c5924556a | |||
99a9b62c6c | |||
1497672a4e | |||
01161fd8ef | |||
68f5ee42e6 | |||
53955a0713 | |||
2271fd43b8 | |||
6a44cfb876 | |||
37c90d53e0 | |||
9a5ac5f13f | |||
6603852355 | |||
5670a71e6b | |||
332b1f74d9 | |||
c28caaa495 | |||
74fed835e8 | |||
6bb7f7dc16 | |||
84bb2338d1 | |||
caa42538a1 | |||
f5b9a8de55 | |||
dd33f554da | |||
7e84d0b108 | |||
9e1217fbf0 | |||
26d3c7f9e0 | |||
76542e6859 | |||
3c4bbf8aa7 | |||
ccc5ac6a1f | |||
b34f86d2f0 | |||
ee5f73519a | |||
22e7ff1424 | |||
7a89888d11 | |||
64fe15cf8c | |||
336813646f | |||
53a18c462a | |||
b893eee086 | |||
792020dd18 | |||
0c11ba05af | |||
2cf82f510e | |||
7c0cbe3a31 | |||
08e659cf01 | |||
97c3f4fa5f | |||
06a24d35cb | |||
9b6d1a957f | |||
189bd4697a | |||
d52252cd55 | |||
303bbc8431 | |||
727dc471c2 | |||
45b5c21ab5 | |||
adddce7764 | |||
b35a9f8f61 | |||
34b46a9280 | |||
383a9953e2 | |||
94bc7127fa | |||
289f0efd24 | |||
3f15586dae | |||
b8a33dabd4 | |||
3a4b44a83c | |||
8c6303f016 | |||
b71f3f559a | |||
6541460821 | |||
e63cd68a3e | |||
5602dc58ff | |||
8ed3561a55 | |||
4e72bc3a72 | |||
45c41ad321 | |||
14eccc761b | |||
6b24c4284f | |||
ba44de87d6 | |||
846181cfc7 | |||
2a8849dd11 | |||
5a879ece3c | |||
ffd734cc2d | |||
ac414489ff | |||
10da0cab47 | |||
59f8119047 | |||
a28a1531d9 | |||
977d0dc761 | |||
a0de1b38ce | |||
401f390e5b | |||
e69febe0c0 | |||
eba4ddbbd8 | |||
49bb276c78 | |||
b48bccb706 | |||
a2160fc8ce | |||
af829e9ac9 | |||
2c5473da27 | |||
e248a58201 | |||
d85ab889b3 | |||
49eae307cd | |||
a2563f0b7d | |||
c8d2866ada | |||
2f8d89012b | |||
a3d9016cfe | |||
24c14ea8f6 | |||
c93e787393 | |||
b6cc3ee022 | |||
30e4112f79 | |||
067e148897 | |||
7fc6f7a7ae | |||
60fcb5fe10 | |||
627ccd024c | |||
9fa1460400 | |||
c780e9b9f5 | |||
c2f15a569f | |||
8311eaba1d | |||
22aa48e343 | |||
87cd37c467 | |||
a78a884c6e | |||
0bfd73bcc6 | |||
113bb35a2b | |||
9b27f25f6c | |||
e09acedf67 | |||
a80449333c | |||
64f71d4265 | |||
fb44700dd5 | |||
7d61be231e | |||
f935922241 | |||
d5bb7679ca | |||
b2a00737d3 | |||
3cd3e08ede | |||
14fe55e5c4 | |||
8cf0f96401 | |||
7da612c083 | |||
afcc3fd0ce | |||
ab9e163105 | |||
63f9c815e0 | |||
6b7e5b6bc3 | |||
2ec22f07d5 | |||
d4f03b6b9e | |||
931e002b12 | |||
247f9b0c9d | |||
0a72791022 | |||
4fbb5f2023 | |||
7589b204fe | |||
aa43abb544 | |||
85df0c2c6d | |||
9bcfa58ec0 | |||
11b2c6651d | |||
6d57872e85 | |||
8f98eff9b5 | |||
e18a34c987 | |||
19f1b4b14e | |||
497f747d69 | |||
5e2debc695 | |||
3ce571daab | |||
1165477360 | |||
05a223d5f0 | |||
0b21c04d82 | |||
21454b9168 | |||
699a3c8bf6 | |||
f9fc0cda95 | |||
9249932a25 | |||
4f6e60683b | |||
ad3cf3fab8 | |||
a1b4ef10db | |||
10fd863408 | |||
908ca9feda | |||
4c4987ee7c | |||
bbe23b911a | |||
8d21425ae0 | |||
78517164d4 | |||
3ad694d4de | |||
824076f730 | |||
5a7d452429 | |||
88fa63d848 | |||
a26a813ad5 | |||
385cb85309 | |||
d4f5a1fff4 | |||
771322aa97 | |||
f1177469c2 | |||
b122957ec0 | |||
ec421e62ff | |||
e9f7f9d091 | |||
cf39095351 | |||
94b618626d | |||
1e50427d62 | |||
73dfb7b91b | |||
88aaa18a24 | |||
3909e50d49 | |||
8683cf88fe | |||
d5376b80c2 | |||
3ec5aa84ad | |||
c80c399e6b | |||
9001e254cc | |||
754b5be768 | |||
064f3b2041 | |||
c6874194a0 | |||
43092ce81a | |||
1abf0bb0d7 | |||
1eb01ca9ff | |||
324b1fed5d | |||
ac238ff2b0 | |||
1d4cf2a446 | |||
89ddfa18b7 | |||
7b60ab50b1 | |||
250d1d66dd | |||
08cd62d924 | |||
4f7a1c784f | |||
8d0d17910c | |||
34e1621b84 | |||
93d90d0ba6 | |||
d5f3c789df | |||
652643f189 | |||
5cfb289736 | |||
570fbd5632 | |||
88efe4a523 | |||
a1db79ff47 | |||
bac159a42c | |||
54b129630e | |||
fdead071e7 | |||
0cfa8de0ad | |||
9adfa2200a | |||
f5ab145906 | |||
3eeba99152 | |||
58dfd3c25c | |||
f97d33ffc1 | |||
75212e643c | |||
22a0c9f401 | |||
7772550438 | |||
a887844a37 | |||
b61f442a15 | |||
0e20a26d6c | |||
b629af8dee | |||
f0ffb3fc10 | |||
96f0e6df2a | |||
fb4a7d2ba3 | |||
0b0321474d | |||
a633423b72 | |||
f5781e8ee7 | |||
2c318cf64f | |||
be330886da | |||
73a39bedf5 | |||
d04950cbc9 | |||
b4d924adfa | |||
3f1316183d | |||
b17724fdda | |||
41c2685dc4 | |||
b450e3db65 | |||
352d2a7bc8 | |||
47f2bc9cd7 | |||
2db0e8f68a | |||
f7d733b407 | |||
4b78ef52e0 | |||
f42e6764b7 | |||
8f627aa382 | |||
9c6e3da304 | |||
319927e1dc | |||
4909928d3a | |||
423d031210 | |||
ab5269ddaf | |||
96a6e81235 | |||
6d8b0e0539 | |||
f09ea971cf | |||
8030bf42ff | |||
008fa2b0c4 | |||
9040704659 | |||
7e793cabe8 | |||
f1a0887e9b | |||
f6bdd92f9e | |||
a0367066b4 | |||
00651c0c3c | |||
a7a3ec711b | |||
de5bc82382 | |||
138208bf82 | |||
0796b3dedf | |||
662a76bbb6 | |||
a664195625 | |||
e533e127bf | |||
346e3df009 | |||
19ba0873f5 | |||
fb4acc62c4 | |||
fd538e95ca | |||
def2d8b75b | |||
586b28af1c | |||
585c279d10 | |||
51bc65e671 | |||
ff1758cdce | |||
72a3c37e84 | |||
c99cdf5566 | |||
ad339710f1 | |||
00d2d12056 | |||
6a7b472c0e | |||
6991d868be | |||
85cc665d17 | |||
5bb22fc345 | |||
5417dc1bed | |||
ee20d33724 | |||
7887bd2b67 | |||
168582efea | |||
2c55d13f91 | |||
23e5f553d4 | |||
bc44eadcec | |||
a3e3136600 | |||
c7bfcee8d2 | |||
12ebd19716 | |||
aec9ffa5db | |||
2e6321342e | |||
6e71da62f0 | |||
5bf33aae75 | |||
06b2dc63ff | |||
1bb0c9dfc2 | |||
a2b167e120 | |||
0909a4b7cc | |||
fd7d2bb9bf | |||
63c40fd816 | |||
0569fa5e58 | |||
ba74952e0b | |||
20c28f785a | |||
e9b249ddc7 | |||
604bb484a3 | |||
010c93793a | |||
dc1d4a66f4 | |||
8ef633d7ef | |||
2176d33da1 | |||
5b794e2d22 | |||
ccd75d56c5 | |||
b700066833 | |||
546ee006d3 | |||
7f333a6a36 | |||
ae757ee371 | |||
69936750d5 | |||
442bfa4ed6 | |||
79e25e69bb | |||
b95c12772d | |||
de47525d7c | |||
f49d20e47c | |||
33b9917229 | |||
2a88e6802f | |||
bcc8b12e13 | |||
9b974505eb | |||
29b1c26771 | |||
02db20d98b | |||
757354df7d | |||
319d7dbe94 | |||
feb8eaf95a | |||
563518cf46 | |||
7c42d9082a | |||
040284af71 | |||
34f64184d9 | |||
b9abd74156 | |||
a1c0bfda6c | |||
617dcef09d | |||
d9c406800a | |||
54b869def1 | |||
d80a583979 | |||
99bfd7379b | |||
5f257382fa | |||
e3e6847c82 | |||
4ee0823acb | |||
d466123b1c | |||
21cbc14a48 | |||
b2f2c3e386 | |||
b03340ed10 | |||
5b563d8e9b | |||
2790487fc7 | |||
ad5a368065 | |||
7c0a631a9a | |||
4a8920749a | |||
8ab118dd06 | |||
e6661cb898 | |||
1671850714 | |||
d568bafe04 | |||
43dcce8478 | |||
ad70a4cffd | |||
6d4a948dd8 | |||
839ba6a964 | |||
b5cfdb9d0a | |||
9706338182 | |||
05f52c3d23 | |||
df3acb6932 | |||
b3c242595e | |||
26985f8d81 | |||
05e5e4efec | |||
e88be30fc8 | |||
4d67f16e94 | |||
334ec1870a | |||
ef5e4fccd3 | |||
8535edcfd4 | |||
bda76200d7 | |||
db0dc96cc7 | |||
6d62b5a150 | |||
217439d673 | |||
1f79a8f7dc | |||
7596786b18 | |||
2540b06c94 | |||
43eeaf3002 | |||
037cd150de | |||
ae0b059217 | |||
8255ce1158 | |||
5bf905723c | |||
3e336f4937 | |||
cd1cc37916 | |||
4ad7183d7e | |||
e1b52e7439 | |||
dca8c9f9d7 | |||
22496e36eb | |||
7e4eba6376 | |||
f642a56eaa | |||
c091089830 | |||
18900d20e1 | |||
6c622b1580 | |||
4290cd23b2 | |||
5076c1e93f | |||
884b701fc6 | |||
73a8ec0295 | |||
a29b6097a4 | |||
a9231e2ed8 | |||
5f4669a7a6 | |||
75c54df109 | |||
2a07f7151d | |||
b6ecff2dd3 | |||
83df27ec99 | |||
ca255985c0 | |||
82f34c38f6 | |||
694b4c8027 | |||
bd25621b2c | |||
fde34be270 | |||
7c7ce159fe | |||
5a57bb59e5 | |||
cd278f4ab5 | |||
8b24e23721 | |||
22fa1411bf | |||
2799a52d0c | |||
4c2e01a7a8 | |||
7267d2ef38 | |||
1eb6b154f7 | |||
f55d61bf0b | |||
5b350274bd | |||
b6d2f9f691 | |||
81106b5deb | |||
66e595e649 | |||
33b7bb6184 | |||
7d9130b2af | |||
482d71743b | |||
1db37a4727 | |||
194d16ff91 | |||
b1e2284c0e | |||
70d1aa70a3 | |||
3b17d6e0ab | |||
9a5819b93b | |||
a260cd67b0 | |||
64111fb0ec | |||
faf2be23d9 | |||
0eb4a6a315 | |||
85673250ed | |||
09daa741ce | |||
55e2379aab | |||
9937977203 | |||
c897e7491a | |||
0a74a95283 | |||
74ef2c3dff | |||
9976dfacc0 | |||
659f8ddc7a | |||
9e4cc2ae57 | |||
a27d78fcdf | |||
e507435bcb | |||
d5f234909f | |||
c17f721625 | |||
600705130f | |||
5c5dce1422 | |||
53585bf2f0 | |||
116f88a503 | |||
aaba8cd2c7 | |||
b67aeb0d3a | |||
f620562d68 | |||
5231d0eaa1 | |||
cb470e3573 | |||
0a0f90aa2e | |||
635207d12c | |||
5e4a829413 | |||
b13b3fd92e | |||
564dc8e6f1 | |||
6e4cced8c6 | |||
29a4a5027c | |||
ee327448b4 | |||
d078960c5c | |||
2e8cd375fc | |||
1f6751cb01 | |||
3cca4e31cd | |||
b93902800c | |||
70f6bb3fda | |||
c075cb6311 | |||
d7db85b062 | |||
b442e7d958 | |||
a495ae637f | |||
94748a96a9 | |||
7657429054 | |||
2ff6dbf975 | |||
4f34628c14 | |||
6ff2111cee | |||
85957980f6 | |||
a6dcfe2c87 | |||
c32d590fbb | |||
ab41462f71 | |||
951f16b1a5 | |||
b5818888bb | |||
1a326bf7e4 | |||
e1afc1cf7a | |||
bb007ddce5 | |||
b5dd0317c7 | |||
3c54541a73 | |||
2657f01135 | |||
7223409eb1 | |||
c41eae63e7 | |||
c8b85c43aa | |||
e918788c3f | |||
b53f4f997c | |||
481d93ebc4 | |||
1ce666f136 | |||
49a8e702bc | |||
5d59e652d7 | |||
02af8c7311 | |||
fadf4e867c | |||
0839859fef | |||
c122b48e35 | |||
cebb297bbf | |||
2e31b796f7 | |||
e0a61b51cb | |||
46e50e622b | |||
7cfa1df0b2 | |||
8a63648339 | |||
bb6b026720 | |||
2a13b314dc | |||
4506b3f6d4 | |||
804abef0de | |||
7e932b920e | |||
46fdfbc507 | |||
a4ff8607c5 | |||
7fe4eeda57 | |||
9f25cddaa4 | |||
eb64fe60d0 | |||
36f404e17d | |||
5398590939 | |||
96d5cfea14 | |||
79007ebd55 | |||
fcb519dac3 | |||
2b487aa959 | |||
733feadcb2 | |||
5ae568f19c | |||
0e51807856 | |||
b6eb165207 | |||
d26dbf4b3d | |||
e06ef01f72 | |||
a722582709 | |||
de64deb5c5 | |||
7de54a2cc4 | |||
c788b8cc82 | |||
cb8db1a541 | |||
8a7a551c3b | |||
84d180b810 | |||
de261a27ca | |||
28288a8f7b | |||
cd8548fc32 | |||
37dbd49589 | |||
5af8d67b62 | |||
72e6309c4a | |||
18f0f3cce1 | |||
8c3e9ff192 | |||
21d6d27435 | |||
39ff7913d6 | |||
402c6fc64a | |||
a1f934466c | |||
15aa114579 | |||
b9cc82e37d | |||
8af9c8b150 | |||
7dcc985222 | |||
9c2bc19897 | |||
801b08359a | |||
c469dee158 | |||
2aa7eda1e9 | |||
f1246c9e00 | |||
2749cbe4d1 | |||
d2a9280ab3 | |||
8e25ee0fc9 | |||
55026f913b | |||
7cc40e2453 | |||
cb6b19952d | |||
983af57842 | |||
3c2820dc31 | |||
1c25b69160 | |||
641cc38ae4 | |||
cd68b07e19 | |||
2b252c9abb | |||
e2e8370bb9 | |||
e9e31394c4 | |||
2825ccbcd5 | |||
377a14ff72 | |||
a83997b9b4 | |||
3e155d8e80 | |||
6953b72ee6 | |||
ab370a1dda | |||
20845e5860 | |||
9ed3257006 | |||
2221e30c0a | |||
ce43dca23b | |||
4acf5d0931 | |||
b742ed73aa | |||
5156b2e0cc | |||
6b81cf4a24 | |||
cca3a68fe4 | |||
adb2904872 | |||
d68b8d03dd | |||
e7afb88f22 | |||
48cbfc64c0 | |||
0b067858bc | |||
2d44d98f17 | |||
74ef3096ae | |||
8f1163fd75 | |||
a240d503c5 | |||
e331a641b2 | |||
85db4b8e0a | |||
0aa139cf4a | |||
4140ca6fbd | |||
a8ce9da9ee | |||
476a33c0ab | |||
8e868c9fda | |||
17c8b1a172 | |||
b374c04d73 | |||
e750d824ad | |||
dd4c50c3eb | |||
20cc869299 | |||
7214dbccdb | |||
e6cebdd546 | |||
0301003ccf | |||
de2efe0c01 | |||
90d765d7f6 | |||
3e0a1721ce | |||
7214fbcd4c | |||
4b8aecfe91 | |||
387c71c0aa | |||
8d5ce21df4 | |||
f6dfcfbae9 | |||
69e9595db9 | |||
de390678fd | |||
cf9a7b8c60 | |||
73e9c16a8d | |||
9775623981 | |||
20b7bb3c99 | |||
3defc16658 | |||
0dbe592182 | |||
a7c0e5bdaa | |||
3b051cbbea | |||
f7edfd1c29 | |||
b182c43965 | |||
4639e7ad2e | |||
56241203a0 | |||
8c8540de5d | |||
b45af78322 | |||
98bcf3bf7e | |||
e28bcfced3 | |||
a5bd990245 | |||
58301e0844 | |||
c9213fb6cd | |||
641819a364 | |||
3ee3a8b41d | |||
5600403088 | |||
3b00bace23 | |||
fcba470aad | |||
206e602d73 | |||
f98d1aaade | |||
310f857257 | |||
a2b1055094 | |||
f23ddecef3 | |||
54687ec3c0 | |||
545f0fcea5 | |||
5db8ce3043 | |||
ed48669ae1 | |||
69c3befb2d | |||
fc39e837ea | |||
6df9f28c02 | |||
f3d0947427 | |||
3326a4cf2a | |||
9a6ea87b0c | |||
12179d0ec0 | |||
d4141fc51e | |||
c32ab6181c | |||
3847882599 | |||
4db157f663 | |||
351b4e84c9 | |||
0c65561bcb | |||
00200f75a0 | |||
58614a74f5 | |||
f3d64663a0 | |||
8be8c270f8 | |||
a56037f1c9 | |||
2ff7955ec3 | |||
f2044c4d26 | |||
4113f0faea | |||
bacd09484a | |||
8253eb62bd | |||
70b659a0a0 | |||
79ed74ab17 | |||
93bb3ebd69 | |||
e8e516159c | |||
1431c031a0 | |||
209c2183e1 | |||
0c98c282a0 | |||
58c10796a1 | |||
603e6a99f3 | |||
6622ebc04e | |||
465dbb4a8d | |||
08ae908453 | |||
c35a1e7c50 | |||
ecb22c3829 | |||
eebf969f7e | |||
5816f25c3e | |||
b2a81d880d | |||
b10c1476a6 | |||
e11cd09a12 | |||
27575eda68 | |||
f33b9a1ec6 | |||
7c45fff7ba | |||
ecdb0775cd | |||
6ef90a56ed | |||
71b86ff43b | |||
0535e06ae1 | |||
6261f5e7cc | |||
f256b74929 | |||
4f1182a230 | |||
e7c20547f8 | |||
9ab4c510fe | |||
7d78c52064 | |||
6223d91291 | |||
840b5e1312 | |||
e69813f6e3 | |||
3c0c057e06 | |||
984d12b3f2 | |||
61dc54f115 | |||
34e47cccc1 | |||
c170345550 | |||
1e40706f72 | |||
ea1a747ebf | |||
a14e967020 | |||
0fff10d2c6 | |||
7c2123614d | |||
d149866703 | |||
18039140db | |||
4de9599018 | |||
bb85829d71 | |||
ff077943ec | |||
f057114bcc | |||
e7bfe7f80d | |||
18112a97ab | |||
8ee6fb58ac | |||
08831fc31d | |||
c5c25394fb | |||
2f649c9866 | |||
91c5dd40fa | |||
e95e688cf0 | |||
9845f1de08 | |||
07032d312d | |||
ccb5d32763 | |||
bf83e4b03b | |||
03b491763f | |||
3abc9edf0e | |||
f9accc51d3 | |||
c3bade81b4 | |||
6edd1f00dd | |||
02be899629 | |||
8e043f289a | |||
30fecf8578 | |||
1112da33e3 | |||
ffa65e871e | |||
f49c7b465b | |||
e6f75156ec | |||
ebafeb19ad | |||
5166c719c4 | |||
bf92ea8340 | |||
cf1e595ba2 | |||
2bf3296c0f | |||
11513f73b7 | |||
b6f60c6835 | |||
e9d276010f | |||
b08c4b0b29 | |||
d684807d96 | |||
9a60ef7c47 | |||
cc446059de | |||
d75b809c13 | |||
9fc3998cf7 | |||
238baa72cf | |||
089f0f7a87 | |||
aa9d3d1931 | |||
2fc6aed4f1 | |||
c2fdea7886 | |||
c8f71946d4 | |||
d1cc6ed88d | |||
f6e6cf3750 | |||
d1c7491704 | |||
fd49c2fd23 | |||
f7fb2efcdd | |||
ff0608c202 | |||
e63e20eade | |||
335292cf4c | |||
7cb927c8b8 | |||
802d6b3dad | |||
c16bf28369 | |||
0de76ae613 | |||
880396e3a6 | |||
879fc2812d | |||
6ac6209bd0 | |||
d7fd76c568 | |||
543e08276f | |||
2e647b9196 | |||
69cf556582 | |||
e168ee2ae6 | |||
274d758ba8 | |||
b52e35be7d | |||
7f1ba8f166 | |||
72a5b9bac5 | |||
34fb0c2753 | |||
e5f0885cb0 | |||
4f93190162 | |||
9d1dcd278a | |||
45d4bce0e7 | |||
680a7206d3 | |||
8a08e9fd64 | |||
0080dabe09 | |||
556ce60b27 | |||
12857e3027 | |||
10965b82a9 | |||
86884607ef | |||
1ff0449332 | |||
57b056b388 | |||
9058e9ac9d | |||
ad3de8bff5 | |||
476b100b04 | |||
b2c7c86609 | |||
f8a8ec2e4d | |||
393a5ba125 | |||
466c2d3eb4 | |||
b325b3537f | |||
e429127313 | |||
2d05521789 | |||
564ffc2be9 | |||
feaf34c124 | |||
c1e0563eba | |||
1c66f35337 | |||
4a7dd64982 | |||
a84f984a07 | |||
1f31a228d7 | |||
309308db15 | |||
a02c38ac45 | |||
49e495dbbe | |||
f6c2ccb0d6 | |||
dcd30f2cad | |||
e5d540ebd2 | |||
1073a610d6 | |||
034f6f8b0e | |||
3edb23be97 | |||
d308c04465 | |||
c5899eba94 | |||
97cb20b731 | |||
f58a5ad524 | |||
e00692956c | |||
b2c1b41981 | |||
ffa8440d1b | |||
32f66b3eaa | |||
42b196bd0b | |||
68dab45931 | |||
af2dbb0389 | |||
5abbc7f9a7 | |||
dcfefad17f | |||
4ece6457fd | |||
53e38336fb | |||
0b16df7731 | |||
900125d92e | |||
6aaaf5a9d3 | |||
bd2f6d8fee | |||
baae22657e | |||
46264c85f4 | |||
2811eb6024 | |||
218c1a5a50 | |||
ab5287a3d4 | |||
d55c62c073 | |||
4833c34800 | |||
fc70e657f0 | |||
ee23f629f6 | |||
44402c9571 | |||
ffefb38161 | |||
6d667f653e | |||
1c75fed727 | |||
e7837aea88 | |||
9c133be779 | |||
71eb953fd3 | |||
f49ef21fed | |||
6a6fa04ba0 | |||
83b0838c94 | |||
4ebc1e671f | |||
08c7e38587 | |||
b863d9feb3 | |||
e527f043b0 | |||
58bb403787 | |||
e4725c23eb | |||
b0db8caf65 | |||
3bcc6bdf93 | |||
eafb75a958 | |||
31ca0939aa | |||
7784fdcd6a | |||
8247eef735 | |||
cb6629f301 | |||
3a6fe1b374 | |||
0ba2f37004 | |||
e052dee753 | |||
9c2ec32d12 | |||
1669c38bc9 | |||
c6ce6d1b49 | |||
bc242b0aa7 | |||
41b67f6af4 | |||
bef21e1cb9 | |||
8c73630f5a | |||
724953d5b7 | |||
a22b231982 | |||
910bfe2318 | |||
70a524da46 | |||
bf6c846fac | |||
b83e4bef3f | |||
9f7fe0d8f7 | |||
741dee57e4 | |||
fff4dba708 | |||
f4f7ab3e49 | |||
88fe99b1b8 | |||
92c1486f6a | |||
caea64cef3 | |||
90783d8ee8 | |||
be57801e21 | |||
ff84786b4e | |||
1e863672cb | |||
fb98a9c383 | |||
05163f22cb | |||
160f12d7d3 | |||
49e4e36184 | |||
c4f8879cda | |||
8f54166653 | |||
b9f020c447 | |||
c357f3eb4d | |||
7ebbb0417a | |||
6e4b4173b5 | |||
87ebad7efb | |||
3294aaed3b | |||
0e21f3eab6 | |||
9fcf692cb8 | |||
da577ea3cc | |||
6ae1d8938a | |||
3e18a7390c | |||
5f43f1afc6 | |||
2fc9c03430 | |||
d951a9ba02 | |||
93385af675 | |||
dd75d0ece7 | |||
dcd37ed916 | |||
2e4d722d7f | |||
09543400ca | |||
8b101e5043 | |||
b31fff9c2b | |||
0c5b100dd9 | |||
253825a35e | |||
8937d19891 | |||
0fdd9e75a6 | |||
77da00c2c5 | |||
3744080d11 | |||
c9e546a8fd | |||
6691992a79 | |||
1ea0f4c339 | |||
8bfa117be2 | |||
b3acecdcea | |||
ec479c7e91 | |||
fd7760d9ed | |||
c9fcec6889 | |||
fd901ef2cf | |||
8afdaa8f0e | |||
254bfccc62 | |||
5b4aeca63c | |||
17871daf0c | |||
cdd4460968 | |||
fa6a37880b | |||
d4e1dabe12 | |||
a3fd376b24 | |||
aaac1f54e8 | |||
41c0329822 | |||
74d48fd7e1 | |||
9c3c953129 | |||
f5cae18da7 | |||
59d47592d9 | |||
2b6c991190 | |||
26020ba8bb | |||
b573bc20b5 | |||
210dbfa265 | |||
b37cac93ff | |||
eea953efb6 | |||
7ad9d7b291 | |||
20937c4f72 | |||
dbbfa07639 | |||
9e1a4cad5c | |||
02bbedcfca | |||
cd70d90914 | |||
819f297de8 | |||
0608adde89 | |||
ad7bcf4669 | |||
2eccc86e83 | |||
16d18f23a1 | |||
5631ae1b6c | |||
5fb29992f6 | |||
910d633ac2 | |||
32f8380e56 | |||
43e4dd6bb6 | |||
4f0b1688db | |||
9e75ee09bb | |||
9ae8822e00 | |||
babffd1fe6 | |||
5615d62032 | |||
4b89d15c1e | |||
815f510d5f | |||
199ba193be | |||
4ae9bd3f9a | |||
1c9cf639ea | |||
0040464ca1 | |||
79997efbb6 | |||
0e42009798 | |||
93fdcb8739 | |||
aca926e202 | |||
9941027b10 | |||
9104de4290 | |||
5dc692809c | |||
8dc1d1bd21 | |||
fe588485a9 | |||
19ef1d7025 | |||
62523a8662 | |||
6e97665e2e | |||
4988680353 | |||
c5de5c20c7 | |||
1a0fee1aa2 | |||
06a44603cd | |||
e48459762e | |||
235ebeae97 | |||
dfe909606e | |||
6fd0c7726c | |||
819e045811 | |||
1ba780598d | |||
aeb0cb3110 | |||
88923838c5 | |||
df9f6fd7fd | |||
98e46d6ac9 | |||
daff614fb4 | |||
5ea324c7f2 | |||
23fedbf94a | |||
593d66d8d6 | |||
851dcd5bf7 | |||
2e919681ae | |||
5da68cd48c | |||
27fdaeff46 | |||
53c0079656 | |||
93780b77e0 | |||
b712ed0421 | |||
ee96f1b345 | |||
d13464df3d | |||
6bde2e4d96 | |||
0a4953c020 | |||
96c488880c | |||
7e0adf3f66 | |||
09f716440a | |||
2251c84171 | |||
5cfe78dcd1 | |||
6a48325132 | |||
294be0a79a | |||
c94b264b44 | |||
7220c4e3e3 | |||
5aadeba2ec | |||
0f47a5b6ba | |||
36f32d28f2 | |||
6d69ccf229 | |||
37073b42be | |||
837501c948 | |||
b300966fa8 | |||
730eb06c84 | |||
aca8d3f4b7 | |||
b5b3af4659 | |||
6cd231426d | |||
0c7cd1cd75 | |||
2425704ead | |||
4e22159206 | |||
52cf1ba02c | |||
516e84182f | |||
a3a9853e18 | |||
08e26600fd | |||
7793c2c6ba | |||
9e826d16dd | |||
80618bbd9c | |||
38ad47ea75 | |||
45ed359bef | |||
fcc26c3e7a | |||
d4ff6b1f40 | |||
557de34eea | |||
e034dc4d90 | |||
cfbd1e5e4b | |||
0df661819f | |||
1a9f6d10d4 | |||
a787215c95 | |||
64ab400af5 | |||
a463878bf2 | |||
9f72024c61 | |||
243fbd4dc9 | |||
4e6a290693 | |||
ac05d529ca | |||
b38d79004a | |||
f4a547df11 | |||
2b87c35058 | |||
b11833e450 | |||
fa8e119514 | |||
677cb5c330 | |||
6e74c79bfe | |||
54474f5908 | |||
99cc0f519b | |||
92a01f89ef | |||
fd83a0c743 | |||
988e46c875 | |||
f081c2fc20 | |||
b4b376a1a5 | |||
0db4179d47 | |||
795b7c6234 | |||
091b9a57f5 | |||
564e1422ac | |||
8ed4ed3fc4 | |||
29fe4566a7 | |||
ae3bfb28ed | |||
14aab97d8a | |||
52d7a47cd7 | |||
f338dcbeed | |||
dcec058a22 | |||
2bdc6b156b | |||
84ca9e6b81 | |||
11cb0fd2db | |||
3f620ffb6f | |||
1a0e05d073 | |||
a4d2de23a1 | |||
85cecc9811 | |||
9899f742a8 | |||
b5484740b7 | |||
016b15b437 | |||
6fb936798e | |||
a692b87843 | |||
19663885a4 | |||
49b87777f9 | |||
d4523bb1e6 | |||
e3200899e2 | |||
36c7a1ab9e | |||
c54fbd5eca | |||
bbe828071e | |||
23f6c7db00 | |||
b1ea9e7a71 | |||
fb71d0e272 | |||
fa72a29999 | |||
af77b31d54 | |||
8280dace26 | |||
ecaf1c7b7c | |||
8702ec29a8 | |||
d8206434bc | |||
c71c2a8710 | |||
e55b881017 | |||
ab906ec417 | |||
0b1ff529f7 | |||
85a6835748 | |||
259271bc0f | |||
b7bc0f178b | |||
688455d0aa | |||
3c96d2ea42 | |||
ab81481e5a | |||
a429ad5dcf | |||
5e1c5b510b | |||
9e63183f4b | |||
b1e740f026 | |||
ce4ea55438 | |||
18ab7cd22f | |||
8807743daf | |||
aad50377ff | |||
4b3ae58ea7 | |||
ce2c68ecc9 | |||
0c155a7a2e | |||
afddfe8b58 | |||
5fa0915271 | |||
6a0a170b17 | |||
4dde5b6e45 | |||
4b93a1cb9e | |||
e3a0639a0c | |||
4d3220820b | |||
a4ac9fb0f3 | |||
1ff79ecf07 | |||
1166b16420 | |||
213224942f | |||
ff16e66275 | |||
3c338e983f | |||
2c11ba6520 | |||
9a21656706 | |||
e96ee5ba53 | |||
b421633a8a | |||
e2e0d62560 | |||
c71fb06940 | |||
e2171af99c | |||
8cebf049d4 | |||
ef139ed1cc | |||
d717de006a | |||
a44f091878 | |||
1b37ba5339 | |||
bbaa90e997 | |||
86e6c4a419 | |||
4159883791 | |||
d8b00da3a1 | |||
a24945bc1b | |||
158759493f | |||
36e32d6ddc | |||
84908e2ec0 | |||
a2dc51d811 | |||
fb3b0e2c29 | |||
1a3e4c68bb | |||
11b2342da0 | |||
80d4a808d3 | |||
da4146eb59 | |||
a0be35db8b | |||
14db9cd523 | |||
0c315385dd | |||
c0a0eb02fb | |||
ee407c32ad | |||
9262d21829 | |||
a13f710325 | |||
eac1a6036f | |||
987f3d7586 | |||
875322669c | |||
33a264b3d0 | |||
c059eff170 | |||
b4a22fc9dd | |||
6d1cbdc463 | |||
2bfbba4daf | |||
21ffe82bde | |||
8e6f597027 | |||
16c5065560 | |||
c4b985f1a4 | |||
042747c7d2 | |||
e4a46f31de | |||
6d9e62d2b4 | |||
9caaa507b1 | |||
5c7d3c5b44 | |||
8bac57d87a | |||
b8d759cd63 | |||
da72e3e5ac | |||
2afd36fee0 | |||
b7e75d8828 | |||
30e20f4e7d | |||
ce0ab8dccf | |||
5b20ab2f7c | |||
daaaed43df | |||
3a4bd791ad | |||
eecddd7f6b | |||
a34eaa136e | |||
53be8b5e96 | |||
f0ae5ea908 | |||
9910556a8b | |||
5997416e1b | |||
9a9fc56f85 | |||
2a5e919f29 | |||
8031d51e15 | |||
56ce9c0d0d | |||
8cd584cbd5 | |||
f5b87f4669 | |||
a1a65c5529 | |||
832434095e | |||
b85f1ef351 | |||
8bee5d788e | |||
0752d857e2 | |||
07e4056694 | |||
0eb4ab85b3 | |||
69ef47daf8 | |||
6eaa1f69ac | |||
5aab75fae0 | |||
7407c98005 | |||
dcd4322e44 | |||
81a4d46b08 | |||
e85895ab55 | |||
095bdb16ba | |||
68de7f897d | |||
51b4c6b1bd | |||
6d4ac977c1 | |||
a73fc5ebc1 | |||
3c8461a39f | |||
de76d06e48 | |||
a27c28c24f | |||
ed234ec88b | |||
7a0a046596 | |||
0641151ca1 | |||
c6dc2377fa | |||
a3050b3983 | |||
69c15bd473 | |||
2be40816b2 | |||
a98bb25133 | |||
d130c23f5d | |||
cd936ee4ef | |||
67e3dca0c3 | |||
de53f1ff40 | |||
d79081dee4 | |||
449f100bc0 | |||
0612b2d0a4 | |||
583a3e541a | |||
9d8d30a864 | |||
95cda8538f | |||
b116f22152 | |||
ff2fb0d6dc | |||
020823e933 | |||
5879972924 | |||
8031294230 | |||
e0a6935c49 | |||
3d581a5454 | |||
317ad8386c | |||
7ad5011280 | |||
76990702f0 | |||
9a27824fe9 | |||
d877d90bf3 | |||
0b790c47e6 | |||
6b49c8dd95 | |||
e56f9b144e | |||
3c82944daf | |||
5e3070a6c4 | |||
a3a0e9eebe | |||
c6593f03bc | |||
54dc4f650c | |||
dec7af0381 | |||
ae001eea54 | |||
4d0e17a11e | |||
c708e619e9 | |||
2cceb3c929 | |||
aee8704691 | |||
43af60237b | |||
e615479b41 | |||
973fb4d2d5 | |||
964feae846 | |||
ea3d8a5634 | |||
48aab2d92a | |||
00eab73954 | |||
5f6f8b12bd | |||
c2d4d6fd49 | |||
04b660ff9b | |||
c292d926be | |||
23b83ceef7 | |||
1324648db6 | |||
735bff3348 | |||
05a6aee782 | |||
c7349c2556 | |||
51f3d06752 | |||
31759d86ab | |||
7c6eed99d2 | |||
bc4b0ec17d | |||
f766348b87 | |||
82281303d0 | |||
1caa17beb0 | |||
1c4d346f9f | |||
4320efb049 | |||
a756423768 | |||
8525fc74c0 | |||
30c0cc5aa8 | |||
b3bbd7c07d | |||
09d4ba2bb0 | |||
30315027c1 | |||
05acefe70e | |||
6c14758e33 | |||
b93ec20119 | |||
ce04646576 | |||
9282dfe491 | |||
fca6280bcc | |||
cdeb575ec6 | |||
271dbe4fb7 | |||
9a0337114d | |||
2d28f4eb55 | |||
f673927e16 | |||
52896b82a9 | |||
9d53ecb0cd | |||
aec3ac32e5 | |||
f150f1568e | |||
309189c55d | |||
f68c54cd3a | |||
bef8545161 | |||
c21cd14ac2 | |||
275d7f0072 | |||
58c8306cf4 | |||
f782b684ad | |||
092b2a5f52 | |||
42b2d40ad6 | |||
3f6fe6cfc0 | |||
1abf542a74 | |||
c4720ca03d | |||
4316878cce | |||
c180d75a83 | |||
4a040b32c0 | |||
ea330a1eef | |||
2451ba0a77 | |||
2c276a56e5 | |||
708030b8b5 | |||
d5fc0582bc | |||
f9dce82c83 | |||
e82602f994 | |||
1d36395e5a | |||
8f8857bc22 | |||
226247b3b6 | |||
b2ea5014f3 | |||
48bc416aa7 | |||
386e7203b2 | |||
9bdb224631 | |||
dd36aacbee | |||
6b57b1c720 | |||
9e9e6d41ff | |||
5140389502 | |||
fc6328131f | |||
9de0083ca6 | |||
f5231b840d | |||
afb6596c4b | |||
dde9afef92 | |||
6595e9c3cb | |||
c0e3b5fe06 | |||
6b8f3bbc51 | |||
9a5a021e64 | |||
14c05fec8c | |||
eaf7a455cd | |||
55bb21f3ee | |||
f123bc0912 | |||
572eb7b1c0 | |||
2befaff8a8 | |||
437a9ce2d3 | |||
1b967b250a | |||
e221f39e07 | |||
21a8838a24 | |||
fad91ccae0 | |||
825914aa4b | |||
a8246d12ee | |||
abb8bf2ebb | |||
7e7071305f | |||
cc8b2e72c1 | |||
a3d6ee44a1 | |||
ac99e2f41f | |||
bf1839c061 | |||
fd5c132a40 | |||
4dfa268eb3 | |||
332ca084f5 | |||
01cbb8680a | |||
bbdaaf30bc | |||
0550b9ff8b | |||
b1a4c5cca5 | |||
785080e14a | |||
3c7e093a3c | |||
89be9f3a86 | |||
6f2ffa7861 | |||
7091f283f2 | |||
2d28003451 | |||
f0ba7d3c0d | |||
cd5f346895 | |||
66cd5aef0c | |||
6f8ec53e8b | |||
622504ff72 | |||
c9d47c483c | |||
07098b89a5 | |||
c583b83cbb | |||
1670e1fe42 | |||
de8809608c | |||
0e194ee045 | |||
4205f6ecbe | |||
4d90ec60e2 | |||
d126a6563b | |||
aecb6ae79c | |||
a65c826717 | |||
66c3705f2b | |||
d18ebb45f8 | |||
d8e01f2c5d | |||
4abbaa3841 | |||
42a463b348 | |||
8e15cf1d45 | |||
2468b4108e | |||
528b1bb607 | |||
b4449bb289 | |||
737e00b490 | |||
55d4c7f4ab | |||
7afb078efd | |||
2c04f6c1e9 | |||
2ad5ed7e73 | |||
f2b7fe46a2 | |||
1a1af62f62 | |||
98f715e652 | |||
fa5f1c230a | |||
c92ae9cfa9 | |||
3dcb3a1a5b | |||
efde71d07c | |||
bff8cf2f32 | |||
72730135f1 | |||
50cf27b686 | |||
b293abffa4 | |||
be84ea299c | |||
d54586426a | |||
6ccf72c707 | |||
5817118461 | |||
ebac1de111 | |||
0d2f841b27 | |||
780ca383c9 | |||
a652a0f4f3 | |||
5bdc812c43 | |||
357bc8d19d | |||
85b54ac011 | |||
17f888019c | |||
947fd0564e | |||
bd51d02902 | |||
36d75c8641 | |||
c75f158b48 | |||
bb37ce9cef | |||
77ff33570d | |||
20383d60a9 | |||
f15c0ecbb0 | |||
79aa5ac5f2 | |||
8be6c0d1d2 | |||
7f5a9e77de | |||
ff19ab8b08 | |||
63dcb2ad39 | |||
795e8ed0e5 | |||
bccb56ed61 | |||
02e2ad89ec | |||
a236e2e5de | |||
ba294c85f8 | |||
beb3dca495 | |||
04101536c6 | |||
2912e7e5dd | |||
bf6fadbde8 | |||
001b49d09a | |||
bbd5bdda95 | |||
7e950e8e2b | |||
8b0efbc737 | |||
93cfbd6696 | |||
acc1d028ab | |||
3476b5acc3 | |||
72ca5da842 | |||
e214280fcd | |||
bf32987a3e | |||
8941fe230b | |||
b6d4abee21 | |||
786bdc41c2 | |||
ed9f08f678 | |||
33fd6768f1 | |||
87b8456531 | |||
a12bde4656 | |||
6f219a4c2a | |||
49d7818b64 | |||
fb0be3272c | |||
994f7d6bea | |||
6e8dcecaf1 | |||
40237374a8 | |||
3d98860369 | |||
804fe33665 | |||
703171f96b | |||
27bdefeea8 | |||
7a3c74020d | |||
7509170dd0 | |||
cd17a97916 | |||
d5e690f964 | |||
a19bd20b6b | |||
f78526dfff | |||
11d6a2020f | |||
fabd48a22c | |||
e2ea98b5ef | |||
4473ab0704 | |||
1f68cc305a | |||
ec2543551f | |||
7b0bedc755 | |||
ab054ca515 | |||
1b49c7804c | |||
764a288b1a | |||
fc6910bc2c | |||
91dd1dcddc | |||
97e6aaca65 | |||
af5ff1ecfb | |||
c9b53b0d3a | |||
d05a62e1ea | |||
a83eec31d8 | |||
729503fe31 | |||
7137ff4257 | |||
6db11a7433 | |||
8666aa62dd | |||
eedcd7a2a6 | |||
e3e8fb663a | |||
6e663210ee | |||
42cd0fe2f0 | |||
daac05c1ad | |||
1d63c393a3 | |||
e8a3751b32 | |||
cb8a41d5be | |||
a8d4f7e23c | |||
93bcdac3be | |||
32d0388556 | |||
633a32ffd6 | |||
cb8b165c8e | |||
57134359b9 | |||
377436e46a | |||
51129aaeff | |||
a2bd5050ff | |||
128c416ce7 | |||
7184773521 | |||
1138313028 | |||
46bd319ebe | |||
cfcc48259c | |||
785ce7a8ab | |||
ad5de216b0 | |||
26b80d6af7 | |||
a8623d8066 | |||
86ab9f72a5 | |||
b3892dab8d | |||
57a5d034dd | |||
cee9569581 | |||
159429da6e | |||
a292cb0b4b | |||
d70985d8d2 | |||
484f95f5d2 | |||
6e0553af9b | |||
cb18d3d765 | |||
f316f38ae5 | |||
5f07cb374b | |||
96d31e07c3 | |||
99a5efe36c | |||
5c46ecc0ed | |||
cf93b68816 | |||
457421b8d6 | |||
d36ea9539a | |||
5a5337dc63 | |||
443081cc28 | |||
ac8503f8c8 | |||
1cc1fd0a5a | |||
34314aa4ca | |||
0d8dcf4829 | |||
47c6d0dd62 | |||
84937e3eec | |||
303e270b56 | |||
29fbcdc0a6 | |||
bb1ada6e14 | |||
4a422cc796 | |||
be0f244c02 | |||
78a8dc8458 | |||
38062af889 | |||
f2eadf5441 | |||
a42931384f | |||
8116ce697b | |||
4964b86d67 | |||
2b331e7655 | |||
c1468b688e | |||
4f7837c88e | |||
fd8e06f1dd | |||
b01a351eaa | |||
604655c02d | |||
6603ac4389 | |||
cca6f952ee | |||
df94a6322d | |||
73e7f64860 | |||
e17e1650d5 | |||
3ecb63d500 | |||
41ee7e90ef | |||
c70bba727e | |||
747248454d | |||
59386241b4 | |||
c70b9b0dd1 | |||
2ee00ed919 | |||
cbfc271da5 | |||
d45b492837 | |||
ed54c145b7 | |||
64ed9a6044 | |||
75267abd91 | |||
ba9a3992b7 | |||
a74c32ed6d | |||
c5f9812acc | |||
bb0d6853e5 | |||
8c9fe168d8 | |||
6c874c01b7 | |||
5bc84b621c | |||
dd421eedf5 | |||
570d8a73cc | |||
a95df42843 | |||
4ecbb30a1b | |||
96b40b9c49 | |||
c32eebdd46 | |||
5b17287555 | |||
fb01257c8b | |||
53470f8788 | |||
89b86936f6 | |||
d3a07edfcb | |||
98a3d6564e | |||
50a20c68ed | |||
3aad681538 | |||
92fb3b7529 | |||
1572f1137a | |||
b5075dd1eb | |||
9119caa843 | |||
f5c5a79064 | |||
357d804124 | |||
d59cb3b470 | |||
8ac292bd97 | |||
c6cab82546 | |||
04bf3692e4 | |||
6602aa0ee4 | |||
c8e099dedb | |||
9ede0800f1 | |||
fbdae316c7 | |||
da0baebb31 | |||
47906499a8 | |||
9ceef8f09e | |||
d5b5c79d14 | |||
6b3ca3230c | |||
49376b1572 | |||
c94c037f65 | |||
2ee45cd7c9 | |||
72079ca028 | |||
94d0bd29cd | |||
8cea4239aa | |||
6dca6a93d8 | |||
92d577e3e2 | |||
59f106bf9e | |||
913a6c3ec3 | |||
57932386bf | |||
e3df4b83eb | |||
ef5b01956a | |||
0e8984e5b1 | |||
403aedf1fe | |||
53d3646523 | |||
305ce9e44d | |||
9f8218efb7 | |||
c4ba470dc4 | |||
637bc75fc2 | |||
4ad3affadb | |||
bd403beb5c | |||
20f528a167 | |||
4ca2bc59b6 | |||
91c6839447 | |||
c388c77f4a | |||
19fb365271 | |||
92946ef6bb | |||
7fad7a67a0 | |||
392377bf2d | |||
95a53d37f6 | |||
2d03fbce79 | |||
37a0692f9f | |||
2c82a2332f | |||
2f78f0f5e2 | |||
c52b8cc98e | |||
fb5f358686 | |||
60d5a8e881 | |||
6cbebf3b13 | |||
f1269bbfe1 | |||
d1be21ace3 | |||
cf2317884c | |||
7a37a6127d | |||
1d4e5792fd | |||
1df329448b | |||
d6762547a3 | |||
a31a6d17d4 | |||
8069ede90a | |||
ce956172df | |||
874cec81e0 | |||
7b24b13dca | |||
e4dd3ed898 | |||
e5804ce778 | |||
b6db47c243 | |||
cde1881ab1 | |||
23afef7857 | |||
bdf09c7a45 | |||
6957a03a87 | |||
11e688d638 | |||
eb39de6b34 | |||
a0f937615e | |||
2ee7d2e698 | |||
7dc39a323a | |||
672717372e | |||
2ad2efa15e | |||
f638fcdf51 | |||
a47695d7e0 | |||
1bc1fea5cc | |||
97544a952f | |||
1c2a55f2fd | |||
8aa0bd189e | |||
d648caa37f | |||
34263661d7 | |||
f4a6fd5c5e | |||
fc9ddfb8d8 | |||
f8d0aff386 | |||
ac100d74bf | |||
9b1e4e9111 | |||
3e54b64bc4 | |||
914d2a787d | |||
95add5b1d0 | |||
b1e24212ea | |||
a1bec78ea2 | |||
05c98eb074 | |||
2688aefdfb | |||
eaa6582e67 | |||
a136bc619d | |||
8780c516fa | |||
b4bdec7970 | |||
671aeadf29 | |||
341d985610 | |||
76b9a78182 | |||
021c0a9429 | |||
7b3462d158 | |||
c180dee414 | |||
769da5c5ca | |||
1a4dd79240 | |||
5cf290b033 | |||
aec3da25b3 | |||
66d7cb563d | |||
551e9c041e | |||
fffb6d5b5e | |||
ac0bfeb360 | |||
7c30059ca3 | |||
fdb9ae6c40 | |||
3c82ffc0ab | |||
5dd3103aba | |||
84fc81f531 | |||
a20cbc62a5 | |||
e6a93e2838 | |||
3cff54561f | |||
e50a6a7876 | |||
b887ec839b | |||
465daa19a0 | |||
6c2b761d95 | |||
0e8f95ce19 | |||
6a17f343c6 | |||
1a45fb0039 | |||
75032898d6 | |||
88a4c97428 | |||
82e7a7edae | |||
eac28f97b8 | |||
e160882db9 | |||
2bc07e77fd | |||
c9b2db625c | |||
e3b41c9bd1 | |||
4aaee35d9c | |||
beedbc695a | |||
7123edc986 | |||
3008a754ce | |||
70e3fb8de6 | |||
9cf75da732 | |||
2cd266caff | |||
28036f1da5 | |||
0dacf2fe30 | |||
32f5ef5e5c | |||
98d91bbde7 | |||
7a92a75d83 | |||
f5556a02fc | |||
1050f4d928 | |||
9276b08f4b | |||
a501af669c | |||
85343fcefe | |||
b11dfde6e6 | |||
38d2108f02 |
375
.all-contributorsrc
Normal file
@ -0,0 +1,375 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "lordvlad",
|
||||
"name": "Waldemar Reusch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1217769?v=4",
|
||||
"profile": "https://github.com/lordvlad",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "willwill96",
|
||||
"name": "William Will",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10997562?v=4",
|
||||
"profile": "https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ann2827",
|
||||
"name": "Bystrova Ann",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32645809?v=4",
|
||||
"profile": "https://github.com/Ann2827",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mkreuzmayr",
|
||||
"name": "Michael Kreuzmayr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20108212?v=4",
|
||||
"profile": "https://github.com/mkreuzmayr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Mstrodl",
|
||||
"name": "Mary ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6877780?v=4",
|
||||
"profile": "https://coolmathgames.tech",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tasyp",
|
||||
"name": "German Öö",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6623212?v=4",
|
||||
"profile": "https://tasyp.xyz/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "revolunet",
|
||||
"name": "Julien Bouquillon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/124937?v=4",
|
||||
"profile": "https://revolunet.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aidangilmore",
|
||||
"name": "Aidan Gilmore",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32880357?v=4",
|
||||
"profile": "https://github.com/aidangilmore",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "0x-Void",
|
||||
"name": "Void",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32745739?v=4",
|
||||
"profile": "https://github.com/0x-Void",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "juffe",
|
||||
"name": "juffe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5393231?v=4",
|
||||
"profile": "https://github.com/juffe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lazToum",
|
||||
"name": "Lazaros Toumanidis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4764837?v=4",
|
||||
"profile": "https://github.com/lazToum",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marcmrf",
|
||||
"name": "Marc",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9928519?v=4",
|
||||
"profile": "https://github.com/marcmrf",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kasir-barati",
|
||||
"name": "Kasir Barati",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73785723?v=4",
|
||||
"profile": "http://kasir-barati.github.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "asashay",
|
||||
"name": "Alex Oliynyk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10714670?v=4",
|
||||
"profile": "https://github.com/asashay",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thosil",
|
||||
"name": "Thomas Silvestre",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1140574?v=4",
|
||||
"profile": "https://www.gravitysoftware.be",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "satanshiro",
|
||||
"name": "satanshiro",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/38865738?v=4",
|
||||
"profile": "https://github.com/satanshiro",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kpoelhekke",
|
||||
"name": "Koen Poelhekke",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1632377?v=4",
|
||||
"profile": "https://poelhekke.dev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zavoloklom",
|
||||
"name": "Sergey Kupletsky",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4151869?v=4",
|
||||
"profile": "https://github.com/zavoloklom",
|
||||
"contributions": [
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rome-user",
|
||||
"name": "rome-user",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/114131048?v=4",
|
||||
"profile": "https://github.com/rome-user",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "celinepelletier",
|
||||
"name": "Céline Pelletier",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/82821620?v=4",
|
||||
"profile": "https://github.com/celinepelletier",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "xgp",
|
||||
"name": "Garth",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/244253?v=4",
|
||||
"profile": "https://github.com/xgp",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "BlackVoid",
|
||||
"name": "Felix Gustavsson",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/673720?v=4",
|
||||
"profile": "https://github.com/BlackVoid",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "msiemens",
|
||||
"name": "Markus Siemens",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1873922?v=4",
|
||||
"profile": "https://m-siemens.de/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "law108000",
|
||||
"name": "Rlok",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8112024?v=4",
|
||||
"profile": "https://github.com/law108000",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Moulyy",
|
||||
"name": "Moulyy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/115405804?v=4",
|
||||
"profile": "https://github.com/Moulyy",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "madmadson",
|
||||
"name": "Tobias Matt",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/798831?v=4",
|
||||
"profile": "https://github.com/madmadson",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "oliviergoulet5",
|
||||
"name": "Olivier Goulet",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/17685861?v=4",
|
||||
"profile": "https://github.com/oliviergoulet5",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "liamlows",
|
||||
"name": "Liam Lowsley-Williams",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1365914?v=4",
|
||||
"profile": "https://github.com/liamlows",
|
||||
"contributions": [
|
||||
"code",
|
||||
"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,
|
||||
"skipCi": true,
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "keycloakify",
|
||||
"projectOwner": "keycloakify",
|
||||
"commitType": "docs"
|
||||
}
|
4
.gitattributes
vendored
@ -1,3 +1,3 @@
|
||||
src/lib/i18n/generated_messages/* linguist-documentation
|
||||
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
||||
src/bin/keycloakify/index.ts -linguist-detectable
|
||||
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
||||
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
3
.github/FUNDING.yaml
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [garronej]
|
138
.github/workflows/ci.yaml
vendored
@ -9,110 +9,115 @@ on:
|
||||
|
||||
jobs:
|
||||
|
||||
test_lint:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- name: If this step fails run 'npm run format' then commit again.
|
||||
run: npm run _format --list-different
|
||||
test:
|
||||
runs-on: macos-10.15
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: test_lint
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ '15', '14', '13' ]
|
||||
name: Test with Node v${{ matrix.node }}
|
||||
node: [ '18' ]
|
||||
os: [ ubuntu-latest ]
|
||||
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Tell if project is using npm or yarn
|
||||
id: step1
|
||||
uses: garronej/github_actions_toolkit@v2.2
|
||||
with:
|
||||
action_name: tell_if_project_uses_npm_or_yarn
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/setup-node@v2.1.3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
||||
run: |
|
||||
yarn build
|
||||
yarn test
|
||||
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
||||
run: |
|
||||
npm run build
|
||||
npm test
|
||||
- run: npm run build
|
||||
- run: npm run test
|
||||
|
||||
storybook:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: npm run build-storybook
|
||||
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./storybook-static -u "github-actions-bot <actions@github.com>"
|
||||
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
if: github.event_name == 'push'
|
||||
# When someone forks the repo and opens a PR we want to enables the tests to be run (the previous jobs)
|
||||
# but obviously only us should be allowed to release.
|
||||
# In the following check we make sure that we own the branch this CI workflow is running on before continuing.
|
||||
# Without this check, trying to release would fail anyway because only us have the correct secret.NPM_TOKEN but
|
||||
# it's cleaner to stop the execution instead of letting the CI crash.
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
outputs:
|
||||
from_version: ${{ steps.step1.outputs.from_version }}
|
||||
to_version: ${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: ${{steps.step1.outputs.is_upgraded_version }}
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
|
||||
steps:
|
||||
- uses: garronej/github_actions_toolkit@v2.2
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
|
||||
update_changelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_if_version_upgraded
|
||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
||||
steps:
|
||||
- uses: garronej/github_actions_toolkit@v2.4
|
||||
with:
|
||||
action_name: update_changelog
|
||||
branch: ${{ github.ref }}
|
||||
|
||||
create_github_release:
|
||||
runs-on: ubuntu-latest
|
||||
# We create release only if the version in the package.json have been upgraded and this CI is running against the main branch.
|
||||
# We allow branches with a PR open on main to publish pre-release (x.y.z-rc.u) but not actual releases.
|
||||
if: |
|
||||
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
|
||||
(
|
||||
github.event_name == 'push' ||
|
||||
needs.check_if_version_upgraded.outputs.is_pre_release == 'true'
|
||||
)
|
||||
needs:
|
||||
- update_changelog
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Build GitHub release body
|
||||
id: step1
|
||||
run: |
|
||||
if [ "$FROM_VERSION" = "0.0.0" ]; then
|
||||
echo "::set-output name=body::🚀"
|
||||
else
|
||||
echo "::set-output name=body::📋 [CHANGELOG](https://github.com/$GITHUB_REPOSITORY/blob/v$TO_VERSION/CHANGELOG.md)"
|
||||
fi
|
||||
env:
|
||||
FROM_VERSION: ${{ needs.check_if_version_upgraded.outputs.from_version }}
|
||||
TO_VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
- uses: garronej/action-gh-release@v0.2.0
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
target_commitish: ${{ github.ref }}
|
||||
body: ${{ steps.step1.outputs.body }}
|
||||
target_commitish: ${{ github.head_ref || github.ref }}
|
||||
generate_release_notes: true
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
publish_on_npm:
|
||||
runs-on: macos-10.15
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- update_changelog
|
||||
- create_github_release
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: actions/checkout@v2.3.4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- uses: actions/setup-node@v2.1.3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '15'
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: |
|
||||
PACKAGE_MANAGER=npm
|
||||
if [ -f "./yarn.lock" ]; then
|
||||
PACKAGE_MANAGER=yarn
|
||||
fi
|
||||
$PACKAGE_MANAGER run build
|
||||
- run: npx -y -p denoify@0.6.5 denoify_enable_short_npm_import_path
|
||||
- run: npm run build
|
||||
- run: npx -y -p denoify@1.6.13 enable_short_npm_import_path
|
||||
env:
|
||||
DRY_RUN: "0"
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
with:
|
||||
action_name: remove_dark_mode_specific_images_from_readme
|
||||
- name: Publishing on NPM
|
||||
run: |
|
||||
if [ "$(npm show . version)" = "$VERSION" ]; then
|
||||
@ -123,7 +128,12 @@ jobs:
|
||||
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
|
||||
false
|
||||
fi
|
||||
npm publish
|
||||
EXTRA_ARGS=""
|
||||
if [ "$IS_PRE_RELEASE" = "true" ]; then
|
||||
EXTRA_ARGS="--tag next"
|
||||
fi
|
||||
npm publish $EXTRA_ARGS
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}
|
||||
|
18
.gitignore
vendored
@ -41,5 +41,21 @@ jspm_packages
|
||||
.DS_Store
|
||||
|
||||
/dist
|
||||
|
||||
/keycloakify_starter_test/
|
||||
/sample_custom_react_project/
|
||||
/sample_react_project/
|
||||
/.yarn_home/
|
||||
|
||||
.idea
|
||||
|
||||
/src/login/i18n/messages_defaultSet/
|
||||
/src/account/i18n/
|
||||
|
||||
# VS Code devcontainers
|
||||
.devcontainer
|
||||
/.yarn
|
||||
/.yarnrc.yml
|
||||
|
||||
/stories/assets/fonts/
|
||||
/build_storybook/
|
||||
/storybook-static/
|
||||
|
16
.prettierignore
Normal file
@ -0,0 +1,16 @@
|
||||
node_modules/
|
||||
/dist/
|
||||
/CHANGELOG.md
|
||||
/.yarn_home/
|
||||
/src/test/apps/
|
||||
/src/tools/types/
|
||||
/build_keycloak/
|
||||
/.vscode/
|
||||
/src/login/i18n/messages_defaultSet/
|
||||
/src/account/i18n/messages_defaultSet/
|
||||
/dist_test
|
||||
/sample_react_project/
|
||||
/sample_custom_react_project/
|
||||
/keycloakify_starter_test/
|
||||
/.storybook/static/keycloak-resources/
|
||||
/src/bin/start-keycloak/*.json
|
24
.prettierrc.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"printWidth": 90,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "useUserProfileForm.tsx",
|
||||
"options": {
|
||||
"printWidth": 150
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
33
.storybook/customTheme.ts
Normal file
@ -0,0 +1,33 @@
|
||||
const brandImage = "logo.png";
|
||||
const brandTitle = "Keycloakify";
|
||||
const brandUrl = "https://github.com/keycloakify/keycloakify";
|
||||
const fontBase = '"Work Sans", sans-serif';
|
||||
const fontCode = "monospace";
|
||||
|
||||
export const darkTheme = {
|
||||
base: "dark",
|
||||
appBg: "#1E1E1E",
|
||||
appContentBg: "#161616",
|
||||
barBg: "#161616",
|
||||
colorSecondary: "#8585F6",
|
||||
textColor: "#FFFFFF",
|
||||
brandImage,
|
||||
brandTitle,
|
||||
brandUrl,
|
||||
fontBase,
|
||||
fontCode
|
||||
};
|
||||
|
||||
export const lightTheme: typeof darkTheme = {
|
||||
base: "light",
|
||||
appBg: "#F6F6F6",
|
||||
appContentBg: "#FFFFFF",
|
||||
barBg: "#FFFFFF",
|
||||
colorSecondary: "#000091",
|
||||
textColor: "#212121",
|
||||
brandImage,
|
||||
brandTitle,
|
||||
brandUrl,
|
||||
fontBase,
|
||||
fontCode
|
||||
};
|
13
.storybook/main.js
Normal file
@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
stories: [
|
||||
"../stories/**/*.stories.tsx"
|
||||
],
|
||||
addons: [
|
||||
"storybook-dark-mode",
|
||||
"@storybook/addon-a11y"
|
||||
],
|
||||
core: {
|
||||
builder: "webpack5"
|
||||
},
|
||||
staticDirs: ["./static", "../dist/res/public"]
|
||||
};
|
32
.storybook/manager-head.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!-- start favicon -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon_package/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon_package/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon_package/favicon-16x16.png">
|
||||
<link rel="manifest" href="/favicon_package/site.webmanifest">
|
||||
<link rel="mask-icon" href="/favicon_package/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<!-- end favicon -->
|
||||
|
||||
<!-- Meta tags generated by metatags.io -->
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Keycloakify Storybook</title>
|
||||
<meta name="title" content="Keycloakify Storybook">
|
||||
<meta name="description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://www.keycloakify.dev">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Keycloakify Storybook">
|
||||
<meta property="og:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
<meta property="og:image" content="https://storybook.keycloakify.dev/preview.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Keycloakify Storybook">
|
||||
<meta name="twitter:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
<meta name="twitter:image" content="https://storybook.keycloakify.dev/preview.png">
|
||||
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">
|
6
.storybook/manager.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
|
||||
addons.setConfig({
|
||||
selectedPanel: 'storybook/a11y/panel',
|
||||
showPanel: false
|
||||
});
|
23
.storybook/preview-head.html
Normal file
@ -0,0 +1,23 @@
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">
|
||||
|
||||
<style>
|
||||
body.sb-show-main.sb-main-padded {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body:not(.kcBodyClass) {
|
||||
background-color: #393939;
|
||||
}
|
||||
|
||||
body.sb-show-preparing-docs > .sb-wrapper {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
body .sb-preparing-story {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
161
.storybook/preview.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { darkTheme, lightTheme } from "./customTheme";
|
||||
import { create as createTheme } from "@storybook/theming";
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
backgrounds: { disable: true },
|
||||
darkMode: {
|
||||
light: createTheme(lightTheme),
|
||||
dark: createTheme(darkTheme),
|
||||
},
|
||||
controls: {
|
||||
disable: true,
|
||||
},
|
||||
actions: {
|
||||
disable: true
|
||||
},
|
||||
viewport: {
|
||||
viewports: {
|
||||
"1440p": {
|
||||
name: "1440p",
|
||||
styles: {
|
||||
width: "2560px",
|
||||
height: "1440px",
|
||||
},
|
||||
},
|
||||
fullHD: {
|
||||
name: "Full HD",
|
||||
styles: {
|
||||
width: "1920px",
|
||||
height: "1080px",
|
||||
},
|
||||
},
|
||||
macBookProBig: {
|
||||
name: "MacBook Pro Big",
|
||||
styles: {
|
||||
width: "1024px",
|
||||
height: "640px",
|
||||
},
|
||||
},
|
||||
macBookProMedium: {
|
||||
name: "MacBook Pro Medium",
|
||||
styles: {
|
||||
width: "1440px",
|
||||
height: "900px",
|
||||
},
|
||||
},
|
||||
macBookProSmall: {
|
||||
name: "MacBook Pro Small",
|
||||
styles: {
|
||||
width: "1680px",
|
||||
height: "1050px",
|
||||
},
|
||||
},
|
||||
pcAgent: {
|
||||
name: "PC Agent",
|
||||
styles: {
|
||||
width: "960px",
|
||||
height: "540px",
|
||||
},
|
||||
},
|
||||
iphone12Pro: {
|
||||
name: "Iphone 12 pro",
|
||||
styles: {
|
||||
width: "390px",
|
||||
height: "844px",
|
||||
},
|
||||
},
|
||||
iphone5se: {
|
||||
name: "Iphone 5/SE",
|
||||
styles: {
|
||||
width: "320px",
|
||||
height: "568px",
|
||||
},
|
||||
},
|
||||
ipadPro: {
|
||||
name: "Ipad pro",
|
||||
styles: {
|
||||
width: "1240px",
|
||||
height: "1366px",
|
||||
},
|
||||
},
|
||||
"Galaxy s9+": {
|
||||
name: "Galaxy S9+",
|
||||
styles: {
|
||||
width: "320px",
|
||||
height: "658px",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
options: {
|
||||
storySort: (a, b) =>
|
||||
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
|
||||
},
|
||||
};
|
||||
|
||||
const { getHardCodedWeight } = (() => {
|
||||
|
||||
const orderedPagesPrefix = [
|
||||
"Introduction",
|
||||
"login/login.ftl",
|
||||
"login/register.ftl",
|
||||
"login/terms.ftl",
|
||||
"login/error.ftl",
|
||||
"login/code.ftl",
|
||||
"login/delete-account-confirm.ftl",
|
||||
"login/delete-credential.ftl",
|
||||
"login/frontchannel-logout.ftl",
|
||||
"login/idp-review-user-profile.ftl",
|
||||
"login/info.ftl",
|
||||
"login/login-config-totp.ftl",
|
||||
"login/login-idp-link-confirm.ftl",
|
||||
"login/login-idp-link-email.ftl",
|
||||
"login/login-oauth-grant.ftl",
|
||||
"login/login-otp.ftl",
|
||||
"login/login-page-expired.ftl",
|
||||
"login/login-password.ftl",
|
||||
"login/login-reset-otp.ftl",
|
||||
"login/login-reset-password.ftl",
|
||||
"login/login-update-password.ftl",
|
||||
"login/login-update-profile.ftl",
|
||||
"login/login-username.ftl",
|
||||
"login/login-verify-email.ftl",
|
||||
"login/login-x509-info.ftl",
|
||||
"login/logout-confirm.ftl",
|
||||
"login/saml-post-form.ftl",
|
||||
"login/select-authenticator.ftl",
|
||||
"login/update-email.ftl",
|
||||
"login/webauthn-authenticate.ftl",
|
||||
"login/webauthn-error.ftl",
|
||||
"login/webauthn-register.ftl",
|
||||
"login/login-oauth2-device-verify-user-code.ftl",
|
||||
"login/login-recovery-authn-code-config.ftl",
|
||||
"login/login-recovery-authn-code-input.ftl",
|
||||
"account/account.ftl",
|
||||
"account/password.ftl",
|
||||
"account/federatedIdentity.ftl",
|
||||
"account/log.ftl",
|
||||
"account/sessions.ftl",
|
||||
"account/totp.ftl",
|
||||
];
|
||||
|
||||
function getHardCodedWeight(kind) {
|
||||
|
||||
for (let i = 0; i < orderedPagesPrefix.length; i++) {
|
||||
if (kind.toLowerCase().startsWith(orderedPagesPrefix[i].toLowerCase())) {
|
||||
return orderedPagesPrefix.length - i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return { getHardCodedWeight };
|
||||
})();
|
1
.storybook/static/CNAME
Normal file
@ -0,0 +1 @@
|
||||
storybook.keycloakify.dev
|
BIN
.storybook/static/favicon_package/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
.storybook/static/favicon_package/android-chrome-384x384.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
.storybook/static/favicon_package/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 30 KiB |
9
.storybook/static/favicon_package/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
.storybook/static/favicon_package/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
.storybook/static/favicon_package/favicon-32x32.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
.storybook/static/favicon_package/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
.storybook/static/favicon_package/mstile-150x150.png
Normal file
After Width: | Height: | Size: 19 KiB |
193
.storybook/static/favicon_package/safari-pinned-tab.svg
Normal file
@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="447.000000pt" height="447.000000pt" viewBox="0 0 447.000000 447.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,447.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2177 4413 c-3 -2 -17 -6 -33 -9 -85 -15 -204 -109 -286 -225 -95
|
||||
-133 -229 -437 -263 -597 -4 -18 -10 -30 -13 -28 -4 2 -7 -11 -8 -30 0 -19 -3
|
||||
-36 -6 -39 -3 -4 -23 1 -44 10 -51 22 -213 73 -289 92 -301 73 -516 74 -670 3
|
||||
-124 -57 -186 -153 -188 -295 -1 -67 5 -128 18 -180 3 -13 15 -45 26 -70 43
|
||||
-99 57 -135 53 -135 -3 0 4 -10 16 -22 11 -12 20 -26 20 -31 0 -5 9 -22 21
|
||||
-38 11 -16 18 -29 14 -29 -3 0 4 -10 15 -22 11 -12 18 -28 15 -36 -2 -7 -1
|
||||
-11 3 -8 4 2 19 -12 32 -32 13 -20 33 -47 44 -59 11 -13 17 -23 13 -23 -3 0
|
||||
12 -19 33 -42 22 -24 38 -49 35 -55 -2 -7 -1 -12 4 -10 4 1 32 -25 62 -58 30
|
||||
-33 88 -94 131 -135 l76 -75 -71 -70 c-112 -110 -174 -181 -262 -300 -106
|
||||
-144 -142 -202 -203 -325 -9 -19 -21 -38 -27 -42 -5 -4 -7 -8 -3 -8 4 0 -4
|
||||
-27 -18 -60 -25 -63 -58 -199 -50 -208 3 -2 1 -12 -4 -22 -5 -10 -6 -20 -3
|
||||
-24 4 -4 8 -23 10 -44 9 -107 77 -201 183 -251 33 -16 56 -32 52 -36 -4 -5 -2
|
||||
-5 4 -2 6 4 44 1 85 -6 41 -7 102 -12 136 -12 41 1 60 -2 55 -9 -3 -6 2 -5 11
|
||||
3 11 8 44 14 86 15 38 0 67 4 64 9 -4 6 10 8 79 11 14 0 24 3 22 5 -4 4 33 14
|
||||
145 37 13 3 33 10 43 15 11 6 26 8 34 5 10 -4 12 -2 8 6 -5 8 -2 9 9 5 10 -3
|
||||
17 -2 17 3 0 6 8 10 18 10 9 0 36 9 58 19 32 15 45 16 54 8 9 -9 11 -9 8 2 -2
|
||||
7 2 15 8 18 10 3 23 -34 38 -108 2 -8 6 -21 9 -29 15 -36 70 -206 70 -217 0
|
||||
-7 4 -13 8 -13 11 0 30 -61 22 -74 -3 -6 -3 -8 2 -4 8 7 86 -135 88 -159 0 -7
|
||||
4 -13 8 -13 13 0 39 -56 31 -66 -4 -5 -3 -6 2 -2 5 4 22 -12 39 -35 17 -23 49
|
||||
-59 71 -80 23 -21 40 -42 39 -47 -2 -6 0 -9 5 -8 15 3 63 -22 68 -37 3 -8 10
|
||||
-12 15 -10 5 3 22 -1 39 -10 17 -9 35 -13 40 -10 6 3 10 2 10 -2 0 -5 12 -8
|
||||
28 -8 15 1 33 -4 41 -9 11 -8 13 -8 8 0 -4 7 7 13 33 17 21 3 53 12 71 21 17
|
||||
9 36 13 42 9 5 -3 7 -1 3 5 -3 6 3 13 14 17 11 3 20 11 20 16 0 5 5 9 11 9 5
|
||||
0 7 -6 3 -13 -4 -7 -3 -9 2 -4 5 5 9 12 9 17 0 4 14 21 30 37 69 67 162 196
|
||||
179 251 4 12 12 20 17 16 5 -3 8 -2 7 3 -1 4 6 29 17 56 14 33 24 45 35 41 11
|
||||
-4 12 -2 4 6 -14 14 -6 52 9 43 5 -3 7 -2 4 4 -3 5 8 45 24 88 17 44 33 86 35
|
||||
94 7 34 56 206 59 209 1 1 15 -3 31 -9 16 -7 34 -13 39 -15 61 -18 144 -51
|
||||
139 -56 -3 -4 3 -4 14 -1 11 2 23 0 26 -6 4 -5 14 -7 23 -4 9 4 14 2 10 -3 -3
|
||||
-5 7 -9 21 -8 34 1 63 -6 58 -15 -2 -3 19 -7 47 -9 28 -2 55 -6 61 -10 13 -8
|
||||
18 -9 79 -10 27 -1 46 -5 44 -9 -3 -5 20 -6 50 -5 44 3 54 1 48 -10 -6 -10 -5
|
||||
-11 6 0 16 15 33 16 23 0 -5 -7 -3 -8 7 -3 7 5 29 10 49 12 74 5 116 11 135
|
||||
18 11 4 37 13 57 21 28 10 38 11 41 1 4 -8 6 -7 6 3 1 8 21 28 46 44 43 26 92
|
||||
86 109 131 28 73 27 217 -3 313 -5 18 -10 33 -9 35 2 23 -118 257 -184 356
|
||||
-44 67 -124 177 -138 191 -3 3 -34 39 -70 80 -36 41 -97 107 -137 146 l-72 71
|
||||
25 22 c14 12 30 19 36 15 6 -4 8 -3 4 4 -3 6 24 42 61 80 38 38 86 91 108 117
|
||||
22 27 46 54 53 61 6 7 12 17 12 23 0 6 3 11 8 11 4 0 22 22 41 50 19 27 37 47
|
||||
41 45 4 -3 7 3 6 13 0 9 5 16 12 14 8 -1 11 2 7 7 -5 9 36 83 55 101 5 5 101
|
||||
200 121 245 7 18 26 84 36 125 2 8 4 27 5 42 0 16 5 25 11 21 6 -4 7 -1 2 7
|
||||
-11 18 -11 62 0 80 5 8 4 11 -2 7 -6 -4 -12 10 -16 36 -6 51 -10 70 -18 77 -3
|
||||
3 -15 21 -27 41 -42 70 -184 145 -292 155 -205 20 -451 -18 -709 -108 -30 -10
|
||||
-60 -17 -68 -14 -8 3 -12 2 -9 -3 5 -7 -42 -31 -60 -31 -1 0 -5 15 -9 33 -18
|
||||
86 -108 342 -156 444 -17 35 -29 66 -29 69 1 4 -2 10 -7 13 -5 3 -24 32 -42
|
||||
64 -108 190 -245 296 -403 311 -20 2 -39 2 -41 -1z m53 -124 c0 -5 5 -7 10 -4
|
||||
14 9 52 -4 45 -16 -3 -5 0 -6 8 -4 17 7 69 -33 61 -47 -5 -7 -2 -8 5 -4 13 8
|
||||
43 -23 35 -36 -3 -4 1 -6 8 -3 7 2 24 -11 38 -31 14 -19 30 -41 35 -47 50 -56
|
||||
148 -233 139 -249 -4 -6 -3 -9 2 -5 12 7 97 -192 90 -210 -3 -8 -2 -12 3 -9
|
||||
11 7 45 -120 35 -135 -4 -8 -3 -9 4 -5 7 4 12 3 12 -3 0 -5 3 -16 6 -25 4 -11
|
||||
-23 -30 -113 -75 -138 -71 -276 -145 -350 -189 -29 -17 -53 -29 -53 -26 0 3
|
||||
-7 -1 -15 -10 -14 -14 -19 -13 -53 6 -20 11 -62 34 -92 51 -30 16 -59 33 -65
|
||||
37 -5 4 -86 47 -180 95 -93 48 -173 91 -177 94 -11 10 3 52 15 44 6 -3 7 -1 3
|
||||
6 -9 14 12 113 22 107 4 -2 8 7 8 20 2 24 42 134 53 144 3 3 7 12 9 20 2 8 17
|
||||
44 33 80 24 50 32 61 40 50 8 -11 9 -10 4 7 -5 17 10 47 60 123 36 55 71 98
|
||||
76 94 5 -3 9 -2 8 3 -4 23 2 35 17 29 8 -3 12 -2 9 3 -7 12 60 78 100 99 25
|
||||
13 70 27 98 31 4 1 7 -4 7 -10z m-1411 -783 c9 -6 12 -5 8 1 -4 6 10 10 34 11
|
||||
29 1 38 -1 33 -11 -5 -9 -4 -9 7 1 7 6 19 12 27 12 8 0 10 -5 6 -12 -6 -10 -5
|
||||
-10 7 -1 9 7 26 10 40 8 13 -3 47 -8 74 -11 63 -7 155 -23 175 -31 15 -6 35
|
||||
-10 71 -12 12 -1 17 -5 13 -13 -4 -7 -3 -8 4 -4 6 4 31 1 54 -5 24 -7 49 -13
|
||||
55 -15 25 -5 73 -29 73 -37 0 -5 4 -6 9 -3 12 8 41 -4 41 -16 0 -5 -4 -6 -10
|
||||
-3 -6 3 -7 -1 -4 -9 3 -9 1 -45 -5 -81 -6 -36 -14 -91 -17 -122 -4 -32 -11
|
||||
-61 -18 -65 -8 -6 -7 -8 2 -8 7 0 11 -4 8 -8 -3 -4 -8 -42 -11 -83 -4 -40 -9
|
||||
-81 -12 -89 -3 -8 -6 -44 -8 -80 -1 -36 -3 -65 -4 -65 -1 0 -3 -27 -4 -61 -3
|
||||
-66 0 -62 -92 -139 -33 -27 -62 -53 -63 -58 -2 -4 -8 -5 -13 -1 -5 3 -9 1 -9
|
||||
-3 0 -5 -41 -44 -91 -88 -50 -43 -98 -85 -106 -93 -13 -14 -23 -6 -91 64 -86
|
||||
90 -172 188 -186 213 -5 9 -12 18 -16 21 -12 9 -106 154 -139 215 -18 33 -37
|
||||
66 -43 72 -6 7 -8 20 -4 28 3 9 2 14 -3 11 -19 -12 -102 225 -105 296 0 27 -4
|
||||
48 -7 48 -16 0 9 108 32 142 22 31 125 81 151 74 10 -2 18 0 18 5 0 5 14 7 30
|
||||
5 17 -2 30 0 30 4 0 9 42 7 59 -4z m2823 1 c6 -9 8 -9 8 1 0 7 4 10 10 7 5 -3
|
||||
29 -8 52 -11 113 -13 201 -66 197 -118 0 -5 4 -12 10 -15 9 -6 11 -27 11 -124
|
||||
0 -16 -4 -26 -9 -23 -5 3 -7 -2 -4 -13 11 -41 -89 -307 -110 -294 -6 3 -7 1
|
||||
-3 -6 13 -20 -142 -281 -166 -281 -6 0 -8 -3 -5 -7 11 -10 -51 -84 -197 -238
|
||||
l-86 -89 -47 44 c-26 25 -52 50 -58 55 -7 6 -39 33 -71 61 -33 29 -86 73 -119
|
||||
100 -86 69 -87 71 -90 112 -5 69 -16 207 -21 242 -4 33 -10 92 -18 170 -4 37
|
||||
-14 114 -21 165 -2 17 -9 52 -14 80 -5 27 -9 50 -8 51 50 22 109 44 129 48 15
|
||||
2 36 10 48 16 12 6 28 9 35 6 8 -3 15 -1 17 5 2 5 19 11 39 13 20 2 40 6 45 9
|
||||
5 3 29 8 54 12 25 4 48 9 52 11 5 3 8 -1 8 -8 0 -9 2 -10 8 -2 9 16 43 21 61
|
||||
10 10 -7 12 -6 6 4 -6 10 -3 12 12 8 12 -3 25 -3 31 1 20 12 206 11 214 -2z
|
||||
m-1950 -189 c23 -13 46 -27 49 -31 3 -5 9 -8 13 -7 15 3 64 -25 59 -34 -3 -5
|
||||
-1 -6 5 -2 14 9 63 -14 55 -27 -3 -6 -2 -7 4 -4 5 3 56 -21 113 -53 57 -33
|
||||
107 -60 111 -60 4 0 11 -5 15 -12 5 -8 2 -9 -9 -5 -9 3 -16 2 -14 -2 1 -5 -48
|
||||
-42 -110 -83 -61 -40 -144 -95 -183 -123 -40 -27 -80 -54 -89 -60 -9 -5 -18
|
||||
-12 -21 -15 -17 -17 -80 -60 -80 -54 0 3 -9 -4 -20 -17 l-20 -24 6 105 c3 58
|
||||
7 121 9 140 2 19 5 52 6 72 1 20 5 36 8 34 3 -2 7 19 7 47 2 79 15 149 31 176
|
||||
8 13 10 20 4 17 -6 -4 -11 -2 -11 3 0 6 4 11 10 11 5 0 7 7 4 15 -8 20 -4 19
|
||||
48 -7z m1114 -66 c3 -26 8 -56 10 -67 10 -50 19 -145 19 -192 0 -28 3 -49 7
|
||||
-47 4 3 6 -16 5 -41 -1 -25 0 -45 3 -45 3 0 6 -32 7 -70 2 -39 -1 -70 -5 -70
|
||||
-5 0 -18 9 -30 20 -12 11 -26 20 -31 20 -5 0 -17 10 -25 22 -9 12 -16 19 -16
|
||||
15 0 -6 -92 56 -135 90 -25 20 -253 169 -278 182 -15 8 -26 15 -25 16 9 7 158
|
||||
95 184 108 17 9 34 13 38 10 3 -4 6 -1 6 5 0 11 175 102 197 102 7 0 13 4 13
|
||||
9 0 5 8 11 18 13 20 4 29 -14 38 -80z m-531 -268 c30 -20 55 -41 55 -45 0 -5
|
||||
6 -8 13 -6 6 1 11 -4 9 -11 -1 -8 2 -11 7 -7 14 8 82 -38 76 -52 -2 -7 2 -10
|
||||
11 -6 8 3 25 -3 37 -14 12 -11 35 -27 50 -37 16 -9 25 -22 21 -28 -4 -7 -3 -8
|
||||
4 -4 10 6 242 -148 250 -166 2 -5 9 -8 16 -8 7 0 21 -9 31 -20 17 -19 14 -24
|
||||
-10 -21 -5 1 -3 -4 5 -11 12 -10 15 -40 16 -138 0 -77 -4 -128 -10 -132 -7 -5
|
||||
-7 -8 -1 -8 12 0 14 -259 2 -277 -5 -7 -4 -13 1 -13 12 0 7 -79 -5 -90 -7 -7
|
||||
-244 -174 -287 -203 -10 -6 -24 -16 -30 -22 -12 -10 -34 -26 -211 -146 -96
|
||||
-65 -108 -71 -131 -60 -14 6 -22 16 -19 21 3 5 0 7 -7 5 -8 -3 -23 4 -35 15
|
||||
-12 11 -24 20 -27 20 -5 0 -246 166 -271 187 -5 4 -30 22 -55 38 -25 17 -49
|
||||
33 -55 38 -5 4 -44 31 -85 61 l-75 54 0 337 c1 317 2 338 19 351 11 7 23 11
|
||||
29 7 6 -3 7 -1 3 5 -4 7 3 17 18 24 14 6 26 15 26 20 0 4 7 8 15 8 8 0 15 5
|
||||
15 11 0 6 7 8 16 5 8 -3 13 -2 9 3 -3 5 12 20 32 32 21 13 41 27 44 32 4 5 12
|
||||
6 19 2 8 -5 11 -4 7 2 -9 15 38 44 60 37 12 -4 14 -3 6 3 -9 6 1 19 37 46 27
|
||||
20 54 37 59 37 5 0 11 3 13 8 5 12 131 95 144 95 7 0 12 4 11 8 -2 8 51 47 66
|
||||
48 4 1 32 -15 62 -35z m-822 -752 c2 -270 4 -262 -48 -215 -12 10 -41 34 -65
|
||||
53 -43 33 -83 67 -157 136 l-34 31 57 49 c177 153 219 185 228 176 4 -4 7 -2
|
||||
6 3 -4 17 0 29 6 23 3 -4 6 -119 7 -256z m1539 245 c7 -7 26 -20 41 -30 15
|
||||
-10 25 -23 21 -29 -4 -7 -3 -8 4 -4 7 4 12 2 12 -3 0 -6 6 -10 13 -8 6 1 11
|
||||
-4 9 -11 -1 -8 2 -11 7 -8 5 4 14 -2 20 -11 5 -10 13 -18 18 -18 4 0 7 -3 6
|
||||
-7 -2 -5 1 -7 5 -5 5 1 37 -22 71 -52 l62 -54 -53 -49 c-29 -27 -66 -59 -81
|
||||
-71 -16 -12 -34 -27 -41 -33 -43 -40 -129 -105 -133 -101 -2 3 0 14 6 25 7 13
|
||||
7 23 -2 32 -9 10 -9 14 1 17 6 3 9 9 6 14 -5 9 -8 289 -4 314 1 6 2 14 1 19
|
||||
-4 22 -8 86 -5 86 1 0 9 -6 16 -13z m-1862 -353 c48 -45 72 -65 109 -93 17
|
||||
-13 31 -30 31 -37 0 -8 3 -13 8 -13 21 4 32 -3 32 -18 0 -9 3 -14 6 -10 4 3
|
||||
13 -1 21 -9 8 -8 30 -26 49 -40 18 -15 31 -31 27 -37 -3 -5 -2 -7 4 -4 23 14
|
||||
43 -19 48 -78 10 -127 16 -186 40 -410 4 -33 10 -79 14 -102 5 -27 4 -46 -3
|
||||
-54 -8 -9 -8 -10 0 -6 7 4 14 -2 17 -13 3 -11 0 -20 -5 -20 -6 0 -5 -6 2 -15
|
||||
7 -8 10 -26 8 -40 -3 -13 -1 -22 3 -19 5 3 9 1 9 -5 0 -8 -35 -25 -50 -23 -3
|
||||
0 -14 -5 -25 -11 -11 -6 -22 -12 -25 -12 -3 -1 -36 -11 -75 -23 -61 -18 -120
|
||||
-33 -190 -48 -11 -2 -40 -7 -65 -10 -25 -3 -49 -8 -53 -11 -5 -3 -70 -6 -145
|
||||
-8 -122 -3 -168 0 -263 18 -72 14 -145 78 -155 136 -3 20 -8 44 -11 54 -2 9
|
||||
-1 17 4 17 4 0 8 17 7 37 -1 50 6 74 20 66 8 -4 8 -3 0 6 -12 14 33 164 47
|
||||
155 5 -3 6 2 3 10 -3 9 6 36 20 62 14 26 26 53 26 60 0 8 5 14 10 14 6 0 10 5
|
||||
10 11 0 22 93 168 103 162 6 -3 7 -2 4 4 -10 17 64 118 77 105 4 -3 5 0 2 8
|
||||
-4 9 12 35 39 65 25 28 73 80 106 117 34 38 65 65 70 62 5 -3 8 -2 7 3 -5 13
|
||||
26 42 37 35 6 -3 26 -21 45 -38z m2364 -103 c-1 -3 7 -12 18 -19 10 -7 15 -18
|
||||
12 -24 -4 -6 -2 -8 3 -5 6 4 21 -8 34 -25 13 -17 29 -36 34 -42 30 -33 63 -84
|
||||
58 -90 -4 -3 -1 -6 5 -6 17 0 85 -108 76 -121 -4 -7 -3 -9 3 -6 13 8 106 -169
|
||||
97 -185 -4 -6 -3 -8 3 -5 12 7 36 -50 28 -63 -3 -4 1 -10 9 -13 7 -3 17 -23
|
||||
21 -44 5 -21 10 -45 12 -53 2 -8 4 -24 6 -35 1 -11 6 -23 10 -26 5 -3 8 -36 8
|
||||
-73 0 -178 -108 -238 -417 -231 -78 2 -150 5 -161 8 -10 3 -34 8 -53 11 -19 3
|
||||
-48 8 -65 11 -114 22 -337 94 -329 106 3 5 -2 6 -12 2 -12 -5 -15 -2 -11 13 6
|
||||
20 16 78 22 129 2 17 6 46 9 65 3 19 8 60 11 90 3 30 8 69 11 87 2 17 7 62 10
|
||||
100 3 37 10 73 15 80 7 7 6 14 -1 18 -6 3 -7 12 -3 18 4 6 6 26 5 45 -2 25 1
|
||||
32 10 26 10 -6 10 -5 2 7 -16 21 -6 71 18 88 11 8 30 25 41 38 11 12 24 23 29
|
||||
23 4 0 25 16 45 36 63 60 110 94 118 86 5 -4 5 -2 2 4 -4 7 9 24 29 39 20 15
|
||||
49 40 66 57 l30 29 71 -72 c40 -39 71 -74 71 -78z m-1901 -294 c-3 -5 -2 -7 4
|
||||
-4 12 8 83 -39 83 -55 0 -6 3 -8 6 -5 7 7 136 -80 142 -95 2 -4 10 -8 18 -8 8
|
||||
0 14 -3 14 -8 0 -4 20 -18 45 -32 25 -14 45 -28 45 -32 0 -5 5 -8 10 -8 12 0
|
||||
124 -72 128 -82 2 -5 11 -8 20 -8 10 0 -36 -32 -101 -70 -65 -39 -124 -67
|
||||
-130 -64 -7 4 -8 3 -4 -2 5 -5 -41 -33 -110 -66 -80 -38 -120 -52 -125 -45 -3
|
||||
7 -9 32 -12 57 -3 25 -8 58 -11 74 -3 15 -8 49 -11 75 -3 25 -7 62 -9 81 -2
|
||||
19 -7 62 -10 94 -3 33 -8 64 -11 68 -3 4 0 8 6 8 6 0 8 5 4 11 -3 6 -8 43 -11
|
||||
82 -5 65 -4 70 11 58 9 -7 13 -18 9 -24z m1261 -64 c-4 -94 -9 -162 -18 -228
|
||||
-2 -16 -7 -55 -10 -85 -4 -30 -8 -62 -10 -70 -1 -8 -7 -37 -11 -65 -5 -27 -10
|
||||
-53 -11 -57 -1 -5 -2 -13 -3 -20 -1 -9 -4 -9 -13 0 -7 7 -20 12 -30 12 -10 0
|
||||
-18 5 -18 12 0 6 -3 9 -6 5 -4 -3 -54 19 -113 50 -58 31 -123 64 -143 75 -21
|
||||
10 -38 23 -38 29 0 6 -4 8 -9 5 -5 -3 -19 2 -32 12 -13 10 -37 24 -52 32 -25
|
||||
12 -26 15 -10 24 10 6 22 11 26 11 4 0 11 5 15 12 4 6 13 13 21 15 8 2 24 12
|
||||
36 23 13 12 36 27 52 35 15 8 35 21 43 28 8 7 18 12 23 12 4 0 16 8 26 18 42
|
||||
37 52 43 64 36 6 -4 9 -3 4 1 -9 11 43 46 56 38 6 -3 7 -2 4 4 -3 5 27 33 68
|
||||
62 41 28 77 56 80 61 12 20 13 8 9 -87z m-524 -403 c8 -5 30 -17 48 -26 17 -9
|
||||
32 -21 32 -26 0 -5 4 -7 9 -3 5 3 36 -11 68 -30 32 -19 63 -35 69 -35 5 0 21
|
||||
-10 34 -22 14 -13 25 -21 25 -18 1 8 154 -64 155 -73 0 -5 -4 -5 -10 -2 -6 4
|
||||
-7 -1 -3 -10 3 -10 0 -21 -8 -26 -11 -6 -11 -9 -1 -9 9 0 10 -5 4 -17 -5 -10
|
||||
-15 -40 -21 -68 -7 -27 -18 -63 -26 -80 -7 -16 -24 -58 -36 -92 -13 -34 -28
|
||||
-59 -33 -56 -5 3 -6 1 -3 -4 9 -14 -22 -75 -34 -68 -5 4 -6 -1 -3 -10 4 -9 -5
|
||||
-33 -20 -55 -14 -22 -26 -42 -26 -46 0 -3 -19 -33 -42 -67 -185 -267 -312
|
||||
-305 -471 -142 -56 56 -164 205 -150 205 4 0 3 4 -3 8 -14 8 -97 164 -109 202
|
||||
-4 14 -16 43 -26 65 -25 55 -76 216 -81 251 -2 23 1 29 14 28 10 -1 16 1 14 5
|
||||
-3 4 19 18 47 31 29 13 55 29 59 35 4 5 8 7 8 3 0 -4 23 6 50 22 28 16 50 26
|
||||
50 22 0 -4 4 -2 8 3 4 6 25 19 47 30 22 11 47 25 55 31 38 28 169 92 176 86 4
|
||||
-4 4 -2 1 5 -19 32 42 11 133 -47z"/>
|
||||
<path d="M2556 3192 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2455 2831 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M2500 2790 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M2144 2592 c-70 -35 -108 -103 -100 -179 3 -38 32 -93 62 -118 9 -7
|
||||
1 -43 -31 -140 -65 -196 -66 -176 5 -173 33 2 60 -1 60 -5 0 -5 4 -6 8 -3 13
|
||||
8 227 10 239 3 18 -11 23 7 13 39 -6 16 -28 83 -49 149 l-39 120 29 27 c16 16
|
||||
29 30 29 33 0 22 1 26 8 22 4 -3 8 20 8 51 3 88 -33 145 -108 176 -44 19 -96
|
||||
18 -134 -2z"/>
|
||||
<path d="M1113 2105 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M859 1903 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
|
||||
<path d="M1436 1803 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
||||
<path d="M3760 1596 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1616 1691 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
|
||||
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
|
||||
<path d="M2710 1590 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
|
||||
-4 -4 -4 -10z"/>
|
||||
<path d="M1090 831 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
19
.storybook/static/favicon_package/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
37
.storybook/static/fonts/WorkSans/font.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
/*400*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("./worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("./worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
/*700*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
BIN
.storybook/static/fonts/WorkSans/worksans-bold-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-medium-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-regular-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-semibold-webfont.woff2
Normal file
BIN
.storybook/static/logo.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
.storybook/static/preview.png
Normal file
After Width: | Height: | Size: 104 KiB |
452
CHANGELOG.md
@ -1,452 +0,0 @@
|
||||
### **1.1.3** (2021-06-14)
|
||||
|
||||
- Add missing key in Login for providers
|
||||
|
||||
### **1.1.2** (2021-06-14)
|
||||
|
||||
|
||||
|
||||
### **1.1.1** (2021-06-14)
|
||||
|
||||
|
||||
|
||||
## **1.1.0** (2021-06-14)
|
||||
|
||||
- Add login-idp-link-confirm.ftl
|
||||
- Fix login-update-profile.ftl
|
||||
- Add login-update-profile.ftl page
|
||||
- Fix default background bug
|
||||
- Remove unused 'markdown' dependency
|
||||
- Fix warning related to powerhooks_useGlobalState_kcLanguageTag
|
||||
- Update README.md
|
||||
|
||||
### **1.0.4** (2021-05-28)
|
||||
|
||||
- Instructions for custom themes with custom components
|
||||
|
||||
### **1.0.3** (2021-05-23)
|
||||
|
||||
- Instuction about how to integrate with non CRA projects
|
||||
- Add mention to awesome list
|
||||
|
||||
### **1.0.2** (2021-05-01)
|
||||
|
||||
|
||||
|
||||
### **1.0.1** (2021-05-01)
|
||||
|
||||
- Fix: LoginOtp (and not otc)
|
||||
|
||||
# **1.0.0** (2021-05-01)
|
||||
|
||||
- #4: Guide for implementing a missing page
|
||||
- Support OTP #4
|
||||
|
||||
### **0.4.4** (2021-04-29)
|
||||
|
||||
- Fix previous release
|
||||
|
||||
### **0.4.3** (2021-04-29)
|
||||
|
||||
- Add infos about the plugin that defines authorizedMailDomains
|
||||
|
||||
### **0.4.2** (2021-04-29)
|
||||
|
||||
- Client side validation of allowed email domains
|
||||
- Support email whitlisting
|
||||
- Restore kickstart video in the readme
|
||||
- Update README.md
|
||||
- Update README.md
|
||||
- Important readme update
|
||||
|
||||
### **0.4.1** (2021-04-11)
|
||||
|
||||
- Quietly re-introduce --external-assets
|
||||
- Give example of customization
|
||||
|
||||
## **0.4.0** (2021-04-09)
|
||||
|
||||
- Acual support of Therms of services
|
||||
|
||||
### **0.3.24** (2021-04-08)
|
||||
|
||||
- Add missing dependency: markdown
|
||||
|
||||
### **0.3.23** (2021-04-08)
|
||||
|
||||
- Allow to lazily load therms
|
||||
|
||||
### **0.3.22** (2021-04-08)
|
||||
|
||||
- update powerhooks
|
||||
- Support terms and condition
|
||||
- Fix info.ftl
|
||||
- For useKcMessage we prefer returning callbacks with a changing references
|
||||
|
||||
### **0.3.21** (2021-04-04)
|
||||
|
||||
- Update powerhooks
|
||||
|
||||
### **0.3.20** (2021-04-01)
|
||||
|
||||
- Always catch freemarker errors
|
||||
|
||||
### **0.3.19** (2021-04-01)
|
||||
|
||||
- Fix previous release
|
||||
|
||||
### **0.3.18** (2021-04-01)
|
||||
|
||||
- Fix error.ftt, Adopt best effort strategy to convert ftl values into JS
|
||||
|
||||
### **0.3.17** (2021-03-29)
|
||||
|
||||
- Use push instead of replace in keycloak-js adapter to enable going back
|
||||
|
||||
### **0.3.15** (2021-03-28)
|
||||
|
||||
- Remove all reference to --external-assets, broken feature
|
||||
|
||||
### **0.3.14** (2021-03-28)
|
||||
|
||||
- Fix standalone mode: imports from js
|
||||
|
||||
### **0.3.13** (2021-03-26)
|
||||
|
||||
|
||||
|
||||
### **0.3.12** (2021-03-26)
|
||||
|
||||
- Fix mocksContext
|
||||
|
||||
### **0.3.11** (2021-03-26)
|
||||
|
||||
- Fix previous build, improve README
|
||||
|
||||
### **0.3.10** (2021-03-26)
|
||||
|
||||
- Handle <style> tag, improve documentation
|
||||
|
||||
### **0.3.9** (2021-03-25)
|
||||
|
||||
- Update readme
|
||||
- Document --external-assets
|
||||
- Update README.md
|
||||
- Update README.md
|
||||
- Update README.md
|
||||
|
||||
### **0.3.8** (2021-03-22)
|
||||
|
||||
- Make standalone mode the default
|
||||
|
||||
### **0.3.7** (2021-03-22)
|
||||
|
||||
- (test) external asset mode by default
|
||||
|
||||
### **0.3.6** (2021-03-22)
|
||||
|
||||
- Fix previous release
|
||||
|
||||
### **0.3.5** (2021-03-22)
|
||||
|
||||
- support homepage with urlPath
|
||||
|
||||
### **0.3.4** (2021-03-22)
|
||||
|
||||
- Bugfix: Import assets from CSS
|
||||
|
||||
### **0.3.3** (2021-03-22)
|
||||
|
||||
- Fix submit not receving correct text
|
||||
|
||||
### **0.3.2** (2021-03-21)
|
||||
|
||||
- Fix broken previous release
|
||||
|
||||
### **0.3.1** (2021-03-21)
|
||||
|
||||
- kcHeaderClass can be updated after initial mount
|
||||
|
||||
## **0.3.0** (2021-03-20)
|
||||
|
||||
- Bump version
|
||||
- Feat: Cary over states using URL search params
|
||||
- Bugfix: with kcHtmlClass
|
||||
|
||||
### **0.2.10** (2021-03-19)
|
||||
|
||||
- Remove dependency to denoify
|
||||
|
||||
### **0.2.9** (2021-03-19)
|
||||
|
||||
- Update deps and CI workflow
|
||||
|
||||
### **0.2.8** (2021-03-19)
|
||||
|
||||
- Bugfix: keycloak_build that grow and grow in size
|
||||
- Add disclaimer about maitainment strategy
|
||||
- Add a note for tested version support
|
||||
|
||||
### **0.2.7** (2021-03-13)
|
||||
|
||||
- Bump version
|
||||
- Update README.md
|
||||
- Update README.md
|
||||
|
||||
### **0.2.6** (2021-03-10)
|
||||
|
||||
- Fix generated gitignore
|
||||
|
||||
### **0.2.5** (2021-03-10)
|
||||
|
||||
- Fix generated .gitignore
|
||||
|
||||
### **0.2.4** (2021-03-10)
|
||||
|
||||
- Update README.md
|
||||
|
||||
### **0.2.3** (2021-03-09)
|
||||
|
||||
- fix gitignore generation
|
||||
|
||||
### **0.2.2** (2021-03-08)
|
||||
|
||||
- Add table of content
|
||||
- Update README.md
|
||||
- Update README.md
|
||||
|
||||
## **0.2.1** (2021-03-08)
|
||||
|
||||
- Update ci.yaml
|
||||
- Update readme
|
||||
- Update readme
|
||||
- update deps
|
||||
- Update readme
|
||||
- Add all mocks for testing
|
||||
- many small fixes
|
||||
|
||||
### **0.1.6** (2021-03-07)
|
||||
|
||||
- Fix Turkish
|
||||
|
||||
### **0.1.5** (2021-03-07)
|
||||
|
||||
- Fix getKcLanguageLabel
|
||||
|
||||
### **0.1.4** (2021-03-07)
|
||||
|
||||
|
||||
|
||||
### **0.1.3** (2021-03-07)
|
||||
|
||||
- Implement LoginVerifyEmail
|
||||
- Implement login-reset-password.ftl
|
||||
|
||||
### **0.1.2** (2021-03-07)
|
||||
|
||||
- Fix build
|
||||
- Fix build
|
||||
|
||||
### **0.1.1** (2021-03-06)
|
||||
|
||||
- Implement Error page
|
||||
- rename pageBasename by pageId
|
||||
- Implement reactive programing for language switching
|
||||
- Add Info page, refactor
|
||||
|
||||
## **0.1.0** (2021-03-05)
|
||||
|
||||
- Rename keycloakify
|
||||
|
||||
### **0.0.33** (2021-03-05)
|
||||
|
||||
- Fix syncronization with non react pages
|
||||
|
||||
### **0.0.32** (2021-03-05)
|
||||
|
||||
- bump version
|
||||
- Add log to tell when we are using react
|
||||
- Fix missing parentesis
|
||||
|
||||
### **0.0.31** (2021-03-05)
|
||||
|
||||
- Fix typo
|
||||
- Fix register page 500
|
||||
|
||||
### **0.0.30** (2021-03-05)
|
||||
|
||||
- Edit language statistique
|
||||
|
||||
### **0.0.30** (2021-03-05)
|
||||
|
||||
- avoid escaping urls
|
||||
- Use default value instead of value
|
||||
- Fix double single quote problem in messages
|
||||
- Fix typo
|
||||
- Fix non editable username
|
||||
- Fix some bugs
|
||||
- Fix Object.deepAssign
|
||||
- Make the dongle to download smaller
|
||||
- Split kcContext among pages
|
||||
- Implement register
|
||||
|
||||
### **0.0.29** (2021-03-04)
|
||||
|
||||
- Fix build
|
||||
- Fix i18n
|
||||
- Login appear to be working now
|
||||
- closer but not there yet
|
||||
|
||||
### **0.0.28** (2021-03-03)
|
||||
|
||||
- fix build
|
||||
- There is no reason not to let use translations outside of keycloak
|
||||
|
||||
### **0.0.27** (2021-03-02)
|
||||
|
||||
- Implement entrypoint
|
||||
|
||||
### **0.0.26** (2021-03-02)
|
||||
|
||||
- Login page implemented
|
||||
- Implement login
|
||||
- remove unesseary log
|
||||
|
||||
### **0.0.25** (2021-03-02)
|
||||
|
||||
- Fix build and reduce size
|
||||
- Implement the template
|
||||
|
||||
### **0.0.24** (2021-03-01)
|
||||
|
||||
- update
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.23** (2021-03-01)
|
||||
|
||||
- update
|
||||
|
||||
### **0.0.23** (2021-03-01)
|
||||
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.23** (2021-03-01)
|
||||
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.23** (2021-03-01)
|
||||
|
||||
- update
|
||||
- Handle formatting in translation function
|
||||
|
||||
### **0.0.22** (2021-02-28)
|
||||
|
||||
- Split page messages
|
||||
|
||||
### **0.0.21** (2021-02-28)
|
||||
|
||||
- Restore yarn file
|
||||
- Multiple fixes
|
||||
- Update deps
|
||||
- Update deps
|
||||
- includes translations
|
||||
- Update README.md
|
||||
- improve docs
|
||||
- update
|
||||
- Update README.md
|
||||
- update
|
||||
- update
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.20** (2021-02-27)
|
||||
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.19** (2021-02-27)
|
||||
|
||||
- update
|
||||
- update
|
||||
|
||||
### **0.0.18** (2021-02-23)
|
||||
|
||||
- Bump version number
|
||||
- Moving on with implementation of the lib
|
||||
- Update readme
|
||||
- Readme eddit
|
||||
- Fixing video link
|
||||
|
||||
### **0.0.16** (2021-02-23)
|
||||
|
||||
- Bump version
|
||||
- Give test container credentials
|
||||
|
||||
### **0.0.14** (2021-02-23)
|
||||
|
||||
- Bump version number
|
||||
- enable the docker container to be run from the root of the react project
|
||||
|
||||
### **0.0.13** (2021-02-23)
|
||||
|
||||
- bump version
|
||||
|
||||
### **0.0.12** (2021-02-23)
|
||||
|
||||
- update readme
|
||||
|
||||
### **0.0.11** (2021-02-23)
|
||||
|
||||
- Add documentation
|
||||
|
||||
### **0.0.10** (2021-02-23)
|
||||
|
||||
- Remove extra closing bracket
|
||||
|
||||
### **0.0.9** (2021-02-22)
|
||||
|
||||
- fix container startup script
|
||||
- minor update
|
||||
|
||||
### **0.0.8** (2021-02-21)
|
||||
|
||||
- Include theme properties
|
||||
|
||||
### **0.0.7** (2021-02-21)
|
||||
|
||||
- fix build
|
||||
- Fix bundle
|
||||
|
||||
### **0.0.6** (2021-02-21)
|
||||
|
||||
- Include missing files in the release bundle
|
||||
|
||||
### **0.0.5** (2021-02-21)
|
||||
|
||||
- Bump version number
|
||||
- Make the install faster
|
||||
|
||||
### **0.0.4** (2021-02-21)
|
||||
|
||||
- Fix script visibility
|
||||
|
||||
### **0.0.3** (2021-02-21)
|
||||
|
||||
- Do not run tests on window
|
||||
- Add script for downloading base themes
|
||||
- Generate debug files to be able to test the container
|
||||
- Fix many little bugs
|
||||
- refactor
|
||||
- Almoste there
|
||||
- Things are starting to take form
|
||||
- Seems to be working
|
||||
- First draft
|
||||
- Remove eslint and prettyer
|
||||
|
||||
### **0.0.2** (2021-02-20)
|
||||
|
||||
- Update package.json
|
||||
|
3
CONTRIBUTING.md
Normal file
@ -0,0 +1,3 @@
|
||||
Looking to contribute? Thank you! PR are more than welcome.
|
||||
|
||||
Please refers to [this documentation page](https://docs.keycloakify.dev/faq-and-help/contributing) that will help you get started.
|
505
README.md
@ -2,387 +2,182 @@
|
||||
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
||||
</p>
|
||||
<p align="center">
|
||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
||||
<i>🔏 Keycloak Theming for the Modern Web 🔏</i>
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=develop">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
||||
<img src="https://img.shields.io/npm/dw/keycloakify">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||
<a href="https://github.com/garronej/keycloakify/actions">
|
||||
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/keycloakify">
|
||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
Check out our discord server!<br/>
|
||||
<a href="https://discord.gg/mJdYJSdcm4">
|
||||
<img src="https://dcbadge.limes.pink/api/server/kYFZG7fQmn"/>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.keycloakify.dev">Home</a>
|
||||
-
|
||||
<a href="https://docs.keycloakify.dev">Documentation</a>
|
||||
-
|
||||
<a href="https://storybook.keycloakify.dev">Storybook</a>
|
||||
-
|
||||
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<i>Ultimately this build tool generates a Keycloak theme</i>
|
||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
|
||||
<br/>
|
||||
<br/>
|
||||
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
||||
</p>
|
||||
|
||||
# Motivations
|
||||
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
|
||||
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
|
||||
It involves, however, a lot of raw JS/CSS/[FTL]() hacking, and bundling the theme is not exactly straightforward.
|
||||
> 📣 **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).
|
||||
|
||||
Beyond that, if you use Keycloak for a specific app you want your login page to be tightly integrated with it.
|
||||
Ideally, you don't want the user to notice when he is being redirected away.
|
||||
## Sponsors
|
||||
|
||||
Trying to reproduce the look and feel of a specific app in another stack is not an easy task not to mention
|
||||
the cheer amount of maintenance that it involves.
|
||||
Project backers, we trust and recommend their services.
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

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

|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<i>Without keycloakify, users suffers from a harsh context switch, no fronted form pre-validation</i><br>
|
||||
<img src="https://user-images.githubusercontent.com/6702424/108838381-dbbbcf80-75d3-11eb-8ae8-db41563ef9db.gif">
|
||||
<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>
|
||||
|
||||
Wouldn't it be great if we could just design the login and register pages as if they were part of our app?
|
||||
Here is `keycloakify` for you 🍸
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

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

|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<i> <a href="https://datalab.sspcloud.fr">With keycloakify:</a> </i>
|
||||
<br>
|
||||
<img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif">
|
||||
</p>
|
||||
|
||||
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
|
||||
|
||||
If you already have a Keycloak custom theme, it can be easily ported to Keycloakify.
|
||||
|
||||
---
|
||||
|
||||
|
||||
- [Motivations](#motivations)
|
||||
- [Requirements](#requirements)
|
||||
- [My framework doesn’t seem to be supported, what can I do?](#my-framework-doesnt-seem-to-be-supported-what-can-i-do)
|
||||
- [How to use](#how-to-use)
|
||||
- [Setting up the build tool](#setting-up-the-build-tool)
|
||||
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
|
||||
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
||||
- [Hot reload](#hot-reload)
|
||||
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
|
||||
- [Support for Terms and conditions](#support-for-terms-and-conditions)
|
||||
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
|
||||
- [GitHub Actions](#github-actions)
|
||||
- [Limitations](#limitations)
|
||||
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported)
|
||||
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-thesrc-dir)
|
||||
- [Example of setup that **won't** work](#example-of-setup-that-wont-work)
|
||||
- [Possible workarounds](#possible-workarounds)
|
||||
- [Implement context persistence (optional)](#implement-context-persistence-optional)
|
||||
- [Kickstart video](#kickstart-video)
|
||||
- [Email domain whitelist](#email-domain-whitelist)
|
||||
|
||||
# Requirements
|
||||
|
||||
Tested with the following Keycloak versions:
|
||||
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
|
||||
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
|
||||
|
||||
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
|
||||
(before you customize it) will always be the ones of Keycloak v11.
|
||||
|
||||
This tool assumes you are bundling your app with Webpack (tested with 4.44.2) .
|
||||
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
|
||||
and a `build/static/` directory generated by webpack.
|
||||
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432)
|
||||
## My framework doesn’t seem to be supported, what can I do?
|
||||
|
||||
Currently Keycloakify is only compatible with `create-react-app` apps.
|
||||
It doesn’t mean that you can't use Keycloakify if you are using Next.js, Express or any other
|
||||
framework that involves SSR but your Keycloak theme will need to be a standalone project.
|
||||
Find specific instructions about how to get started [**here**](https://github.com/garronej/keycloakify-demo-app#keycloak-theme-only).
|
||||
|
||||
To share your styles between your main app and your login pages you will need to externalize your design system by making it a
|
||||
separate module. Checkout [ts_ci](https://github.com/garronej/ts_ci), it can help with that.
|
||||
# How to use
|
||||
## Setting up the build tool
|
||||
|
||||
```bash
|
||||
yarn add keycloakify
|
||||
```
|
||||
|
||||
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
|
||||
```json
|
||||
"scripts": {
|
||||
"keycloak": "yarn build && build-keycloak-theme",
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
yarn keycloak # generates keycloak-theme.jar
|
||||
```
|
||||
|
||||
On the console will be printed all the instructions about how to load the generated theme in Keycloak
|
||||
|
||||
### Changing just the look of the default Keycloak theme
|
||||
|
||||
The first approach is to only customize the style of the default Keycloak login by providing
|
||||
your own class names.
|
||||
|
||||
If you have created a new React project specifically to create a Keycloak theme and nothing else then
|
||||
your index should look something like:
|
||||
|
||||
`src/index.tsx`
|
||||
```tsx
|
||||
import { App } from "./<wherever>/App";
|
||||
import {
|
||||
KcApp,
|
||||
defaultKcProps,
|
||||
kcContext
|
||||
} from "keycloakify";
|
||||
import { css } from "tss-react";
|
||||
|
||||
const myClassName = css({ "color": "red" });
|
||||
|
||||
reactDom.render(
|
||||
<KcApp
|
||||
kcContext={kcContext}
|
||||
{...{
|
||||
...defaultKcProps,
|
||||
"kcHeaderWrapperClass": myClassName
|
||||
}}
|
||||
/>
|
||||
document.getElementById("root")
|
||||
);
|
||||
```
|
||||
|
||||
If you share a unique project for your app and the Keycloak theme, your index should look
|
||||
more like this:
|
||||
|
||||
`src/index.tsx`
|
||||
```tsx
|
||||
import { App } from "./<wherever>/App";
|
||||
import {
|
||||
KcApp,
|
||||
defaultKcProps,
|
||||
kcContext
|
||||
} from "keycloakify";
|
||||
import { css } from "tss-react";
|
||||
|
||||
const myClassName = css({ "color": "red" });
|
||||
|
||||
reactDom.render(
|
||||
// Unless the app is currently being served by Keycloak
|
||||
// kcContext is undefined.
|
||||
kcContext !== undefined ?
|
||||
<KcApp
|
||||
kcContext={kcContext}
|
||||
{...{
|
||||
...defaultKcProps,
|
||||
"kcHeaderWrapperClass": myClassName
|
||||
}}
|
||||
/> :
|
||||
<App />, // Your actual app
|
||||
document.getElementById("root")
|
||||
);
|
||||
```
|
||||
|
||||
|
||||
<p align="center">
|
||||
<i>result:</i></br>
|
||||
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
|
||||
<a href="https://www.zone2.tech/services/keycloak-consulting">
|
||||
<i><strong>Keycloak Consulting Services</strong> - Your partner in Keycloak deployment, configuration, and extension development for optimized identity management solutions.</i>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx)
|
||||
(the [index.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) )
|
||||
and the result you can expect:
|
||||
<div align="center">
|
||||
|
||||

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

|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<i> <a href="https://datalab.sspcloud.fr">Customization using only CSS:</a> </i>
|
||||
<br>
|
||||
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
|
||||
<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/>
|
||||
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
||||
</p>
|
||||
|
||||
### Changing the look **and** feel
|
||||
## Contributors ✨
|
||||
|
||||
If you want to really re-implement the pages, the best approach is to
|
||||
create your own version of the [`<KcApp />`](https://github.com/garronej/keycloakify/blob/develop/src/lib/components/KcApp.tsx).
|
||||
Copy/past some of [the components](https://github.com/garronej/keycloakify/tree/develop/src/lib/components) provided by this module and start hacking around.
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
You can find an example of such customization [here](https://github.com/InseeFrLab/onyxia-ui/tree/master/src/app/components/KcApp).
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lordvlad"><img src="https://avatars.githubusercontent.com/u/1217769?v=4?s=100" width="100px;" alt="Waldemar Reusch"/><br /><sub><b>Waldemar Reusch</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lordvlad" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/"><img src="https://avatars.githubusercontent.com/u/10997562?v=4?s=100" width="100px;" alt="William Will"/><br /><sub><b>William Will</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=willwill96" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ann2827"><img src="https://avatars.githubusercontent.com/u/32645809?v=4?s=100" width="100px;" alt="Bystrova Ann"/><br /><sub><b>Bystrova Ann</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Ann2827" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkreuzmayr"><img src="https://avatars.githubusercontent.com/u/20108212?v=4?s=100" width="100px;" alt="Michael Kreuzmayr"/><br /><sub><b>Michael Kreuzmayr</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=mkreuzmayr" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://coolmathgames.tech"><img src="https://avatars.githubusercontent.com/u/6877780?v=4?s=100" width="100px;" alt="Mary "/><br /><sub><b>Mary </b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Mstrodl" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tasyp.xyz/"><img src="https://avatars.githubusercontent.com/u/6623212?v=4?s=100" width="100px;" alt="German Öö"/><br /><sub><b>German Öö</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Tasyp" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://revolunet.com"><img src="https://avatars.githubusercontent.com/u/124937?v=4?s=100" width="100px;" alt="Julien Bouquillon"/><br /><sub><b>Julien Bouquillon</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=revolunet" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aidangilmore"><img src="https://avatars.githubusercontent.com/u/32880357?v=4?s=100" width="100px;" alt="Aidan Gilmore"/><br /><sub><b>Aidan Gilmore</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=aidangilmore" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0x-Void"><img src="https://avatars.githubusercontent.com/u/32745739?v=4?s=100" width="100px;" alt="Void"/><br /><sub><b>Void</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=0x-Void" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juffe"><img src="https://avatars.githubusercontent.com/u/5393231?v=4?s=100" width="100px;" alt="juffe"/><br /><sub><b>juffe</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=juffe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lazToum"><img src="https://avatars.githubusercontent.com/u/4764837?v=4?s=100" width="100px;" alt="Lazaros Toumanidis"/><br /><sub><b>Lazaros Toumanidis</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lazToum" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcmrf"><img src="https://avatars.githubusercontent.com/u/9928519?v=4?s=100" width="100px;" alt="Marc"/><br /><sub><b>Marc</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=marcmrf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://kasir-barati.github.io"><img src="https://avatars.githubusercontent.com/u/73785723?v=4?s=100" width="100px;" alt="Kasir Barati"/><br /><sub><b>Kasir Barati</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kasir-barati" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asashay"><img src="https://avatars.githubusercontent.com/u/10714670?v=4?s=100" width="100px;" alt="Alex Oliynyk"/><br /><sub><b>Alex Oliynyk</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=asashay" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/satanshiro"><img src="https://avatars.githubusercontent.com/u/38865738?v=4?s=100" width="100px;" alt="satanshiro"/><br /><sub><b>satanshiro</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=satanshiro" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://poelhekke.dev"><img src="https://avatars.githubusercontent.com/u/1632377?v=4?s=100" width="100px;" alt="Koen Poelhekke"/><br /><sub><b>Koen Poelhekke</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kpoelhekke" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xgp"><img src="https://avatars.githubusercontent.com/u/244253?v=4?s=100" width="100px;" alt="Garth"/><br /><sub><b>Garth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=xgp" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BlackVoid"><img src="https://avatars.githubusercontent.com/u/673720?v=4?s=100" width="100px;" alt="Felix Gustavsson"/><br /><sub><b>Felix Gustavsson</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=BlackVoid" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madmadson"><img src="https://avatars.githubusercontent.com/u/798831?v=4?s=100" width="100px;" alt="Tobias Matt"/><br /><sub><b>Tobias Matt</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=madmadson" 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>
|
||||
</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>
|
||||
</table>
|
||||
|
||||
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
Note that you don’t have to re write **all** components, only the ones that you most need customized.
|
||||
Look at [here for example](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L112-L120).
|
||||
We want to have our very own login and register page, so we wrote customs [Login.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/master/src/app/components/KcApp/Login.tsx) and [Register.txs](https://github.com/InseeFrLab/onyxia-ui/blob/master/src/app/components/KcApp/Register.tsx), we import them [here](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L9-L10) and use them [here](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L113-L114).
|
||||
We don't want to bother, however, customizing `login-reset-password.ftl`. We are fine using the component from [the default theme](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L13) with just some [CSS customization](https://github.com/InseeFrLab/onyxia-ui/blob/3bf18aa82b198fc6ba7998c30abf0a9ae54a58b1/src/app/components/KcApp/KcApp.tsx#L103-L110).
|
||||
|
||||
WARNING: If you chose to go this way use:
|
||||
```json
|
||||
"dependencies": {
|
||||
"keycloakify": "~X.Y.Z"
|
||||
}
|
||||
```
|
||||
in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app.
|
||||
|
||||
### Hot reload
|
||||
|
||||
Rebuild the theme each time you make a change to see the result is not practical.
|
||||
If you want to test your login screens outside of Keycloak, in [storybook](https://storybook.js.org/)
|
||||
for example you can use `kcContextMocks`.
|
||||
|
||||
```tsx
|
||||
import {
|
||||
KcApp,
|
||||
defaultKcProps,
|
||||
kcContextMocks
|
||||
} from "keycloakify";
|
||||
|
||||
reactDom.render(
|
||||
<KcApp
|
||||
kcContext={kcContextMocks.kcLoginContext}
|
||||
{...defaultKcProps}
|
||||
/>
|
||||
document.getElementById("root")
|
||||
);
|
||||
```
|
||||
|
||||
Then `yarn start`, you will see your login page.
|
||||
|
||||
Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-app/blob/main/src/index.tsx)
|
||||
|
||||
## Enable loading in a blink of an eye of login pages ⚡ (--external-assets)
|
||||
|
||||
By default the theme generated is standalone. Meaning that when your users
|
||||
reach the login pages all scripts, images and stylesheet are downloaded from the Keycloak server.
|
||||
If you are specifically building a theme to integrate with an app or a website that allows users
|
||||
to first browse unauthenticated before logging in, you will get a significant
|
||||
performance boost if you jump through those hoops:
|
||||
|
||||
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2)
|
||||
- Build the theme using `npx build-keycloak-theme --external-assets` [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/.github/workflows/ci.yaml#L21)
|
||||
- Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app.
|
||||
- Make sure not to build your app and the keycloak theme separately
|
||||
and remember to update the Keycloak theme every time you update your app.
|
||||
- Be mindful that if your app is down your login pages are down as well.
|
||||
|
||||
Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-app#about-keycloakify)
|
||||
|
||||
# Support for Terms and conditions
|
||||
|
||||
[Many organizations have a requirement that when a new user logs in for the first time, they need to agree to the terms and conditions of the website.](https://www.keycloak.org/docs/4.8/server_admin/#terms-and-conditions).
|
||||
|
||||
First you need to enable the required action on the Keycloak server admin console:
|
||||

|
||||
|
||||
Then to load your own therms of services using [like this](https://github.com/garronej/keycloakify-demo-app/blob/8168c928a66605f2464f9bd28a4dc85fb0a231f9/src/index.tsx#L42-L66).
|
||||
|
||||
# Some pages still have the default theme. Why?
|
||||
|
||||
This project only support the most common user facing pages of Keycloak login.
|
||||
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
|
||||
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
|
||||
If you need to customize pages that are not supported yet you can submit an issue about it and wait for me get it implemented.
|
||||
If you can't wait, PR are welcome! [Here](https://github.com/InseeFrLab/keycloakify/commit/0163459ad6b1ad0afcc34fae5f3cc28dbcf8b4a7) is the commit that adds support
|
||||
for the `login-otp.ftl` page. You can use it as a model for implementing other pages.
|
||||
|
||||
# GitHub Actions
|
||||
|
||||

|
||||
|
||||
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
||||
the building and publishing of the theme (the .jar file).
|
||||
|
||||
|
||||
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
|
||||
|
||||
- For building the theme: `mvn` (Maven) must be installed (but you can build the theme in the CI)
|
||||
- For testing the theme in a local Keycloak container (which is not mandatory for development):
|
||||
`rm`, `mkdir`, `wget`, `unzip` are assumed to be available and `docker` up and running.
|
||||
# Limitations
|
||||
## `process.env.PUBLIC_URL` not supported.
|
||||
|
||||
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
|
||||
(This isn't recommended anyway).
|
||||
|
||||
|
||||
|
||||
## `@font-face` importing fonts from the `src/` dir
|
||||
|
||||
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
|
||||
this limitation doesn't apply, you can import fonts however you see fit.
|
||||
|
||||
### Example of setup that **won't** work
|
||||
|
||||
- We have a `fonts/` directory in `src/`
|
||||
- We import the font like this [`src: url("/fonts/my-font.woff2") format("woff2");`](https://github.com/garronej/keycloakify-demo-app/blob/07d54a3012ef354ee12b1374c6f7ad1cb125d56b/src/fonts.scss#L4) in a `.scss` a file.
|
||||
|
||||
### Possible workarounds
|
||||
|
||||
- Use [`--external-assets`](#enable-loading-in-a-blink-of-a-eye-of-login-pages-).
|
||||
- If it is possible, use Google Fonts or any other font provider.
|
||||
- If you want to host your font recommended approach is to move your fonts into the `public`
|
||||
directory and to place your `@font-face` statements in the `public/index.html`.
|
||||
Example [here](https://github.com/InseeFrLab/onyxia-ui/blob/0e3a04610cfe872ca71dad59e05ced8f785dee4b/public/index.html#L6-L51).
|
||||
- You can also [use non relative url](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/src/fonts.scss#L16) but don't forget [`Access-Control-Allow-Origin`](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/nginx.conf#L17-L19).
|
||||
|
||||
# Implement context persistence (optional)
|
||||
|
||||
If, before logging in, a user has selected a specific language
|
||||
you don't want it to be reset to default when the user gets redirected to
|
||||
the login or register pages.
|
||||
|
||||
Same goes for the dark mode, you don't want, if the user had it enabled
|
||||
to show the login page with light themes.
|
||||
|
||||
The problem is that you are probably using `localStorage` to persist theses values across
|
||||
reload but, as the Keycloak pages are not served on the same domain that the rest of your
|
||||
app you won't be able to carry over states using `localStorage`.
|
||||
|
||||
The only reliable solution is to inject parameters into the URL before
|
||||
redirecting to Keycloak. We integrate with
|
||||
[`keycloak-js`](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc),
|
||||
by providing you a way to tell `keycloak-js` that you would like to inject
|
||||
some search parameters before redirecting.
|
||||
|
||||
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
|
||||
|
||||
You can implement your own mechanism to pass the states in the URL and
|
||||
restore it on the other side but we recommend using `powerhooks/useGlobalState`
|
||||
from the library [`powerhooks`](https://www.powerhooks.dev) that provide an elegant
|
||||
way to handle states such as `isDarkModeEnabled` or `selectedLanguage`.
|
||||
|
||||
Let's modify [the example](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc) from the official `keycloak-js` documentation to
|
||||
enables the states of `useGlobalStates` to be injected in the URL before redirecting.
|
||||
Note that the states are automatically restored on the other side by `powerhooks`
|
||||
|
||||
```typescript
|
||||
import keycloak_js from "keycloak-js";
|
||||
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
|
||||
import { createKeycloakAdapter } from "keycloakify";
|
||||
|
||||
//...
|
||||
|
||||
const keycloakInstance = keycloak_js({
|
||||
"url": "http://keycloak-server/auth",
|
||||
"realm": "myrealm",
|
||||
"clientId": "myapp"
|
||||
});
|
||||
|
||||
keycloakInstance.init({
|
||||
"onLoad": 'check-sso',
|
||||
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html",
|
||||
"adapter": createKeycloakAdapter({
|
||||
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
|
||||
keycloakInstance
|
||||
})
|
||||
});
|
||||
|
||||
//...
|
||||
```
|
||||
|
||||
If you really want to go the extra miles and avoid having the white
|
||||
flash of the blank html before the js bundle have been evaluated
|
||||
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
||||
|
||||
# Kickstart video
|
||||
|
||||
*NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded*
|
||||
[](https://youtu.be/xTz0Rj7i2v8)
|
||||
|
||||
# Email domain whitelist
|
||||
|
||||
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
|
||||
and `kcRegisterContext["authorizedMailDomains"]` to validate on.
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
136
package.json
Executable file → Normal file
@ -1,33 +1,55 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "1.1.3",
|
||||
"description": "Keycloak theme generator for Reacts app",
|
||||
"version": "11.8.23",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/garronej/keycloakify.git"
|
||||
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||
},
|
||||
"main": "dist/lib/index.js",
|
||||
"types": "dist/lib/index.d.ts",
|
||||
"scripts": {
|
||||
"clean": "rimraf dist/",
|
||||
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||
"test": "node dist/test",
|
||||
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
||||
"generate-messages": "node dist/bin/generate-i18n-messages.js"
|
||||
"prepare": "tsx scripts/generate-i18n-messages.ts",
|
||||
"build": "tsx scripts/build/main.ts",
|
||||
"storybook": "tsx scripts/start-storybook.ts",
|
||||
"link-in-starter": "tsx scripts/link-in-starter.ts",
|
||||
"test": "yarn test:types && vitest run",
|
||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||
"format": "yarn _format --write",
|
||||
"link-in-app": "tsx scripts/link-in-app.ts",
|
||||
"build-storybook": "tsx scripts/build-storybook.ts",
|
||||
"dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
|
||||
},
|
||||
"bin": {
|
||||
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
|
||||
"install-builtin-keycloak-themes": "dist/bin/install-builtin-keycloak-themes.js"
|
||||
"keycloakify": "dist/bin/main.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,json,md}": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged -v"
|
||||
}
|
||||
},
|
||||
"author": "u/garronej",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src/",
|
||||
"!src/test/",
|
||||
"dist/",
|
||||
"!dist/test/",
|
||||
"!dist/tsconfig.tsbuildinfo"
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/",
|
||||
"dist/bin/**/*.d.ts",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"dist/bin/shared/customHandler.js",
|
||||
"dist/bin/shared/customHandler.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
"dist/vite-plugin/vite-plugin.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"keycloak",
|
||||
@ -36,26 +58,68 @@
|
||||
"FreeMarker",
|
||||
"ftl",
|
||||
"login",
|
||||
"register"
|
||||
"register",
|
||||
"account",
|
||||
"bluehats"
|
||||
],
|
||||
"homepage": "https://github.com/garronej/keycloakify",
|
||||
"devDependencies": {
|
||||
"@types/node": "^10.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "^17.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"evt": "2.0.0-beta.15",
|
||||
"minimal-polyfills": "^2.1.6",
|
||||
"path": "^0.12.7",
|
||||
"powerhooks": "^0.1.0",
|
||||
"react-markdown": "^5.0.3",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"tss-react": "^0.0.12"
|
||||
"tsafe": "^1.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.5",
|
||||
"@babel/generator": "^7.24.5",
|
||||
"@babel/parser": "^7.24.5",
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@storybook/addon-a11y": "^6.5.16",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@types/babel__generator": "^7.6.4",
|
||||
"@types/dompurify": "^2.0.0",
|
||||
"@types/make-fetch-happen": "^10.0.1",
|
||||
"@types/minimist": "^1.2.2",
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/properties-parser": "^0.3.3",
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"babel-loader": "9.1.3",
|
||||
"chalk": "^4.1.2",
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"chokidar-cli": "^3.0.0",
|
||||
"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",
|
||||
"isomorphic-dompurify": "^2.15.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"powerhooks": "^1.0.19",
|
||||
"prettier": "^3.2.5",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"recast": "^0.23.3",
|
||||
"run-exclusive": "^2.2.19",
|
||||
"storybook-dark-mode": "^1.1.2",
|
||||
"termost": "^v0.12.1",
|
||||
"tsc-alias": "^1.8.10",
|
||||
"tss-react": "^4.9.10",
|
||||
"tsx": "^4.15.5",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^5.2.11",
|
||||
"vitest": "^1.6.0",
|
||||
"webpack": "5.93.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"yauzl": "^2.10.0",
|
||||
"zod": "^3.17.10"
|
||||
}
|
||||
}
|
||||
|
27
renovate.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"baseBranches": ["main"],
|
||||
"extends": ["config:base"],
|
||||
"dependencyDashboard": false,
|
||||
"bumpVersion": "patch",
|
||||
"rangeStrategy": "bump",
|
||||
"ignorePaths": [".github/**"],
|
||||
"branchPrefix": "renovate_",
|
||||
"vulnerabilityAlerts": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"packagePatterns": ["*"],
|
||||
"excludePackagePatterns": ["tsafe", "evt"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"packagePatterns": ["tsafe", "evt"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true,
|
||||
"automergeType": "branch",
|
||||
"groupName": "garronej_modules_update"
|
||||
}
|
||||
]
|
||||
}
|
4
scripts/build-storybook.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { run } from "./shared/run";
|
||||
|
||||
run("yarn build");
|
||||
run("npx build-storybook");
|
79
scripts/build/createAccountV1Dir.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { accountMultiPageSupportedLanguages } from "../generate-i18n-messages";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function createAccountV1Dir() {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: "FOR_ACCOUNT_MULTI_PAGE"
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"account-v1"
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "base", "account"),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "account", "resources"),
|
||||
destDirPath: pathJoin(destDirPath, "resources")
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(
|
||||
destDirPath,
|
||||
"resources",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
|
||||
)
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(destDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
"accountResourceProvider=account-v1",
|
||||
"",
|
||||
`locales=${accountMultiPageSupportedLanguages.join(",")}`,
|
||||
"",
|
||||
"styles=" +
|
||||
[
|
||||
"css/account.css",
|
||||
"img/icon-sidebar-active.png",
|
||||
"img/logo.png",
|
||||
...[
|
||||
"patternfly.min.css",
|
||||
"patternfly-additions.min.css",
|
||||
"patternfly-additions.min.css"
|
||||
].map(
|
||||
fileBasename =>
|
||||
`${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}`
|
||||
)
|
||||
].join(" "),
|
||||
"",
|
||||
"##### css classes for form buttons",
|
||||
"# main class used for all buttons",
|
||||
"kcButtonClass=btn",
|
||||
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
|
||||
"kcButtonPrimaryClass=btn-primary",
|
||||
"kcButtonDefaultClass=btn-default",
|
||||
"# classes defining size of the button",
|
||||
"kcButtonLargeClass=btn-lg",
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
73
scripts/build/createPublicKeycloakifyDevResourcesDir.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function createPublicKeycloakifyDevResourcesDir() {
|
||||
await Promise.all(
|
||||
(["login", "account"] as const).map(async themeType => {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
|
||||
themeType
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
base_resources: {
|
||||
const srcDirPath = pathJoin(
|
||||
extractedDirPath,
|
||||
"base",
|
||||
themeType,
|
||||
"resources"
|
||||
);
|
||||
|
||||
if (!(await existsAsync(srcDirPath))) {
|
||||
break base_resources;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath,
|
||||
destDirPath
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(
|
||||
extractedDirPath,
|
||||
"keycloak",
|
||||
themeType,
|
||||
"resources"
|
||||
),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
|
||||
destDirPath: pathJoin(
|
||||
destDirPath,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
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);
|
||||
}
|
188
scripts/build/main.ts
Normal file
@ -0,0 +1,188 @@
|
||||
import * as fs from "fs";
|
||||
import { join } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
|
||||
import { createAccountV1Dir } from "./createAccountV1Dir";
|
||||
import chalk from "chalk";
|
||||
import { run } from "../shared/run";
|
||||
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||
|
||||
(async () => {
|
||||
console.log(chalk.cyan("Building Keycloakify..."));
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
|
||||
fs.renameSync(
|
||||
join("dist", "bin", "main.original.js"),
|
||||
join("dist", "bin", "main.js")
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
|
||||
fs.rmSync(join("dist", "bin", fileBasename));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
|
||||
|
||||
if (
|
||||
!fs
|
||||
.readFileSync(join("dist", "bin", "main.js"))
|
||||
.toString("utf8")
|
||||
.includes("__nccwpck_require__")
|
||||
) {
|
||||
fs.cpSync(
|
||||
join("dist", "bin", "main.js"),
|
||||
join("dist", "bin", "main.original.js")
|
||||
);
|
||||
}
|
||||
|
||||
run(
|
||||
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
|
||||
);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
destDirPath: join("dist", "bin"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath === "index.js") {
|
||||
return {
|
||||
newFileName: "main.js",
|
||||
modifiedSourceCode: sourceCode
|
||||
};
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
let hasBeenPatched = false;
|
||||
|
||||
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage(
|
||||
join("dist", "bin", fileBasename)
|
||||
);
|
||||
|
||||
if (hasBeenPatched_i) {
|
||||
hasBeenPatched = true;
|
||||
}
|
||||
});
|
||||
|
||||
assert(hasBeenPatched);
|
||||
}
|
||||
|
||||
fs.chmodSync(
|
||||
join("dist", "bin", "main.js"),
|
||||
fs.statSync(join("dist", "bin", "main.js")).mode |
|
||||
fs.constants.S_IXUSR |
|
||||
fs.constants.S_IXGRP |
|
||||
fs.constants.S_IXOTH
|
||||
);
|
||||
|
||||
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
|
||||
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
|
||||
vendorFrontendDependencies({ distDirPath: join(process.cwd(), "dist") });
|
||||
|
||||
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
|
||||
fs.renameSync(
|
||||
join("dist", "vite-plugin", "index.original.js"),
|
||||
join("dist", "vite-plugin", "index.js")
|
||||
);
|
||||
}
|
||||
|
||||
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
|
||||
|
||||
if (
|
||||
!fs
|
||||
.readFileSync(join("dist", "vite-plugin", "index.js"))
|
||||
.toString("utf8")
|
||||
.includes("__nccwpck_require__")
|
||||
) {
|
||||
fs.cpSync(
|
||||
join("dist", "vite-plugin", "index.js"),
|
||||
join("dist", "vite-plugin", "index.original.js")
|
||||
);
|
||||
}
|
||||
|
||||
run(
|
||||
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join(
|
||||
"dist",
|
||||
"ncc_out"
|
||||
)}`
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => {
|
||||
assert(!fileBasename.endsWith(".index.js"));
|
||||
assert(!fileBasename.endsWith(".node"));
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
destDirPath: join("dist", "vite-plugin"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
assert(fileRelativePath === "index.js");
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
|
||||
|
||||
{
|
||||
const dirBasename = "src";
|
||||
|
||||
const destDirPath = join("dist", dirBasename);
|
||||
|
||||
fs.rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
fs.cpSync(dirBasename, destDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("stories"),
|
||||
destDirPath: join("dist", "stories"),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (!fileRelativePath.endsWith(".stories.tsx")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
|
||||
await createPublicKeycloakifyDevResourcesDir();
|
||||
await createAccountV1Dir();
|
||||
await downloadKeycloakifyLogging({
|
||||
distDirPath: join(process.cwd(), "dist")
|
||||
});
|
||||
|
||||
console.log(
|
||||
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
||||
);
|
||||
})();
|
||||
|
||||
function patchDeprecatedBufferApiUsage(filePath: string) {
|
||||
const before = fs.readFileSync(filePath).toString("utf8");
|
||||
|
||||
const after = before.replace(
|
||||
`var buffer = new Buffer(toRead);`,
|
||||
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
|
||||
);
|
||||
|
||||
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
|
||||
|
||||
const hasBeenPatched = after !== before;
|
||||
|
||||
return { hasBeenPatched };
|
||||
}
|
97
scripts/build/vendorFrontendDependencies.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, basename as pathBasename, dirname as pathDirname } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { run } from "../shared/run";
|
||||
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
||||
|
||||
export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
const { distDirPath } = params;
|
||||
|
||||
const vendorDirPath = pathJoin(distDirPath, "tools", "vendor");
|
||||
const cacheDirPath = pathJoin(cacheDirPath_base, "vendorFrontendDependencies");
|
||||
|
||||
const extraBundleFileBasenames = new Set<string>();
|
||||
|
||||
fs.readdirSync(vendorDirPath)
|
||||
.filter(fileBasename => fileBasename.endsWith(".js"))
|
||||
.map(fileBasename => pathJoin(vendorDirPath, fileBasename))
|
||||
.forEach(filePath => {
|
||||
{
|
||||
const mapFilePath = `${filePath}.map`;
|
||||
|
||||
if (fs.existsSync(mapFilePath)) {
|
||||
fs.unlinkSync(mapFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(cacheDirPath)) {
|
||||
fs.mkdirSync(cacheDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
const webpackConfigJsFilePath = pathJoin(cacheDirPath, "webpack.config.js");
|
||||
const webpackOutputDirPath = pathJoin(cacheDirPath, "webpack_output");
|
||||
const webpackOutputFilePath = pathJoin(webpackOutputDirPath, "index.js");
|
||||
|
||||
fs.writeFileSync(
|
||||
webpackConfigJsFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
``,
|
||||
`module.exports = {`,
|
||||
` mode: 'production',`,
|
||||
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` output: {`,
|
||||
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
||||
` libraryTarget: 'module',`,
|
||||
` },`,
|
||||
` target: "web",`,
|
||||
` module: {`,
|
||||
` rules: [`,
|
||||
` {`,
|
||||
` test: /\.js$/,`,
|
||||
` use: {`,
|
||||
` loader: 'babel-loader',`,
|
||||
` options: {`,
|
||||
` presets: ['@babel/preset-env'],`,
|
||||
` }`,
|
||||
` }`,
|
||||
` }`,
|
||||
` ]`,
|
||||
` },`,
|
||||
` experiments: {`,
|
||||
` outputModule: true`,
|
||||
` }`,
|
||||
`};`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
|
||||
cwd: pathDirname(webpackConfigJsFilePath)
|
||||
});
|
||||
|
||||
fs.readdirSync(webpackOutputDirPath)
|
||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||
.map(fileBasename => pathJoin(webpackOutputDirPath, fileBasename))
|
||||
.forEach(bundleFilePath => {
|
||||
assert(bundleFilePath.endsWith(".js"));
|
||||
|
||||
if (pathBasename(bundleFilePath) === "index.js") {
|
||||
fs.renameSync(webpackOutputFilePath, filePath);
|
||||
} else {
|
||||
const bundleFileBasename = pathBasename(bundleFilePath);
|
||||
|
||||
assert(!extraBundleFileBasenames.has(bundleFileBasename));
|
||||
extraBundleFileBasenames.add(bundleFileBasename);
|
||||
|
||||
fs.renameSync(
|
||||
bundleFilePath,
|
||||
pathJoin(pathDirname(filePath), bundleFileBasename)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
fs.rmSync(webpackOutputDirPath, { recursive: true });
|
||||
});
|
||||
}
|
45
scripts/dump-keycloak-realm.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import { CONTAINER_NAME } from "../src/bin/shared/constants";
|
||||
import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
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";
|
||||
|
||||
(async () => {
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
.split(":")[1]
|
||||
).major;
|
||||
|
||||
const parsedRealmJson = await dumpContainerConfig({
|
||||
buildContext: {
|
||||
cacheDirPath
|
||||
},
|
||||
keycloakMajorVersionNumber,
|
||||
realmName: "myrealm"
|
||||
});
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
"realmConfig",
|
||||
"defaultConfig",
|
||||
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||
);
|
||||
|
||||
await writeRealmJsonFile({
|
||||
parsedRealmJson,
|
||||
realmJsonFilePath,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
|
||||
})();
|
753
scripts/generate-i18n-messages.ts
Normal file
@ -0,0 +1,753 @@
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { same } from "evt/tools/inDepth";
|
||||
import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { deepAssign } from "../src/tools/deepAssign";
|
||||
import { THEME_TYPES } from "../src/bin/shared/constants";
|
||||
import { transformCodebase } from "../src/bin/tools/transformCodebase";
|
||||
import propertiesParser from "properties-parser";
|
||||
|
||||
if (require.main === module) {
|
||||
generateI18nMessages();
|
||||
}
|
||||
|
||||
async function generateI18nMessages() {
|
||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const accountI18nDirPath = pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"src",
|
||||
"account",
|
||||
"i18n"
|
||||
);
|
||||
|
||||
if (fs.existsSync(accountI18nDirPath)) {
|
||||
fs.rmSync(accountI18nDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
type Dictionary = { [idiomId: string]: string };
|
||||
|
||||
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||
|
||||
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(extractedDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
|
||||
crawl({
|
||||
dirPath: baseThemeDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
}).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, themeType_here, language] = match;
|
||||
|
||||
if (themeType_here !== themeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
|
||||
Object.fromEntries(
|
||||
Object.entries(
|
||||
propertiesParser.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||
.toString("utf8")
|
||||
) as Record<string, string>
|
||||
)
|
||||
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
||||
.map(([key, value]) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value
|
||||
])
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
key === "termsText" ? "" : value
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const recordForThemeType = record[themeType];
|
||||
|
||||
const languages = Object.keys(recordForThemeType);
|
||||
|
||||
const keycloakifyExtraMessages = (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return keycloakifyExtraMessages_login;
|
||||
case "account":
|
||||
return keycloakifyExtraMessages_account;
|
||||
}
|
||||
assert(false);
|
||||
})();
|
||||
|
||||
/* Migration helper
|
||||
|
||||
console.log({ themeType });
|
||||
|
||||
{
|
||||
|
||||
const all = new Set<string>();
|
||||
|
||||
languages.forEach(languages => all.add(languages));
|
||||
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
|
||||
currentlySupportedLanguages.forEach(languages => all.add(languages));
|
||||
|
||||
all.forEach(language => {
|
||||
console.log([
|
||||
`"${language}": `,
|
||||
`isInLanguages: ${languages.includes(language)}`,
|
||||
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
|
||||
].join(" "))
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
assert(
|
||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||
takeIntoAccountArraysOrdering: false
|
||||
})
|
||||
);
|
||||
|
||||
deepAssign({
|
||||
target: recordForThemeType,
|
||||
source: keycloakifyExtraMessages
|
||||
});
|
||||
|
||||
const messagesDirPath = pathJoin(
|
||||
thisCodebaseRootDirPath,
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"messages_defaultSet"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(messagesDirPath)) {
|
||||
fs.mkdirSync(messagesDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(messagesDirPath, "types.ts"),
|
||||
Buffer.from(
|
||||
[
|
||||
``,
|
||||
`export const languageTags = ${JSON.stringify(languages, null, 2)} as const;`,
|
||||
``,
|
||||
`export type LanguageTag = typeof languageTags[number];`,
|
||||
``,
|
||||
`export type MessageKey = keyof typeof import("./en")["default"];`,
|
||||
``
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
const generatedFileHeader = [
|
||||
`//This code was automatically generated by running ${pathRelative(
|
||||
thisCodebaseRootDirPath,
|
||||
__filename
|
||||
)}`,
|
||||
"//PLEASE DO NOT EDIT MANUALLY"
|
||||
].join("\n");
|
||||
|
||||
languages.forEach(language => {
|
||||
const filePath = pathJoin(messagesDirPath, `${language}.ts`);
|
||||
|
||||
fs.mkdirSync(pathDirname(filePath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
Buffer.from(
|
||||
[
|
||||
generatedFileHeader,
|
||||
"",
|
||||
"/* spell-checker: disable */",
|
||||
`const messages= ${JSON.stringify(
|
||||
recordForThemeType[language],
|
||||
null,
|
||||
2
|
||||
)};`,
|
||||
"",
|
||||
"export default messages;",
|
||||
"/* spell-checker: enable */"
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
//console.log(`${filePath} wrote`);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(messagesDirPath, "index.ts"),
|
||||
Buffer.from(
|
||||
[
|
||||
generatedFileHeader,
|
||||
`import * as en from "./en";`,
|
||||
"",
|
||||
"export async function fetchMessages_defaultSet(currentLanguageTag: string) {",
|
||||
" const { default: messages_defaultSet } = await (() => {",
|
||||
" switch (currentLanguageTag) {",
|
||||
` case "en": return en;`,
|
||||
...languages
|
||||
.filter(language => language !== "en")
|
||||
.map(
|
||||
language =>
|
||||
` case "${language}": return import("./${language}");`
|
||||
),
|
||||
' default: return { "default": {} };',
|
||||
" }",
|
||||
" })();",
|
||||
" return messages_defaultSet;",
|
||||
"}"
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(thisCodebaseRootDirPath, "src", "login", "i18n"),
|
||||
destDirPath: accountI18nDirPath,
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (fileRelativePath.startsWith("messages_defaultSet")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const keycloakifyExtraMessages_login: Record<
|
||||
| "en"
|
||||
| "ar"
|
||||
| "ca"
|
||||
| "cs"
|
||||
| "da"
|
||||
| "de"
|
||||
| "el"
|
||||
| "es"
|
||||
| "fa"
|
||||
| "fi"
|
||||
| "fr"
|
||||
| "hu"
|
||||
| "it"
|
||||
| "ja"
|
||||
| "lt"
|
||||
| "lv"
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
| "sv"
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "ka"
|
||||
| "zh-CN"
|
||||
| "zh-TW",
|
||||
Record<
|
||||
| "shouldBeEqual"
|
||||
| "shouldBeDifferent"
|
||||
| "shouldMatchPattern"
|
||||
| "mustBeAnInteger"
|
||||
| "notAValidOption"
|
||||
| "selectAnOption"
|
||||
| "remove"
|
||||
| "addValue"
|
||||
| "languages",
|
||||
string
|
||||
>
|
||||
> = {
|
||||
en: {
|
||||
shouldBeEqual: "{0} should be equal to {1}",
|
||||
shouldBeDifferent: "{0} should be different to {1}",
|
||||
shouldMatchPattern: "Pattern should match: `/{0}/`",
|
||||
mustBeAnInteger: "Must be an integer",
|
||||
notAValidOption: "Not a valid option",
|
||||
selectAnOption: "Select an option",
|
||||
remove: "Remove",
|
||||
addValue: "Add value",
|
||||
languages: "Languages"
|
||||
},
|
||||
/* spell-checker: disable */
|
||||
ar: {
|
||||
shouldBeEqual: "{0} يجب أن يكون مساويًا لـ {1}",
|
||||
shouldBeDifferent: "{0} يجب أن يكون مختلفًا عن {1}",
|
||||
shouldMatchPattern: "`/يجب أن يطابق النمط: `/{0}/",
|
||||
mustBeAnInteger: "يجب أن يكون عددًا صحيحًا",
|
||||
notAValidOption: "ليس خيارًا صالحًا",
|
||||
selectAnOption: "اختر خيارًا",
|
||||
remove: "إزالة",
|
||||
addValue: "أضف قيمة",
|
||||
languages: "اللغات"
|
||||
},
|
||||
ca: {
|
||||
shouldBeEqual: "{0} hauria de ser igual a {1}",
|
||||
shouldBeDifferent: "{0} hauria de ser diferent de {1}",
|
||||
shouldMatchPattern: "El patró hauria de coincidir: `/{0}/`",
|
||||
mustBeAnInteger: "Ha de ser un enter",
|
||||
notAValidOption: "No és una opció vàlida",
|
||||
selectAnOption: "Selecciona una opció",
|
||||
remove: "Elimina",
|
||||
addValue: "Afegeix valor",
|
||||
languages: "Idiomes"
|
||||
},
|
||||
cs: {
|
||||
shouldBeEqual: "{0} by měl být roven {1}",
|
||||
shouldBeDifferent: "{0} by měl být odlišný od {1}",
|
||||
shouldMatchPattern: "Vzor by měl odpovídat: `/{0}/`",
|
||||
mustBeAnInteger: "Musí být celé číslo",
|
||||
notAValidOption: "Není platná možnost",
|
||||
selectAnOption: "Vyberte možnost",
|
||||
remove: "Odstranit",
|
||||
addValue: "Přidat hodnotu",
|
||||
languages: "Jazyky"
|
||||
},
|
||||
da: {
|
||||
shouldBeEqual: "{0} bør være lig med {1}",
|
||||
shouldBeDifferent: "{0} bør være forskellig fra {1}",
|
||||
shouldMatchPattern: "Mønsteret bør matche: `/{0}/`",
|
||||
mustBeAnInteger: "Skal være et heltal",
|
||||
notAValidOption: "Ikke en gyldig mulighed",
|
||||
selectAnOption: "Vælg en mulighed",
|
||||
remove: "Fjern",
|
||||
addValue: "Tilføj værdi",
|
||||
languages: "Sprog"
|
||||
},
|
||||
de: {
|
||||
shouldBeEqual: "{0} sollte gleich {1} sein",
|
||||
shouldBeDifferent: "{0} sollte sich von {1} unterscheiden",
|
||||
shouldMatchPattern: "Muster sollte übereinstimmen: `/{0}/`",
|
||||
mustBeAnInteger: "Muss eine ganze Zahl sein",
|
||||
notAValidOption: "Keine gültige Option",
|
||||
selectAnOption: "Wählen Sie eine Option",
|
||||
remove: "Entfernen",
|
||||
addValue: "Wert hinzufügen",
|
||||
languages: "Sprachen"
|
||||
},
|
||||
el: {
|
||||
shouldBeEqual: "Το {0} πρέπει να είναι ίσο με {1}",
|
||||
shouldBeDifferent: "Το {0} πρέπει να διαφέρει από το {1}",
|
||||
shouldMatchPattern: "Το πρότυπο πρέπει να ταιριάζει: `/{0}/`",
|
||||
mustBeAnInteger: "Πρέπει να είναι ακέραιος",
|
||||
notAValidOption: "Δεν είναι μια έγκυρη επιλογή",
|
||||
selectAnOption: "Επιλέξτε μια επιλογή",
|
||||
remove: "Αφαίρεση",
|
||||
addValue: "Προσθήκη τιμής",
|
||||
languages: "Γλώσσες"
|
||||
},
|
||||
es: {
|
||||
shouldBeEqual: "{0} debería ser igual a {1}",
|
||||
shouldBeDifferent: "{0} debería ser diferente a {1}",
|
||||
shouldMatchPattern: "El patrón debería coincidir: `/{0}/`",
|
||||
mustBeAnInteger: "Debe ser un número entero",
|
||||
notAValidOption: "No es una opción válida",
|
||||
selectAnOption: "Selecciona una opción",
|
||||
remove: "Eliminar",
|
||||
addValue: "Añadir valor",
|
||||
languages: "Idiomas"
|
||||
},
|
||||
fa: {
|
||||
shouldBeEqual: "{0} باید برابر باشد با {1}",
|
||||
shouldBeDifferent: "{0} باید متفاوت باشد از {1}",
|
||||
shouldMatchPattern: "الگو باید مطابقت داشته باشد: `/{0}/`",
|
||||
mustBeAnInteger: "باید یک عدد صحیح باشد",
|
||||
notAValidOption: "یک گزینه معتبر نیست",
|
||||
selectAnOption: "یک گزینه انتخاب کنید",
|
||||
remove: "حذف",
|
||||
addValue: "افزودن مقدار",
|
||||
languages: "زبانها"
|
||||
},
|
||||
fi: {
|
||||
shouldBeEqual: "{0} pitäisi olla yhtä suuri kuin {1}",
|
||||
shouldBeDifferent: "{0} pitäisi olla erilainen kuin {1}",
|
||||
shouldMatchPattern: "Mallin tulisi vastata: `/{0}/`",
|
||||
mustBeAnInteger: "On oltava kokonaisluku",
|
||||
notAValidOption: "Ei ole kelvollinen vaihtoehto",
|
||||
selectAnOption: "Valitse vaihtoehto",
|
||||
remove: "Poista",
|
||||
addValue: "Lisää arvo",
|
||||
languages: "Kielet"
|
||||
},
|
||||
fr: {
|
||||
shouldBeEqual: "{0} devrait être égal à {1}",
|
||||
shouldBeDifferent: "{0} devrait être différent de {1}",
|
||||
shouldMatchPattern: "Le motif devrait correspondre: `/{0}/`",
|
||||
mustBeAnInteger: "Doit être un entier",
|
||||
notAValidOption: "Pas une option valide",
|
||||
selectAnOption: "Sélectionnez une option",
|
||||
remove: "Supprimer",
|
||||
addValue: "Ajouter une valeur",
|
||||
languages: "Langues"
|
||||
},
|
||||
hu: {
|
||||
shouldBeEqual: "{0} egyenlő kell legyen {1}-vel",
|
||||
shouldBeDifferent: "{0} különbözőnek kell lennie, mint {1}",
|
||||
shouldMatchPattern: "A mintának egyeznie kell: `/{0}/`",
|
||||
mustBeAnInteger: "Egész számnak kell lennie",
|
||||
notAValidOption: "Nem érvényes opció",
|
||||
selectAnOption: "Válasszon egy lehetőséget",
|
||||
remove: "Eltávolítás",
|
||||
addValue: "Érték hozzáadása",
|
||||
languages: "Nyelvek"
|
||||
},
|
||||
it: {
|
||||
shouldBeEqual: "{0} dovrebbe essere uguale a {1}",
|
||||
shouldBeDifferent: "{0} dovrebbe essere diverso da {1}",
|
||||
shouldMatchPattern: "Il modello dovrebbe corrispondere: `/{0}/`",
|
||||
mustBeAnInteger: "Deve essere un numero intero",
|
||||
notAValidOption: "Non è un'opzione valida",
|
||||
selectAnOption: "Seleziona un'opzione",
|
||||
remove: "Rimuovi",
|
||||
addValue: "Aggiungi valore",
|
||||
languages: "Lingue"
|
||||
},
|
||||
ja: {
|
||||
shouldBeEqual: "{0} は {1} と等しい必要があります",
|
||||
shouldBeDifferent: "{0} は {1} と異なる必要があります",
|
||||
shouldMatchPattern: "パターンは一致する必要があります: `/{0}/`",
|
||||
mustBeAnInteger: "整数である必要があります",
|
||||
notAValidOption: "有効なオプションではありません",
|
||||
selectAnOption: "オプションを選択",
|
||||
remove: "削除",
|
||||
addValue: "値を追加",
|
||||
languages: "言語"
|
||||
},
|
||||
lt: {
|
||||
shouldBeEqual: "{0} turėtų būti lygus {1}",
|
||||
shouldBeDifferent: "{0} turėtų skirtis nuo {1}",
|
||||
shouldMatchPattern: "Šablonas turėtų atitikti: `/{0}/`",
|
||||
mustBeAnInteger: "Turi būti sveikasis skaičius",
|
||||
notAValidOption: "Netinkama parinktis",
|
||||
selectAnOption: "Pasirinkite parinktį",
|
||||
remove: "Pašalinti",
|
||||
addValue: "Pridėti reikšmę",
|
||||
languages: "Kalbos"
|
||||
},
|
||||
lv: {
|
||||
shouldBeEqual: "{0} jābūt vienādam ar {1}",
|
||||
shouldBeDifferent: "{0} jābūt atšķirīgam no {1}",
|
||||
shouldMatchPattern: "Mustrim jāsakrīt: `/{0}/`",
|
||||
mustBeAnInteger: "Jābūt veselam skaitlim",
|
||||
notAValidOption: "Nav derīga opcija",
|
||||
selectAnOption: "Izvēlieties opciju",
|
||||
remove: "Noņemt",
|
||||
addValue: "Pievienot vērtību",
|
||||
languages: "Valodas"
|
||||
},
|
||||
nl: {
|
||||
shouldBeEqual: "{0} moet gelijk zijn aan {1}",
|
||||
shouldBeDifferent: "{0} moet verschillen van {1}",
|
||||
shouldMatchPattern: "Patroon moet overeenkomen: `/{0}/`",
|
||||
mustBeAnInteger: "Moet een geheel getal zijn",
|
||||
notAValidOption: "Geen geldige optie",
|
||||
selectAnOption: "Selecteer een optie",
|
||||
remove: "Verwijderen",
|
||||
addValue: "Waarde toevoegen",
|
||||
languages: "Talen"
|
||||
},
|
||||
no: {
|
||||
shouldBeEqual: "{0} skal være lik {1}",
|
||||
shouldBeDifferent: "{0} skal være forskjellig fra {1}",
|
||||
shouldMatchPattern: "Mønsteret skal matche: `/{0}/`",
|
||||
mustBeAnInteger: "Må være et heltall",
|
||||
notAValidOption: "Ikke et gyldig alternativ",
|
||||
selectAnOption: "Velg et alternativ",
|
||||
remove: "Fjern",
|
||||
addValue: "Legg til verdi",
|
||||
languages: "Språk"
|
||||
},
|
||||
pl: {
|
||||
shouldBeEqual: "{0} powinno być równe {1}",
|
||||
shouldBeDifferent: "{0} powinno być różne od {1}",
|
||||
shouldMatchPattern: "Wzór pow inien pasować: `/{0}/`",
|
||||
mustBeAnInteger: "Musi być liczbą całkowitą",
|
||||
notAValidOption: "Nieprawidłowa opcja",
|
||||
selectAnOption: "Wybierz opcję",
|
||||
remove: "Usuń",
|
||||
addValue: "Dodaj wartość",
|
||||
languages: "Języki"
|
||||
},
|
||||
pt: {
|
||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||
mustBeAnInteger: "Deve ser um número inteiro",
|
||||
notAValidOption: "Não é uma opção válida",
|
||||
selectAnOption: "Selecione uma opção",
|
||||
remove: "Remover",
|
||||
addValue: "Adicionar valor",
|
||||
languages: "Idiomas"
|
||||
},
|
||||
"pt-BR": {
|
||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||
mustBeAnInteger: "Deve ser um número inteiro",
|
||||
notAValidOption: "Não é uma opção válida",
|
||||
selectAnOption: "Selecione uma opção",
|
||||
remove: "Remover",
|
||||
addValue: "Adicionar valor",
|
||||
languages: "Idiomas"
|
||||
},
|
||||
ru: {
|
||||
shouldBeEqual: "{0} должно быть равно {1}",
|
||||
shouldBeDifferent: "{0} должно отличаться от {1}",
|
||||
shouldMatchPattern: "Шаблон должен соответствовать: `/{0}/`",
|
||||
mustBeAnInteger: "Должно быть целым числом",
|
||||
notAValidOption: "Недопустимый вариант",
|
||||
selectAnOption: "Выберите вариант",
|
||||
remove: "Удалить",
|
||||
addValue: "Добавить значение",
|
||||
languages: "Языки"
|
||||
},
|
||||
sk: {
|
||||
shouldBeEqual: "{0} by mal byť rovnaký ako {1}",
|
||||
shouldBeDifferent: "{0} by mal byť odlišný od {1}",
|
||||
shouldMatchPattern: "Vzor by mal zodpovedať: `/{0}/`",
|
||||
mustBeAnInteger: "Musí byť celé číslo",
|
||||
notAValidOption: "Nie je platná možnosť",
|
||||
selectAnOption: "Vyberte možnosť",
|
||||
remove: "Odstrániť",
|
||||
addValue: "Pridať hodnotu",
|
||||
languages: "Jazyky"
|
||||
},
|
||||
sv: {
|
||||
shouldBeEqual: "{0} bör vara lika med {1}",
|
||||
shouldBeDifferent: "{0} bör vara annorlunda än {1}",
|
||||
shouldMatchPattern: "Mönstret bör matcha: `/{0}/`",
|
||||
mustBeAnInteger: "Måste vara ett heltal",
|
||||
notAValidOption: "Inte ett giltigt alternativ",
|
||||
selectAnOption: "Välj ett alternativ",
|
||||
remove: "Ta bort",
|
||||
addValue: "Lägg till värde",
|
||||
languages: "Språk"
|
||||
},
|
||||
th: {
|
||||
shouldBeEqual: "{0} ควรเท่ากับ {1}",
|
||||
shouldBeDifferent: "{0} ควรแตกต่างจาก {1}",
|
||||
shouldMatchPattern: "รูปแบบควรตรงกับ: `/{0}/`",
|
||||
mustBeAnInteger: "ต้องเป็นจำนวนเต็ม",
|
||||
notAValidOption: "ไม่ใช่ตัวเลือกที่ถูกต้อง",
|
||||
selectAnOption: "เลือกตัวเลือก",
|
||||
remove: "ลบ",
|
||||
addValue: "เพิ่มค่า",
|
||||
languages: "ภาษา"
|
||||
},
|
||||
tr: {
|
||||
shouldBeEqual: "{0} {1} eşit olmalıdır",
|
||||
shouldBeDifferent: "{0} {1} farklı olmalıdır",
|
||||
shouldMatchPattern: "Desen eşleşmelidir: `/{0}/`",
|
||||
mustBeAnInteger: "Tam sayı olmalıdır",
|
||||
notAValidOption: "Geçerli bir seçenek değil",
|
||||
selectAnOption: "Bir seçenek seçin",
|
||||
remove: "Kaldır",
|
||||
addValue: "Değer ekle",
|
||||
languages: "Diller"
|
||||
},
|
||||
uk: {
|
||||
shouldBeEqual: "{0} повинно бути рівним {1}",
|
||||
shouldBeDifferent: "{0} повинно відрізнятися від {1}",
|
||||
shouldMatchPattern: "Шаблон повинен відповідати: `/{0}/`",
|
||||
mustBeAnInteger: "Повинно бути цілим числом",
|
||||
notAValidOption: "Не є дійсною опцією",
|
||||
selectAnOption: "Виберіть опцію",
|
||||
remove: "Видалити",
|
||||
addValue: "Додати значення",
|
||||
languages: "Мови"
|
||||
},
|
||||
ka: {
|
||||
shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის",
|
||||
shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის",
|
||||
shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`",
|
||||
mustBeAnInteger: "უნდა იყოს მთელი რიცხვი",
|
||||
notAValidOption: "არასწორი ვარიანტი",
|
||||
selectAnOption: "აირჩიეთ ვარიანტი",
|
||||
remove: "წაშალეთ",
|
||||
addValue: "დაამატეთ მნიშვნელობა",
|
||||
languages: "ენები"
|
||||
},
|
||||
"zh-CN": {
|
||||
shouldBeEqual: "{0} 应该等于 {1}",
|
||||
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||
shouldMatchPattern: "模式应匹配: `/{0}/`",
|
||||
mustBeAnInteger: "必须是整数",
|
||||
notAValidOption: "不是有效选项",
|
||||
selectAnOption: "选择一个选项",
|
||||
remove: "移除",
|
||||
addValue: "添加值",
|
||||
languages: "语言"
|
||||
},
|
||||
"zh-TW": {
|
||||
shouldBeEqual: "{0} 應該等於 {1}",
|
||||
shouldBeDifferent: "{0} 應該不同於 {1}",
|
||||
shouldMatchPattern: "模式應匹配: `/{0}/`",
|
||||
mustBeAnInteger: "必須是整數",
|
||||
notAValidOption: "不是有效選項",
|
||||
selectAnOption: "選擇一個選項",
|
||||
remove: "移除",
|
||||
addValue: "添加值",
|
||||
languages: "語言"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
||||
|
||||
export const accountMultiPageSupportedLanguages = [
|
||||
"en",
|
||||
"ar",
|
||||
"ca",
|
||||
"cs",
|
||||
"da",
|
||||
"de",
|
||||
"es",
|
||||
"fi",
|
||||
"fr",
|
||||
"hu",
|
||||
"it",
|
||||
"ja",
|
||||
"lt",
|
||||
"lv",
|
||||
"nl",
|
||||
"no",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"sk",
|
||||
"sv",
|
||||
"tr",
|
||||
"zh-CN"
|
||||
] as const;
|
||||
|
||||
const keycloakifyExtraMessages_account: Record<
|
||||
(typeof accountMultiPageSupportedLanguages)[number],
|
||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||
> = {
|
||||
en: {
|
||||
newPasswordSameAsOld: "New password must be different from the old one",
|
||||
passwordConfirmNotMatch: "Password confirmation does not match"
|
||||
},
|
||||
/* spell-checker: disable */
|
||||
ar: {
|
||||
newPasswordSameAsOld: "يجب أن تكون كلمة المرور الجديدة مختلفة عن القديمة",
|
||||
passwordConfirmNotMatch: "تأكيد كلمة المرور لا يتطابق"
|
||||
},
|
||||
ca: {
|
||||
newPasswordSameAsOld: "La nova contrasenya ha de ser diferent de l'anterior",
|
||||
passwordConfirmNotMatch: "La confirmació de la contrasenya no coincideix"
|
||||
},
|
||||
cs: {
|
||||
newPasswordSameAsOld: "Nové heslo musí být odlišné od starého",
|
||||
passwordConfirmNotMatch: "Potvrzení hesla se neshoduje"
|
||||
},
|
||||
da: {
|
||||
newPasswordSameAsOld: "Det nye kodeord skal være forskelligt fra det gamle",
|
||||
passwordConfirmNotMatch: "Adgangskodebekræftelse matcher ikke"
|
||||
},
|
||||
de: {
|
||||
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
||||
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
||||
},
|
||||
es: {
|
||||
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
||||
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
||||
},
|
||||
fi: {
|
||||
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
||||
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
||||
},
|
||||
fr: {
|
||||
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
|
||||
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
|
||||
},
|
||||
hu: {
|
||||
newPasswordSameAsOld: "Az új jelszónak különböznie kell az előzőtől",
|
||||
passwordConfirmNotMatch: "A jelszó megerősítése nem egyezik"
|
||||
},
|
||||
it: {
|
||||
newPasswordSameAsOld:
|
||||
"La nuova password deve essere diversa da quella precedente",
|
||||
passwordConfirmNotMatch: "La conferma della password non corrisponde"
|
||||
},
|
||||
ja: {
|
||||
newPasswordSameAsOld: "新しいパスワードは古いパスワードと異なる必要があります",
|
||||
passwordConfirmNotMatch: "パスワード確認が一致しません"
|
||||
},
|
||||
lt: {
|
||||
newPasswordSameAsOld: "Naujas slaptažodis turi skirtis nuo seno",
|
||||
passwordConfirmNotMatch: "Slaptažodžio patvirtinimas neatitinka"
|
||||
},
|
||||
lv: {
|
||||
newPasswordSameAsOld: "Jaunajam parolam jābūt atšķirīgam no vecā",
|
||||
passwordConfirmNotMatch: "Paroles apstiprināšana neatbilst"
|
||||
},
|
||||
nl: {
|
||||
newPasswordSameAsOld: "Het nieuwe wachtwoord moet verschillend zijn van het oude",
|
||||
passwordConfirmNotMatch: "Wachtwoordbevestiging komt niet overeen"
|
||||
},
|
||||
no: {
|
||||
newPasswordSameAsOld: "Det nye passordet må være forskjellig fra det gamle",
|
||||
passwordConfirmNotMatch: "Passordbekreftelsen stemmer ikke"
|
||||
},
|
||||
pl: {
|
||||
newPasswordSameAsOld: "Nowe hasło musi być inne niż stare",
|
||||
passwordConfirmNotMatch: "Potwierdzenie hasła nie pasuje"
|
||||
},
|
||||
"pt-BR": {
|
||||
newPasswordSameAsOld: "A nova senha deve ser diferente da antiga",
|
||||
passwordConfirmNotMatch: "A confirmação da senha não corresponde"
|
||||
},
|
||||
ru: {
|
||||
newPasswordSameAsOld: "Новый пароль должен отличаться от старого",
|
||||
passwordConfirmNotMatch: "Подтверждение пароля не совпадает"
|
||||
},
|
||||
sk: {
|
||||
newPasswordSameAsOld: "Nové heslo musí byť odlišné od starého",
|
||||
passwordConfirmNotMatch: "Potvrdenie hesla sa nezhoduje"
|
||||
},
|
||||
sv: {
|
||||
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
||||
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
||||
},
|
||||
tr: {
|
||||
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
||||
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
||||
},
|
||||
"zh-CN": {
|
||||
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
||||
passwordConfirmNotMatch: "密码确认不匹配"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
241
scripts/link-in-app.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import { execSync } from "child_process";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
|
||||
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||
|
||||
// For example [ "@emotion" ] it's more convenient than
|
||||
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||
// in singletonDependencies
|
||||
const namespaceSingletonDependencies: string[] = [];
|
||||
|
||||
const rootDirPath = getThisCodebaseRootDirPath();
|
||||
|
||||
const commonThirdPartyDeps = [
|
||||
...namespaceSingletonDependencies
|
||||
.map(namespaceModuleName =>
|
||||
fs
|
||||
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||
)
|
||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||
...singletonDependencies
|
||||
];
|
||||
|
||||
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||
{
|
||||
let modifiedPackageJsonContent = fs
|
||||
.readFileSync(pathJoin(rootDirPath, "package.json"))
|
||||
.toString("utf8");
|
||||
|
||||
modifiedPackageJsonContent = (() => {
|
||||
const o = JSON.parse(modifiedPackageJsonContent);
|
||||
|
||||
delete o.files;
|
||||
|
||||
return JSON.stringify(o, null, 2);
|
||||
})();
|
||||
|
||||
modifiedPackageJsonContent = modifiedPackageJsonContent
|
||||
.replace(/"dist\//g, '"')
|
||||
.replace(/"\.\/dist\//g, '"./')
|
||||
.replace(/"!dist\//g, '"!')
|
||||
.replace(/"!\.\/dist\//g, '"!./');
|
||||
|
||||
modifiedPackageJsonContent = JSON.stringify(
|
||||
{
|
||||
...JSON.parse(modifiedPackageJsonContent),
|
||||
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
|
||||
},
|
||||
null,
|
||||
4
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(rootDirPath, "dist", "package.json"),
|
||||
Buffer.from(modifiedPackageJsonContent, "utf8")
|
||||
);
|
||||
}
|
||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||
|
||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||
fs.mkdirSync(yarnGlobalDirPath);
|
||||
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
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 = [
|
||||
"yarn",
|
||||
"link",
|
||||
...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])
|
||||
].join(" ");
|
||||
|
||||
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||
|
||||
execSync(cmd, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...(os.platform() === "win32"
|
||||
? {
|
||||
USERPROFILE: yarnGlobalDirPath,
|
||||
LOCALAPPDATA: yarnGlobalDirPath
|
||||
}
|
||||
: { HOME: yarnGlobalDirPath })
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const testAppPaths = (() => {
|
||||
const [, , ...testAppNames] = process.argv;
|
||||
|
||||
return testAppNames
|
||||
.map(testAppName => {
|
||||
const testAppPath = pathJoin(rootDirPath, "..", testAppName);
|
||||
|
||||
if (fs.existsSync(testAppPath)) {
|
||||
return testAppPath;
|
||||
}
|
||||
|
||||
console.warn(
|
||||
`Skipping ${testAppName} since it cant be found here: ${testAppPath}`
|
||||
);
|
||||
|
||||
return undefined;
|
||||
})
|
||||
.filter((path): path is string => path !== undefined);
|
||||
})();
|
||||
|
||||
if (testAppPaths.length === 0) {
|
||||
console.error("No test app to link into!");
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
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 ===");
|
||||
|
||||
const total = commonThirdPartyDeps.length;
|
||||
let current = 0;
|
||||
|
||||
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||
current++;
|
||||
|
||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||
|
||||
const localInstallPath = pathJoin(
|
||||
...[
|
||||
rootDirPath,
|
||||
"node_modules",
|
||||
...(commonThirdPartyDep.startsWith("@")
|
||||
? commonThirdPartyDep.split("/")
|
||||
: [commonThirdPartyDep])
|
||||
]
|
||||
);
|
||||
|
||||
execYarnLink({ cwd: localInstallPath });
|
||||
});
|
||||
|
||||
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
cwd: testAppPath,
|
||||
targetModuleName: commonThirdPartyDep
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log("=== Linking in house dependencies ===");
|
||||
|
||||
execYarnLink({ cwd: pathJoin(rootDirPath, "dist") });
|
||||
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
cwd: testAppPath,
|
||||
targetModuleName: JSON.parse(
|
||||
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
|
||||
)["name"]
|
||||
})
|
||||
);
|
||||
|
||||
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 {};
|
88
scripts/link-in-starter.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
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 parentDirPath = pathJoin(getThisCodebaseRootDirPath(), "..");
|
||||
|
||||
const { starterName } = await (async () => {
|
||||
const starterNames = fs
|
||||
.readdirSync(parentDirPath)
|
||||
.filter(
|
||||
basename =>
|
||||
basename.includes("starter") &&
|
||||
basename.includes("keycloakify") &&
|
||||
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
|
||||
);
|
||||
|
||||
if (starterNames.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const starterName = await (async () => {
|
||||
if (starterNames.length === 1) {
|
||||
return starterNames[0];
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\nSelect a starter to link in:`));
|
||||
|
||||
const { value } = await cliSelect<string>({
|
||||
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
return value.split(pathSep)[1];
|
||||
})();
|
||||
|
||||
return { starterName };
|
||||
})();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(chalk.cyan(`\n\nLinking in ..${pathSep}${starterName}...`));
|
||||
|
||||
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`));
|
||||
})();
|
9
scripts/shared/cacheDirPath.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export const cacheDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"node_modules",
|
||||
".cache",
|
||||
"scripts"
|
||||
);
|
357
scripts/shared/downloadKeycloakDefaultTheme.ts
Normal file
@ -0,0 +1,357 @@
|
||||
import { relative as pathRelative } from "path";
|
||||
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
|
||||
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { cacheDirPath } from "./cacheDirPath";
|
||||
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
|
||||
const KEYCLOAK_VERSION = {
|
||||
FOR_LOGIN_THEME: "25.0.4",
|
||||
FOR_ACCOUNT_MULTI_PAGE: "21.1.2"
|
||||
} as const;
|
||||
|
||||
export async function downloadKeycloakDefaultTheme(params: {
|
||||
keycloakVersionId: keyof typeof KEYCLOAK_VERSION;
|
||||
}) {
|
||||
const { keycloakVersionId } = params;
|
||||
|
||||
const keycloakVersion = KEYCLOAK_VERSION[keycloakVersionId];
|
||||
|
||||
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
||||
}),
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles",
|
||||
onArchiveFile: async params => {
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
if (fileRelativePath.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { readFile, writeFile } = params;
|
||||
|
||||
if (
|
||||
!fileRelativePath.startsWith("base") &&
|
||||
!fileRelativePath.startsWith("keycloak")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (keycloakVersion) {
|
||||
case KEYCLOAK_VERSION.FOR_LOGIN_THEME:
|
||||
if (
|
||||
!fileRelativePath.startsWith(pathJoin("base", "login")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "login")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".ftl")) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE:
|
||||
if (
|
||||
!fileRelativePath.startsWith(pathJoin("base", "account")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "account")) &&
|
||||
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
assert<Equals<typeof keycloakVersion, never>>(false);
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
skip_web_modules: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "web_modules")
|
||||
)
|
||||
) {
|
||||
break skip_web_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_lib: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "lib")
|
||||
)
|
||||
) {
|
||||
break skip_lib;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_node_modules: {
|
||||
const nodeModulesRelativeDirPath = pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules"
|
||||
);
|
||||
|
||||
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
|
||||
kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([
|
||||
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"css",
|
||||
"patternfly-additions.min.css"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Regular-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Light-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.woff"
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
|
||||
nodeModulesRelativeDirPath.length + 1
|
||||
);
|
||||
|
||||
if (
|
||||
kcNodeModulesKeepFilePaths_lastAccountV1.has(
|
||||
fileRelativeToNodeModulesPath
|
||||
)
|
||||
) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
patch_account_css: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "account", "resources", "css", "account.css")
|
||||
) {
|
||||
break patch_account_css;
|
||||
}
|
||||
|
||||
await writeFile({
|
||||
fileRelativePath,
|
||||
modifiedData: Buffer.from(
|
||||
(await readFile())
|
||||
.toString("utf8")
|
||||
.replace("top: -34px;", "top: -34px !important;"),
|
||||
"utf8"
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
skip_unused_resources: {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_LOGIN_THEME) {
|
||||
break skip_unused_resources;
|
||||
}
|
||||
|
||||
skip_node_modules: {
|
||||
const nodeModulesRelativeDirPath = pathJoin(
|
||||
"keycloak",
|
||||
"common",
|
||||
"resources",
|
||||
"node_modules"
|
||||
);
|
||||
|
||||
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
if (kcNodeModulesKeepFilePaths === undefined) {
|
||||
kcNodeModulesKeepFilePaths = new Set([
|
||||
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
|
||||
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"css",
|
||||
"patternfly-additions.min.css"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Regular-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Light-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-Bold-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"fontawesome-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.ttf"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"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("jquery", "dist", "jquery.min.js"),
|
||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||
]);
|
||||
}
|
||||
|
||||
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
|
||||
nodeModulesRelativeDirPath.length + 1
|
||||
);
|
||||
|
||||
if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) {
|
||||
break skip_node_modules;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_vendor: {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("keycloak", "common", "resources", "vendor")
|
||||
)
|
||||
) {
|
||||
break skip_vendor;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_rollup_config: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "common", "resources", "rollup.config.js")
|
||||
) {
|
||||
break skip_rollup_config;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
skip_package_json: {
|
||||
if (
|
||||
fileRelativePath !==
|
||||
pathJoin("keycloak", "common", "resources", "package.json")
|
||||
) {
|
||||
break skip_package_json;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await writeFile({ fileRelativePath });
|
||||
}
|
||||
});
|
||||
|
||||
return { extractedDirPath };
|
||||
}
|
8
scripts/shared/run.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function run(command: string, options?: { cwd: string }) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
||||
child_process.execSync(command, { stdio: "inherit", ...options });
|
||||
}
|
40
scripts/shared/startRebuildOnSrcChange.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import * as child_process from "child_process";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import chokidar from "chokidar";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function startRebuildOnSrcChange() {
|
||||
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
|
||||
|
||||
const runYarnBuild = runExclusive.build(async () => {
|
||||
console.log(chalk.green("Running `yarn build`"));
|
||||
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn("yarn", ["build"], { shell: true });
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", () => dCompleted.resolve());
|
||||
|
||||
await dCompleted.pr;
|
||||
|
||||
console.log("\n\n");
|
||||
});
|
||||
|
||||
console.log(chalk.green("Watching for changes in src/"));
|
||||
|
||||
chokidar
|
||||
.watch(["src", "stories"], { ignoreInitial: true })
|
||||
.on("all", async (event, path) => {
|
||||
console.log(chalk.bold(`${event}: ${path}`));
|
||||
|
||||
await waitForDebounce();
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
}
|
21
scripts/start-storybook.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import * as child_process from "child_process";
|
||||
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
|
||||
import { run } from "./shared/run";
|
||||
|
||||
(async () => {
|
||||
run("yarn build");
|
||||
|
||||
{
|
||||
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", process.exit.bind(process));
|
||||
}
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
})();
|
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
23
src/PUBLIC_URL.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
/**
|
||||
* This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects.
|
||||
* This works both in your main app and in your Keycloak theme.
|
||||
*/
|
||||
export const PUBLIC_URL = (() => {
|
||||
const kcContext: { "x-keycloakify": { resourcesPath: string } } | undefined = (
|
||||
window as any
|
||||
).kcContext;
|
||||
|
||||
if (kcContext === undefined || process.env.NODE_ENV === "development") {
|
||||
assert(
|
||||
process.env.PUBLIC_URL !== undefined,
|
||||
`If you use keycloakify/PUBLIC_URL you should be in Webpack and thus process.env.PUBLIC_URL should be defined`
|
||||
);
|
||||
|
||||
return process.env.PUBLIC_URL;
|
||||
}
|
||||
|
||||
return `${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}`;
|
||||
})();
|
41
src/account/DefaultPage.tsx
Normal file
@ -0,0 +1,41 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "keycloakify/account/KcContext";
|
||||
import { I18n } from "keycloakify/account/i18n";
|
||||
|
||||
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||
const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
|
||||
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
|
||||
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
|
||||
const Log = lazy(() => import("keycloakify/account/pages/Log"));
|
||||
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
|
||||
|
||||
export default function DefaultPage(props: PageProps<KcContext, I18n>) {
|
||||
const { kcContext, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "password.ftl":
|
||||
return <Password kcContext={kcContext} {...rest} />;
|
||||
case "sessions.ftl":
|
||||
return <Sessions kcContext={kcContext} {...rest} />;
|
||||
case "account.ftl":
|
||||
return <Account kcContext={kcContext} {...rest} />;
|
||||
case "totp.ftl":
|
||||
return <Totp kcContext={kcContext} {...rest} />;
|
||||
case "applications.ftl":
|
||||
return <Applications kcContext={kcContext} {...rest} />;
|
||||
case "log.ftl":
|
||||
return <Log kcContext={kcContext} {...rest} />;
|
||||
case "federatedIdentity.ftl":
|
||||
return <FederatedIdentity kcContext={kcContext} {...rest} />;
|
||||
}
|
||||
assert<Equals<typeof kcContext, never>>(false);
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
310
src/account/KcContext/KcContext.ts
Normal file
@ -0,0 +1,310 @@
|
||||
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { ValueOf } from "keycloakify/tools/ValueOf";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
|
||||
export type ExtendKcContext<
|
||||
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtensionPerPage extends Record<string, Record<string, unknown>>
|
||||
> = ValueOf<{
|
||||
[PageId in keyof KcContextExtensionPerPage | KcContext["pageId"]]: Extract<
|
||||
KcContext,
|
||||
{ pageId: PageId }
|
||||
> extends never
|
||||
? KcContext.Common &
|
||||
KcContextExtension & {
|
||||
pageId: PageId;
|
||||
} & KcContextExtensionPerPage[PageId]
|
||||
: Extract<KcContext, { pageId: PageId }> &
|
||||
KcContextExtension &
|
||||
KcContextExtensionPerPage[PageId];
|
||||
}>;
|
||||
|
||||
export type KcContext =
|
||||
| KcContext.Password
|
||||
| KcContext.Account
|
||||
| KcContext.Sessions
|
||||
| KcContext.Totp
|
||||
| KcContext.Applications
|
||||
| KcContext.Log
|
||||
| KcContext.FederatedIdentity;
|
||||
|
||||
export declare namespace KcContext {
|
||||
export type Common = {
|
||||
themeVersion: string;
|
||||
keycloakifyVersion: string;
|
||||
themeType: "account";
|
||||
themeName: string;
|
||||
locale?: {
|
||||
supported: {
|
||||
url: string;
|
||||
label: string;
|
||||
languageTag: string;
|
||||
}[];
|
||||
currentLanguageTag: string;
|
||||
};
|
||||
url: {
|
||||
accountUrl: string;
|
||||
passwordUrl: string;
|
||||
totpUrl: string;
|
||||
socialUrl: string;
|
||||
sessionsUrl: string;
|
||||
applicationsUrl: string;
|
||||
logUrl: string;
|
||||
logoutUrl: string;
|
||||
resourceUrl: string;
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
|
||||
referrerURI?: string;
|
||||
getLogoutUrl: () => string;
|
||||
};
|
||||
features: {
|
||||
passwordUpdateSupported: boolean;
|
||||
identityFederation: boolean;
|
||||
log: boolean;
|
||||
authorization: boolean;
|
||||
};
|
||||
realm: {
|
||||
internationalizationEnabled: boolean;
|
||||
userManagedAccessAllowed: boolean;
|
||||
};
|
||||
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
|
||||
message?: {
|
||||
type: "success" | "warning" | "error" | "info";
|
||||
summary: string;
|
||||
};
|
||||
referrer?: {
|
||||
url: string; // The url of the App
|
||||
name: string; // Client id
|
||||
};
|
||||
messagesPerField: {
|
||||
/**
|
||||
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
|
||||
*
|
||||
* @param fieldName to check for
|
||||
* @param text to return
|
||||
* @return text if message exists for given field, else undefined
|
||||
*/
|
||||
printIfExists: <T extends string>(
|
||||
fieldName: string,
|
||||
text: T
|
||||
) => T | undefined;
|
||||
/**
|
||||
* Check if exists error message for given fields
|
||||
*
|
||||
* @param fields
|
||||
* @return boolean
|
||||
*/
|
||||
existsError: (fieldName: string) => boolean;
|
||||
/**
|
||||
* Get message for given field.
|
||||
*
|
||||
* @param fieldName
|
||||
* @return message text or empty string
|
||||
*/
|
||||
get: (fieldName: string) => string;
|
||||
/**
|
||||
* Check if message for given field exists
|
||||
*
|
||||
* @param field
|
||||
* @return boolean
|
||||
*/
|
||||
exists: (fieldName: string) => boolean;
|
||||
};
|
||||
account: {
|
||||
email?: string;
|
||||
firstName: string;
|
||||
lastName?: string;
|
||||
username?: string;
|
||||
};
|
||||
properties: {};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
export type Password = Common & {
|
||||
pageId: "password.ftl";
|
||||
password: {
|
||||
passwordSet: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Account = Common & {
|
||||
pageId: "account.ftl";
|
||||
url: {
|
||||
accountUrl: string;
|
||||
};
|
||||
realm: {
|
||||
registrationEmailAsUsername: boolean;
|
||||
editUsernameAllowed: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Sessions = Common & {
|
||||
pageId: "sessions.ftl";
|
||||
sessions: {
|
||||
sessions: {
|
||||
expires: string;
|
||||
clients: string[];
|
||||
ipAddress: string;
|
||||
started: string;
|
||||
lastAccess: string;
|
||||
id: string;
|
||||
}[];
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Totp = Common & {
|
||||
pageId: "totp.ftl";
|
||||
totp: {
|
||||
enabled: boolean;
|
||||
totpSecretEncoded: string;
|
||||
qrUrl: string;
|
||||
policy: {
|
||||
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
|
||||
digits: number;
|
||||
lookAheadWindow: number;
|
||||
getAlgorithmKey: () => string;
|
||||
} & (
|
||||
| {
|
||||
type: "totp";
|
||||
period: number;
|
||||
}
|
||||
| {
|
||||
type: "hotp";
|
||||
initialCounter: number;
|
||||
}
|
||||
);
|
||||
supportedApplications: string[];
|
||||
totpSecretQrCode: string;
|
||||
manualUrl: string;
|
||||
totpSecret: string;
|
||||
otpCredentials: { id: string; userLabel: string }[];
|
||||
};
|
||||
mode?: "qr" | "manual" | undefined | null;
|
||||
isAppInitiatedAction: boolean;
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Applications = Common & {
|
||||
pageId: "applications.ftl";
|
||||
features: {
|
||||
log: boolean;
|
||||
identityFederation: boolean;
|
||||
authorization: boolean;
|
||||
passwordUpdateSupported: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
applications: {
|
||||
applications: {
|
||||
realmRolesAvailable: {
|
||||
name: string;
|
||||
description: string;
|
||||
compositesStream?: Record<string, unknown>;
|
||||
clientRole?: boolean;
|
||||
composite?: boolean;
|
||||
id?: string;
|
||||
containerId?: string;
|
||||
attributes?: Record<string, unknown>;
|
||||
}[];
|
||||
resourceRolesAvailable: Record<
|
||||
string,
|
||||
{
|
||||
roleName: string;
|
||||
roleDescription?: string;
|
||||
clientName: string;
|
||||
clientId: string;
|
||||
}[]
|
||||
>;
|
||||
additionalGrants: string[];
|
||||
clientScopesGranted: string[];
|
||||
effectiveUrl?: string;
|
||||
client: {
|
||||
alwaysDisplayInConsole: boolean;
|
||||
attributes: Record<string, unknown>;
|
||||
authenticationFlowBindingOverrides: Record<string, unknown>;
|
||||
baseUrl?: string;
|
||||
bearerOnly: boolean;
|
||||
clientAuthenticatorType: string;
|
||||
clientId: string;
|
||||
consentRequired: boolean;
|
||||
consentScreenText: string;
|
||||
description: string;
|
||||
directAccessGrantsEnabled: boolean;
|
||||
displayOnConsentScreen: boolean;
|
||||
dynamicScope: boolean;
|
||||
enabled: boolean;
|
||||
frontchannelLogout: boolean;
|
||||
fullScopeAllowed: boolean;
|
||||
id: string;
|
||||
implicitFlowEnabled: boolean;
|
||||
includeInTokenScope: boolean;
|
||||
managementUrl: string;
|
||||
name?: string;
|
||||
nodeReRegistrationTimeout: string;
|
||||
notBefore: string;
|
||||
protocol: string;
|
||||
protocolMappersStream: Record<string, unknown>;
|
||||
publicClient: boolean;
|
||||
realm: Record<string, unknown>;
|
||||
realmScopeMappingsStream: Record<string, unknown>;
|
||||
redirectUris: string[];
|
||||
registeredNodes: Record<string, unknown>;
|
||||
rolesStream: Record<string, unknown>;
|
||||
rootUrl?: string;
|
||||
scopeMappingsStream: Record<string, unknown>;
|
||||
secret: string;
|
||||
serviceAccountsEnabled: boolean;
|
||||
standardFlowEnabled: boolean;
|
||||
surrogateAuthRequired: boolean;
|
||||
webOrigins: string[];
|
||||
};
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type Log = Common & {
|
||||
pageId: "log.ftl";
|
||||
log: {
|
||||
events: {
|
||||
date: string | number | Date;
|
||||
event: string;
|
||||
ipAddress: string;
|
||||
client: string;
|
||||
details: { value: string; key: string }[];
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type FederatedIdentity = Common & {
|
||||
pageId: "federatedIdentity.ftl";
|
||||
stateChecker: string;
|
||||
federatedIdentity: {
|
||||
identities: {
|
||||
providerId: string;
|
||||
displayName: string;
|
||||
userName: string;
|
||||
connected: boolean;
|
||||
}[];
|
||||
removeLinkPossible: boolean;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
type Got = KcContext["pageId"];
|
||||
type Expected = AccountThemePageId;
|
||||
|
||||
type OnlyInGot = Exclude<Got, Expected>;
|
||||
type OnlyInExpected = Exclude<Expected, Got>;
|
||||
|
||||
assert<Equals<OnlyInGot, never>>();
|
||||
assert<Equals<OnlyInExpected, never>>();
|
||||
}
|
||||
|
||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
69
src/account/KcContext/getKcContextMock.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
|
||||
import type { AccountThemePageId } from "keycloakify/bin/shared/constants";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export function createGetKcContextMock<
|
||||
KcContextExtension extends { properties?: Record<string, string | undefined> },
|
||||
KcContextExtensionPerPage extends Record<`${string}.ftl`, Record<string, unknown>>
|
||||
>(params: {
|
||||
kcContextExtension: KcContextExtension;
|
||||
kcContextExtensionPerPage: KcContextExtensionPerPage;
|
||||
overrides?: DeepPartial<KcContextExtension & KcContextBase.Common>;
|
||||
overridesPerPage?: {
|
||||
[PageId in AccountThemePageId | keyof KcContextExtensionPerPage]?: DeepPartial<
|
||||
Extract<
|
||||
ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>,
|
||||
{ pageId: PageId }
|
||||
>
|
||||
>;
|
||||
};
|
||||
}) {
|
||||
const {
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage,
|
||||
overrides: overrides_global,
|
||||
overridesPerPage: overridesPerPage_global
|
||||
} = params;
|
||||
|
||||
type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|
||||
|
||||
function getKcContextMock<
|
||||
PageId extends AccountThemePageId | keyof KcContextExtensionPerPage
|
||||
>(params: {
|
||||
pageId: PageId;
|
||||
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
|
||||
}): Extract<KcContext, { pageId: PageId }> {
|
||||
const { pageId, overrides } = params;
|
||||
|
||||
const kcContextMock = structuredCloneButFunctions(
|
||||
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
|
||||
...kcContextCommonMock,
|
||||
pageId
|
||||
}
|
||||
);
|
||||
|
||||
[
|
||||
kcContextExtension,
|
||||
kcContextExtensionPerPage[pageId],
|
||||
overrides_global,
|
||||
overridesPerPage_global?.[pageId],
|
||||
overrides
|
||||
]
|
||||
.filter(exclude(undefined))
|
||||
.forEach(overrides =>
|
||||
deepAssign({
|
||||
target: kcContextMock,
|
||||
source: overrides
|
||||
})
|
||||
);
|
||||
|
||||
// @ts-expect-error
|
||||
return kcContextMock;
|
||||
}
|
||||
|
||||
return { getKcContextMock };
|
||||
}
|
2
src/account/KcContext/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type { ExtendKcContext, KcContext } from "./KcContext";
|
||||
export { createGetKcContextMock } from "./getKcContextMock";
|
213
src/account/KcContext/kcContextMocks.ts
Normal file
@ -0,0 +1,213 @@
|
||||
import "keycloakify/tools/Object.fromEntries";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { BASE_URL } from "keycloakify/lib/BASE_URL";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { LanguageTag } from "keycloakify/account/i18n/messages_defaultSet/types";
|
||||
|
||||
const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES}/account`;
|
||||
|
||||
export const kcContextCommonMock: KcContext.Common = {
|
||||
themeVersion: "0.0.0",
|
||||
keycloakifyVersion: "0.0.0",
|
||||
themeType: "account",
|
||||
themeName: "my-theme-name",
|
||||
url: {
|
||||
resourcesPath,
|
||||
resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`,
|
||||
resourceUrl: "#",
|
||||
accountUrl: "#",
|
||||
applicationsUrl: "#",
|
||||
logoutUrl: "#",
|
||||
getLogoutUrl: () => "#",
|
||||
logUrl: "#",
|
||||
passwordUrl: "#",
|
||||
sessionsUrl: "#",
|
||||
socialUrl: "#",
|
||||
totpUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
internationalizationEnabled: true,
|
||||
userManagedAccessAllowed: true
|
||||
},
|
||||
messagesPerField: {
|
||||
printIfExists: () => {
|
||||
return undefined;
|
||||
},
|
||||
existsError: () => false,
|
||||
get: key => `Fake error for ${key}`,
|
||||
exists: () => false
|
||||
},
|
||||
locale: {
|
||||
supported: (
|
||||
[
|
||||
/* spell-checker: disable */
|
||||
["de", "Deutsch"],
|
||||
["no", "Norsk"],
|
||||
["ru", "Русский"],
|
||||
["sv", "Svenska"],
|
||||
["pt-BR", "Português (Brasil)"],
|
||||
["lt", "Lietuvių"],
|
||||
["en", "English"],
|
||||
["it", "Italiano"],
|
||||
["fr", "Français"],
|
||||
["zh-CN", "中文简体"],
|
||||
["es", "Español"],
|
||||
["cs", "Čeština"],
|
||||
["ja", "日本語"],
|
||||
["sk", "Slovenčina"],
|
||||
["pl", "Polski"],
|
||||
["ca", "Català"],
|
||||
["nl", "Nederlands"],
|
||||
["tr", "Türkçe"],
|
||||
["ar", "العربية"],
|
||||
["da", "Dansk"],
|
||||
["fi", "Suomi"],
|
||||
["hu", "Magyar"],
|
||||
["lv", "Latviešu"]
|
||||
/* spell-checker: enable */
|
||||
] as const
|
||||
).map(([languageTag, label]) => {
|
||||
{
|
||||
type Got = typeof languageTag;
|
||||
type Expected = LanguageTag;
|
||||
|
||||
type Missing = Exclude<Expected, Got>;
|
||||
type Unexpected = Exclude<Got, Expected>;
|
||||
|
||||
assert<Equals<Missing, never>>;
|
||||
assert<Equals<Unexpected, never>>;
|
||||
}
|
||||
|
||||
return {
|
||||
languageTag,
|
||||
label,
|
||||
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
|
||||
} as const;
|
||||
}),
|
||||
|
||||
currentLanguageTag: "en"
|
||||
},
|
||||
features: {
|
||||
authorization: true,
|
||||
identityFederation: true,
|
||||
log: true,
|
||||
passwordUpdateSupported: true
|
||||
},
|
||||
referrer: undefined,
|
||||
account: {
|
||||
firstName: "john",
|
||||
lastName: "doe",
|
||||
email: "john.doe@code.gouv.fr",
|
||||
username: "doe_j"
|
||||
},
|
||||
properties: {},
|
||||
"x-keycloakify": {
|
||||
messages: {}
|
||||
}
|
||||
};
|
||||
|
||||
export const kcContextMocks: KcContext[] = [
|
||||
id<KcContext.Password>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "password.ftl",
|
||||
password: {
|
||||
passwordSet: true
|
||||
},
|
||||
stateChecker: "state checker"
|
||||
}),
|
||||
id<KcContext.Account>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "account.ftl",
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
referrerURI: "#",
|
||||
accountUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
registrationEmailAsUsername: true,
|
||||
editUsernameAllowed: true
|
||||
},
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Sessions>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "sessions.ftl",
|
||||
sessions: {
|
||||
sessions: [
|
||||
{
|
||||
ipAddress: "127.0.0.1",
|
||||
started: new Date().toString(),
|
||||
lastAccess: new Date().toString(),
|
||||
expires: new Date().toString(),
|
||||
clients: ["Chrome", "Firefox"],
|
||||
id: "f8951177-817d-4a70-9c02-86d3c170fe51"
|
||||
}
|
||||
]
|
||||
},
|
||||
stateChecker: "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
|
||||
}),
|
||||
id<KcContext.Totp>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "totp.ftl",
|
||||
totp: {
|
||||
enabled: true,
|
||||
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
|
||||
qrUrl: "#",
|
||||
totpSecretQrCode:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
|
||||
manualUrl: "#",
|
||||
totpSecret: "G4nsI8lQagRMUchH8jEG",
|
||||
otpCredentials: [],
|
||||
supportedApplications: [
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppGoogleName"
|
||||
],
|
||||
policy: {
|
||||
algorithm: "HmacSHA1",
|
||||
digits: 6,
|
||||
lookAheadWindow: 1,
|
||||
type: "totp",
|
||||
period: 30,
|
||||
getAlgorithmKey: () => "SHA1"
|
||||
}
|
||||
},
|
||||
mode: "qr",
|
||||
isAppInitiatedAction: false,
|
||||
stateChecker: ""
|
||||
}),
|
||||
id<KcContext.Log>({
|
||||
...kcContextCommonMock,
|
||||
pageId: "log.ftl",
|
||||
log: {
|
||||
events: [
|
||||
{
|
||||
date: "2/21/2024, 1:28:39 PM",
|
||||
event: "login",
|
||||
ipAddress: "172.17.0.1",
|
||||
client: "security-admin-console",
|
||||
details: [{ key: "openid-connect", value: "admin" }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContext.FederatedIdentity>({
|
||||
...kcContextCommonMock,
|
||||
stateChecker: "",
|
||||
pageId: "federatedIdentity.ftl",
|
||||
federatedIdentity: {
|
||||
identities: [
|
||||
{
|
||||
providerId: "keycloak-oidc",
|
||||
displayName: "keycloak-oidc",
|
||||
userName: "John",
|
||||
connected: true
|
||||
}
|
||||
],
|
||||
removeLinkPossible: true
|
||||
}
|
||||
})
|
||||
];
|
141
src/account/Template.tsx
Normal file
@ -0,0 +1,141 @@
|
||||
import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/account/Template.useInitialize";
|
||||
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
|
||||
const { url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = msgStr("accountManagementTitle");
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "html",
|
||||
className: kcClsx("kcHtmlClass")
|
||||
});
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "body",
|
||||
className: clsx("admin-console", "user", kcClsx("kcBodyClass"))
|
||||
});
|
||||
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="navbar navbar-default navbar-pf navbar-main header">
|
||||
<nav className="navbar" role="navigation">
|
||||
<div className="navbar-header">
|
||||
<div className="container">
|
||||
<h1 className="navbar-title">Keycloak</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-collapse navbar-collapse-1">
|
||||
<div className="container">
|
||||
<ul className="nav navbar-nav navbar-utility">
|
||||
{enabledLanguages.length > 1 && (
|
||||
<li>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{currentLanguage.label}
|
||||
</a>
|
||||
<ul>
|
||||
{enabledLanguages.map(({ languageTag, label, href }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href={href}>{label}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
{referrer?.url && (
|
||||
<li>
|
||||
<a href={referrer.url} id="referrer">
|
||||
{msg("backTo", referrer.name)}
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<a href={url.getLogoutUrl()}>{msg("doSignOut")}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<div className="container">
|
||||
<div className="bs-sidebar col-sm-3">
|
||||
<ul>
|
||||
<li className={clsx(active === "account" && "active")}>
|
||||
<a href={url.accountUrl}>{msg("account")}</a>
|
||||
</li>
|
||||
{features.passwordUpdateSupported && (
|
||||
<li className={clsx(active === "password" && "active")}>
|
||||
<a href={url.passwordUrl}>{msg("password")}</a>
|
||||
</li>
|
||||
)}
|
||||
<li className={clsx(active === "totp" && "active")}>
|
||||
<a href={url.totpUrl}>{msg("authenticator")}</a>
|
||||
</li>
|
||||
{features.identityFederation && (
|
||||
<li className={clsx(active === "social" && "active")}>
|
||||
<a href={url.socialUrl}>{msg("federatedIdentity")}</a>
|
||||
</li>
|
||||
)}
|
||||
<li className={clsx(active === "sessions" && "active")}>
|
||||
<a href={url.sessionsUrl}>{msg("sessions")}</a>
|
||||
</li>
|
||||
<li className={clsx(active === "applications" && "active")}>
|
||||
<a href={url.applicationsUrl}>{msg("applications")}</a>
|
||||
</li>
|
||||
{features.log && (
|
||||
<li className={clsx(active === "log" && "active")}>
|
||||
<a href={url.logUrl}>{msg("log")}</a>
|
||||
</li>
|
||||
)}
|
||||
{realm.userManagedAccessAllowed && features.authorization && (
|
||||
<li className={clsx(active === "authorization" && "active")}>
|
||||
<a href={url.resourceUrl}>{msg("myResources")}</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-9 content-area">
|
||||
{message !== undefined && (
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className="pficon pficon-ok"></span>}
|
||||
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(message.summary)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
35
src/account/Template.useInitialize.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import type { KcContext } from "keycloakify/account/KcContext";
|
||||
|
||||
export type KcContextLike = {
|
||||
url: {
|
||||
resourcesCommonPath: string;
|
||||
resourcesPath: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export function useInitialize(params: {
|
||||
kcContext: KcContextLike;
|
||||
doUseDefaultCss: boolean;
|
||||
}) {
|
||||
const { kcContext, doUseDefaultCss } = params;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
]
|
||||
});
|
||||
|
||||
return { isReadyToRender: areAllStyleSheetsLoaded };
|
||||
}
|
14
src/account/TemplateProps.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import type { ReactNode } from "react";
|
||||
import type { ClassKey } from "keycloakify/account/lib/kcClsx";
|
||||
|
||||
export type TemplateProps<KcContext, I18n> = {
|
||||
kcContext: KcContext;
|
||||
i18n: I18n;
|
||||
doUseDefaultCss: boolean;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
children: ReactNode;
|
||||
|
||||
active: string;
|
||||
};
|
||||
|
||||
export type { ClassKey };
|
3
src/account/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export type { ExtendKcContext } from "keycloakify/account/KcContext";
|
||||
export type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||
export { i18nBuilder, type MessageKey_defaultSet } from "keycloakify/account/i18n";
|
37
src/account/lib/kcClsx.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
|
||||
|
||||
export type ClassKey =
|
||||
| "kcHtmlClass"
|
||||
| "kcBodyClass"
|
||||
| "kcButtonClass"
|
||||
| "kcButtonPrimaryClass"
|
||||
| "kcButtonLargeClass"
|
||||
| "kcButtonDefaultClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcFormClass"
|
||||
| "kcFormGroupClass"
|
||||
| "kcInputWrapperClass"
|
||||
| "kcLabelClass"
|
||||
| "kcInputClass"
|
||||
| "kcInputErrorMessageClass";
|
||||
|
||||
export const { getKcClsx } = createGetKcClsx<ClassKey>({
|
||||
defaultClasses: {
|
||||
kcHtmlClass: undefined,
|
||||
kcBodyClass: undefined,
|
||||
kcButtonClass: "btn",
|
||||
kcContentWrapperClass: "row",
|
||||
kcButtonPrimaryClass: "btn-primary",
|
||||
kcButtonLargeClass: "btn-lg",
|
||||
kcButtonDefaultClass: "btn-default",
|
||||
kcFormClass: "form-horizontal",
|
||||
kcFormGroupClass: "form-group",
|
||||
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||
kcLabelClass: "control-label",
|
||||
kcInputClass: "form-control",
|
||||
kcInputErrorMessageClass:
|
||||
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
|
||||
}
|
||||
});
|
||||
|
||||
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];
|
127
src/account/pages/Account.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||
|
||||
const classes = {
|
||||
...props.classes,
|
||||
kcBodyClass: clsx(props.classes?.kcBodyClass, "user")
|
||||
};
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="account">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("editAccountHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div className="col-md-2 subtitle">
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={url.accountUrl} className="form-horizontal" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div className={clsx("form-group", messagesPerField.printIfExists("username", "has-error"))}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="username" className="control-label">
|
||||
{msg("username")}
|
||||
</label>
|
||||
{realm.editUsernameAllowed && <span className="required">*</span>}
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
id="username"
|
||||
name="username"
|
||||
disabled={!realm.editUsernameAllowed}
|
||||
defaultValue={account.username ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx("form-group", messagesPerField.printIfExists("email", "has-error"))}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="email" className="control-label">
|
||||
{msg("email")}
|
||||
</label>{" "}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="email" name="email" autoFocus defaultValue={account.email ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx("form-group", messagesPerField.printIfExists("firstName", "has-error"))}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="firstName" className="control-label">
|
||||
{msg("firstName")}
|
||||
</label>{" "}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="firstName" name="firstName" defaultValue={account.firstName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx("form-group", messagesPerField.printIfExists("lastName", "has-error"))}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="lastName" className="control-label">
|
||||
{msg("lastName")}
|
||||
</label>{" "}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="text" className="form-control" id="lastName" name="lastName" defaultValue={account.lastName ?? ""} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||
<div>
|
||||
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
|
||||
<button
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
{msg("doSave")}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Cancel"
|
||||
>
|
||||
{msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
136
src/account/pages/Applications.tsx
Normal file
@ -0,0 +1,136 @@
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const {
|
||||
url,
|
||||
applications: { applications },
|
||||
stateChecker
|
||||
} = kcContext;
|
||||
|
||||
const { msg, advancedMsg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="applications">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("applicationsHtmlTitle")}</h2>
|
||||
</div>
|
||||
|
||||
<form action={url.applicationsUrl} method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" id="referrer" name="referrer" value={stateChecker} />
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{msg("application")}</td>
|
||||
<td>{msg("availableRoles")}</td>
|
||||
<td>{msg("grantedPermissions")}</td>
|
||||
<td>{msg("additionalGrants")}</td>
|
||||
<td>{msg("action")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{applications.map(application => (
|
||||
<tr key={application.client.clientId}>
|
||||
<td>
|
||||
{application.effectiveUrl && (
|
||||
<a href={application.effectiveUrl}>
|
||||
{(application.client.name && advancedMsg(application.client.name)) || application.client.clientId}
|
||||
</a>
|
||||
)}
|
||||
{!application.effectiveUrl &&
|
||||
((application.client.name && advancedMsg(application.client.name)) || application.client.clientId)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) &&
|
||||
application.realmRolesAvailable.map((role, index) => (
|
||||
<span key={role.name}>
|
||||
{role.description ? advancedMsg(role.description) : advancedMsg(role.name)}
|
||||
{index < application.realmRolesAvailable.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
{application.resourceRolesAvailable &&
|
||||
Object.keys(application.resourceRolesAvailable).map(resource => (
|
||||
<span key={resource}>
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) && ", "}
|
||||
{application.resourceRolesAvailable[resource].map(clientRole => (
|
||||
<span key={clientRole.roleName}>
|
||||
{clientRole.roleDescription
|
||||
? advancedMsg(clientRole.roleDescription)
|
||||
: advancedMsg(clientRole.roleName)}{" "}
|
||||
{msg("inResource")}{" "}
|
||||
<strong>
|
||||
{clientRole.clientName ? advancedMsg(clientRole.clientName) : clientRole.clientId}
|
||||
</strong>
|
||||
{clientRole !==
|
||||
application.resourceRolesAvailable[resource][
|
||||
application.resourceRolesAvailable[resource].length - 1
|
||||
] && ", "}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{application.client.consentRequired ? (
|
||||
application.clientScopesGranted.map(claim => (
|
||||
<span key={claim}>
|
||||
{advancedMsg(claim)}
|
||||
{claim !== application.clientScopesGranted[application.clientScopesGranted.length - 1] && ", "}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<strong>{msg("fullAccess")}</strong>
|
||||
)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{application.additionalGrants.map(grant => (
|
||||
<span key={grant}>
|
||||
{advancedMsg(grant)}
|
||||
{grant !== application.additionalGrants[application.additionalGrants.length - 1] && ", "}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{(application.client.consentRequired && application.clientScopesGranted.length > 0) ||
|
||||
application.additionalGrants.length > 0 ? (
|
||||
<button
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonPrimaryClass", "kcButtonClass")}
|
||||
id={`revoke-${application.client.clientId}`}
|
||||
name="clientId"
|
||||
value={application.client.id}
|
||||
>
|
||||
{msg("revoke")}
|
||||
</button>
|
||||
) : null}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
function isArrayWithEmptyObject(variable: any): boolean {
|
||||
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
|
||||
}
|
58
src/account/pages/FederatedIdentity.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { url, federatedIdentity, stateChecker } = kcContext;
|
||||
const { msg } = i18n;
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="social">
|
||||
<div className="main-layout social">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("federatedIdentitiesHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div id="federated-identities">
|
||||
{federatedIdentity.identities.map(identity => (
|
||||
<div key={identity.providerId} className="row margin-bottom">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor={identity.providerId} className="control-label">
|
||||
{identity.displayName}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-5">
|
||||
<input disabled className="form-control" value={identity.userName} />
|
||||
</div>
|
||||
<div className="col-sm-5 col-md-5">
|
||||
{identity.connected ? (
|
||||
federatedIdentity.removeLinkPossible && (
|
||||
<form action={url.socialUrl} method="post" className="form-inline">
|
||||
<input type="hidden" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" name="action" value="remove" />
|
||||
<input type="hidden" name="providerId" value={identity.providerId} />
|
||||
<button id={`remove-link-${identity.providerId}`} className="btn btn-default">
|
||||
{msg("doRemove")}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
) : (
|
||||
<form action={url.socialUrl} method="post" className="form-inline">
|
||||
<input type="hidden" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" name="action" value="add" />
|
||||
<input type="hidden" name="providerId" value={identity.providerId} />
|
||||
<button id={`add-link-${identity.providerId}`} className="btn btn-default">
|
||||
{msg("doAdd")}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
70
src/account/pages/Log.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import type { Key } from "react";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { log } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("accountLogHtmlTitle")}</h2>
|
||||
</div>
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>{msg("date")}</td>
|
||||
<td>{msg("event")}</td>
|
||||
<td>{msg("ip")}</td>
|
||||
<td>{msg("client")}</td>
|
||||
<td>{msg("details")}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{log.events.map(
|
||||
(
|
||||
event: {
|
||||
date: string | number | Date;
|
||||
event: string;
|
||||
ipAddress: string;
|
||||
client: any;
|
||||
details: any[];
|
||||
},
|
||||
index: Key | null | undefined
|
||||
) => (
|
||||
<tr key={index}>
|
||||
<td>{event.date ? new Date(event.date).toLocaleString() : ""}</td>
|
||||
<td>{event.event}</td>
|
||||
<td>{event.ipAddress}</td>
|
||||
<td>{event.client || ""}</td>
|
||||
<td>
|
||||
{event.details.map((detail, detailIndex) => (
|
||||
<span key={detailIndex}>
|
||||
{`${detail.key} = ${detail.value}`}
|
||||
{detailIndex < event.details.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
11
src/account/pages/PageProps.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
|
||||
export type PageProps<NarrowedKcContext, I18n> = {
|
||||
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||
kcContext: NarrowedKcContext;
|
||||
i18n: I18n;
|
||||
doUseDefaultCss: boolean;
|
||||
classes?: Partial<Record<ClassKey, string>>;
|
||||
};
|
209
src/account/pages/Password.tsx
Normal file
@ -0,0 +1,209 @@
|
||||
import { useState } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template } = props;
|
||||
|
||||
const classes = {
|
||||
...props.classes,
|
||||
kcBodyClass: clsx(props.classes?.kcBodyClass, "password")
|
||||
};
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, password, account, stateChecker } = kcContext;
|
||||
|
||||
const { msgStr, msg } = i18n;
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPasswordConfirm, setNewPasswordConfirm] = useState("");
|
||||
const [newPasswordError, setNewPasswordError] = useState("");
|
||||
const [newPasswordConfirmError, setNewPasswordConfirmError] = useState("");
|
||||
const [hasNewPasswordBlurred, setHasNewPasswordBlurred] = useState(false);
|
||||
const [hasNewPasswordConfirmBlurred, setHasNewPasswordConfirmBlurred] = useState(false);
|
||||
|
||||
const checkNewPassword = (newPassword: string) => {
|
||||
if (!password.passwordSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === currentPassword) {
|
||||
setNewPasswordError(msgStr("newPasswordSameAsOld"));
|
||||
} else {
|
||||
setNewPasswordError("");
|
||||
}
|
||||
};
|
||||
|
||||
const checkNewPasswordConfirm = (newPasswordConfirm: string) => {
|
||||
if (newPasswordConfirm === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword !== newPasswordConfirm) {
|
||||
setNewPasswordConfirmError(msgStr("passwordConfirmNotMatch"));
|
||||
} else {
|
||||
setNewPasswordConfirmError("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{
|
||||
kcContext: {
|
||||
...kcContext,
|
||||
message: (() => {
|
||||
if (newPasswordError !== "") {
|
||||
return {
|
||||
type: "error",
|
||||
summary: newPasswordError
|
||||
};
|
||||
}
|
||||
|
||||
if (newPasswordConfirmError !== "") {
|
||||
return {
|
||||
type: "error",
|
||||
summary: newPasswordConfirmError
|
||||
};
|
||||
}
|
||||
|
||||
return kcContext.message;
|
||||
})()
|
||||
},
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
}}
|
||||
active="password"
|
||||
>
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("changePasswordHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div className="col-md-2 subtitle">
|
||||
<span className="subtitle">{msg("allFieldsRequired")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={url.passwordUrl} className="form-horizontal" method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={account.username ?? ""}
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
|
||||
{password.passwordSet && (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password" className="control-label">
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
value={currentPassword}
|
||||
onChange={event => setCurrentPassword(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-new" className="control-label">
|
||||
{msg("passwordNew")}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password-new"
|
||||
name="password-new"
|
||||
autoComplete="new-password"
|
||||
value={newPassword}
|
||||
onChange={event => {
|
||||
const newPassword = event.target.value;
|
||||
|
||||
setNewPassword(newPassword);
|
||||
if (hasNewPasswordBlurred) {
|
||||
checkNewPassword(newPassword);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHasNewPasswordBlurred(true);
|
||||
checkNewPassword(newPassword);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-confirm" className="control-label two-lines">
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="password"
|
||||
className="form-control"
|
||||
id="password-confirm"
|
||||
name="password-confirm"
|
||||
autoComplete="new-password"
|
||||
value={newPasswordConfirm}
|
||||
onChange={event => {
|
||||
const newPasswordConfirm = event.target.value;
|
||||
|
||||
setNewPasswordConfirm(newPasswordConfirm);
|
||||
if (hasNewPasswordConfirmBlurred) {
|
||||
checkNewPasswordConfirm(newPasswordConfirm);
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
setHasNewPasswordConfirmBlurred(true);
|
||||
checkNewPasswordConfirm(newPasswordConfirm);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||
<div>
|
||||
<button
|
||||
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
{msg("doSave")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
64
src/account/pages/Sessions.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, stateChecker, sessions } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("sessionsHtmlTitle")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table className="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{msg("ip")}</th>
|
||||
<th>{msg("started")}</th>
|
||||
<th>{msg("lastAccess")}</th>
|
||||
<th>{msg("expires")}</th>
|
||||
<th>{msg("clients")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody role="rowgroup">
|
||||
{sessions.sessions.map((session, index: number) => (
|
||||
<tr key={index}>
|
||||
<td>{session.ipAddress}</td>
|
||||
<td>{session?.started}</td>
|
||||
<td>{session?.lastAccess}</td>
|
||||
<td>{session?.expires}</td>
|
||||
<td>
|
||||
{session.clients.map((client: string, clientIndex: number) => (
|
||||
<div key={clientIndex}>
|
||||
{client}
|
||||
<br />
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<form action={url.sessionsUrl} method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<button id="logout-all-sessions" type="submit" className={kcClsx("kcButtonDefaultClass", "kcButtonClass")}>
|
||||
{msg("doLogOutAllSessions")}
|
||||
</button>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
226
src/account/pages/Totp.tsx
Normal file
@ -0,0 +1,226 @@
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
|
||||
<>
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("authenticatorTitle")}</h2>
|
||||
</div>
|
||||
{totp.otpCredentials.length === 0 && (
|
||||
<div className="subtitle col-md-2">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{totp.enabled && (
|
||||
<table className="table table-bordered table-striped">
|
||||
<thead>
|
||||
{totp.otpCredentials.length > 1 ? (
|
||||
<tr>
|
||||
<th colSpan={4}>{msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
) : (
|
||||
<tr>
|
||||
<th colSpan={3}>{msg("configureAuthenticators")}</th>
|
||||
</tr>
|
||||
)}
|
||||
</thead>
|
||||
<tbody>
|
||||
{totp.otpCredentials.map((credential, index) => (
|
||||
<tr key={index}>
|
||||
<td className="provider">{msg("mobile")}</td>
|
||||
{totp.otpCredentials.length > 1 && <td className="provider">{credential.id}</td>}
|
||||
<td className="provider">{credential.userLabel || ""}</td>
|
||||
<td className="action">
|
||||
<form action={url.totpUrl} method="post" className="form-inline">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<input type="hidden" id="submitAction" name="submitAction" value="Delete" />
|
||||
<input type="hidden" id="credentialId" name="credentialId" value={credential.id} />
|
||||
<button id={`remove-mobile-${index}`} className="btn btn-default">
|
||||
<i className="pficon pficon-delete"></i>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
{!totp.enabled && (
|
||||
<div>
|
||||
<hr />
|
||||
<ol id="kc-totp-settings">
|
||||
<li>
|
||||
<p>{msg("totpStep1")}</p>
|
||||
|
||||
<ul id="kc-totp-supported-apps">{totp.supportedApplications?.map(app => <li key={app}>{advancedMsg(app)}</li>)}</ul>
|
||||
</li>
|
||||
|
||||
{mode && mode == "manual" ? (
|
||||
<>
|
||||
<li>
|
||||
<p>{msg("totpManualStep2")}</p>
|
||||
<p>
|
||||
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href={totp.qrUrl} id="mode-barcode">
|
||||
{msg("totpScanBarcode")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>{msg("totpManualStep3")}</p>
|
||||
<ul>
|
||||
<li id="kc-totp-type">
|
||||
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
|
||||
</li>
|
||||
<li id="kc-totp-algorithm">
|
||||
{msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
|
||||
</li>
|
||||
<li id="kc-totp-digits">
|
||||
{msg("totpDigits")}: {totp.policy.digits}
|
||||
</li>
|
||||
{totp.policy.type === "totp" ? (
|
||||
<li id="kc-totp-period">
|
||||
{msg("totpInterval")}: {totp.policy.period}
|
||||
</li>
|
||||
) : (
|
||||
<li id="kc-totp-counter">
|
||||
{msg("totpCounter")}: {totp.policy.initialCounter}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</li>
|
||||
</>
|
||||
) : (
|
||||
<li>
|
||||
<p>{msg("totpStep2")}</p>
|
||||
<p>
|
||||
<img
|
||||
id="kc-totp-secret-qr-code"
|
||||
src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
|
||||
alt="Figure: Barcode"
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<a href={totp.manualUrl} id="mode-manual">
|
||||
{msg("totpUnableToScan")}
|
||||
</a>
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<p>{msg("totpStep3")}</p>
|
||||
<p>{msg("totpStep3DeviceName")}</p>
|
||||
</li>
|
||||
</ol>
|
||||
<hr />
|
||||
<form action={url.totpUrl} className={kcClsx("kcFormClass")} id="kc-totp-settings-form" method="post">
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="totp" className="control-label">
|
||||
{msg("authenticatorCode")}
|
||||
</label>
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
autoComplete="off"
|
||||
className={kcClsx("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("totp"))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||
</div>
|
||||
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
|
||||
{msg("totpDeviceName")}
|
||||
</label>
|
||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
</div>
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input
|
||||
type="text"
|
||||
id="userLabel"
|
||||
name="userLabel"
|
||||
autoComplete="off"
|
||||
className={kcClsx("kcInputClass")}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span
|
||||
id="input-error-otp-label"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("userLabel"))
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(kcClsx("kcFormGroupClass"), "text-right")}>
|
||||
<div className={kcClsx("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSave")}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
|
||||
id="cancelTOTPBtn"
|
||||
name="submitAction"
|
||||
value="Cancel"
|
||||
>
|
||||
{msg("doCancel")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</Template>
|
||||
);
|
||||
}
|
165
src/bin/add-story.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
const values = THEME_TYPES.filter(themeType => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
|
||||
assert(values.length > 0, "No theme is implemented in this project");
|
||||
|
||||
if (values.length === 1) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
return value;
|
||||
})();
|
||||
|
||||
if (
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||
) {
|
||||
console.log(
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for Single-Page Account themes.`
|
||||
);
|
||||
|
||||
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(chalk.cyan("Select the page you want to create a Storybook for:"));
|
||||
|
||||
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [...LOGIN_THEME_PAGE_IDS];
|
||||
case "account":
|
||||
return [...ACCOUNT_THEME_PAGE_IDS];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
console.log(`→ ${pageId}`);
|
||||
|
||||
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
|
||||
/ftl$/,
|
||||
"stories.tsx"
|
||||
);
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"pages",
|
||||
componentBasename
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(`${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let sourceCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"stories",
|
||||
themeType,
|
||||
"pages",
|
||||
componentBasename
|
||||
)
|
||||
)
|
||||
.toString("utf8")
|
||||
.replace('import React from "react";\n', "")
|
||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
[
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} copy pasted from the Keycloakify source code into your project`,
|
||||
`You can start storybook with ${chalk.bold("npm run storybook")}`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||
import type { ParsedPackageJson } from "./generateJavaStackFiles";
|
||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
||||
import { URL } from "url";
|
||||
|
||||
|
||||
const reactProjectDirPath = process.cwd();
|
||||
|
||||
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
||||
|
||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
||||
|
||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||
|
||||
export function main() {
|
||||
|
||||
console.log("🔏 Building the keycloak theme...⌚");
|
||||
|
||||
generateKeycloakThemeResources({
|
||||
keycloakThemeBuildingDirPath,
|
||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||
"themeName": parsedPackageJson.name,
|
||||
...(() => {
|
||||
|
||||
const url = (() => {
|
||||
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
return homepage === undefined ?
|
||||
undefined :
|
||||
new URL(homepage);
|
||||
|
||||
})();
|
||||
|
||||
return {
|
||||
"urlPathname":
|
||||
url === undefined ?
|
||||
"/" :
|
||||
url.pathname.replace(/([^/])$/, "$1/"),
|
||||
"urlOrigin": !doUseExternalAssets ? undefined : (() => {
|
||||
|
||||
if (url === undefined) {
|
||||
console.error("ERROR: You must specify 'homepage' in your package.json");
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
return url.origin;
|
||||
|
||||
})()
|
||||
|
||||
};
|
||||
|
||||
})()
|
||||
});
|
||||
|
||||
const { jarFilePath } = generateJavaStackFiles({
|
||||
parsedPackageJson,
|
||||
keycloakThemeBuildingDirPath
|
||||
});
|
||||
|
||||
child_process.execSync(
|
||||
"mvn package",
|
||||
{ "cwd": keycloakThemeBuildingDirPath }
|
||||
);
|
||||
|
||||
generateDebugFiles({
|
||||
keycloakThemeBuildingDirPath,
|
||||
"packageJsonName": parsedPackageJson.name
|
||||
});
|
||||
|
||||
console.log([
|
||||
'',
|
||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||
`It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image. (Tested with 11.0.3)`,
|
||||
'',
|
||||
'Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:',
|
||||
'',
|
||||
'value.yaml: ',
|
||||
' extraInitContainers: |',
|
||||
' - name: realm-ext-provider',
|
||||
' image: curlimages/curl',
|
||||
' imagePullPolicy: IfNotPresent',
|
||||
' command:',
|
||||
' - sh',
|
||||
' args:',
|
||||
' - -c',
|
||||
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
|
||||
' volumeMounts:',
|
||||
' - name: extensions',
|
||||
' mountPath: /extensions',
|
||||
' ',
|
||||
' extraVolumeMounts: |',
|
||||
' - name: extensions',
|
||||
' mountPath: /opt/jboss/keycloak/standalone/deployments',
|
||||
'',
|
||||
'',
|
||||
'To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:',
|
||||
'',
|
||||
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename))} 👈`,
|
||||
'',
|
||||
'To enable the theme within keycloak log into the admin console ( 👉 http://localhost:8080 username: admin, password: admin 👈), create a realm (called "myrealm" for example),',
|
||||
`go to your realm settings, click on the theme tab then select ${parsedPackageJson.name}.`,
|
||||
`More details: https://www.keycloak.org/getting-started/getting-started-docker`,
|
||||
'',
|
||||
'Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈',
|
||||
'',
|
||||
].join("\n"));
|
||||
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
export const ftlValuesGlobalName = "kcContext";
|
@ -1,74 +0,0 @@
|
||||
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
|
||||
|
||||
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
||||
|
||||
/** Files for being able to run a hot reload keycloak container */
|
||||
export function generateDebugFiles(
|
||||
params: {
|
||||
packageJsonName: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
}
|
||||
) {
|
||||
|
||||
const { packageJsonName, keycloakThemeBuildingDirPath } = params;
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
||||
Buffer.from(
|
||||
[
|
||||
"FROM jboss/keycloak:11.0.3",
|
||||
"",
|
||||
"USER root",
|
||||
"",
|
||||
"WORKDIR /",
|
||||
"",
|
||||
"ADD configuration /opt/jboss/keycloak/standalone/configuration/",
|
||||
"",
|
||||
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
const dockerImage = `${packageJsonName}/keycloak-hot-reload`;
|
||||
const containerName = "keycloak-testing-container";
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename),
|
||||
Buffer.from(
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"",
|
||||
`cd ${keycloakThemeBuildingDirPath}`,
|
||||
"",
|
||||
`docker rm ${containerName} || true`,
|
||||
"",
|
||||
`docker build . -t ${dockerImage}`,
|
||||
"",
|
||||
"docker run \\",
|
||||
" -p 8080:8080 \\",
|
||||
` --name ${containerName} \\`,
|
||||
" -e KEYCLOAK_USER=admin \\",
|
||||
" -e KEYCLOAK_PASSWORD=admin \\",
|
||||
` -v ${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", packageJsonName)
|
||||
}:/opt/jboss/keycloak/themes/${packageJsonName}:rw \\`,
|
||||
` -it ${dockerImage}:latest`,
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
),
|
||||
{ "mode": 0o755 }
|
||||
);
|
||||
|
||||
const standaloneHaFilePath = pathJoin(keycloakThemeBuildingDirPath, "configuration", "standalone-ha.xml");
|
||||
|
||||
try { fs.mkdirSync(pathDirname(standaloneHaFilePath)); } catch { }
|
||||
|
||||
fs.writeFileSync(
|
||||
standaloneHaFilePath,
|
||||
fs.readFileSync(pathJoin(__dirname, pathBasename(standaloneHaFilePath)))
|
||||
);
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./generateDebugFiles";
|
@ -1,666 +0,0 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
|
||||
<server xmlns="urn:jboss:domain:13.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.clustering.jgroups"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.security"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.microprofile.config-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.health-smallrye"/>
|
||||
<extension module="org.wildfly.extension.microprofile.metrics-smallrye"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<security-realms>
|
||||
<security-realm name="ManagementRealm">
|
||||
<authentication>
|
||||
<local default-user="$local" skip-group-loading="true"/>
|
||||
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization map-groups-to-roles="false">
|
||||
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
<security-realm name="ApplicationRealm">
|
||||
<server-identities>
|
||||
<ssl>
|
||||
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
|
||||
</ssl>
|
||||
</server-identities>
|
||||
<authentication>
|
||||
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
|
||||
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authentication>
|
||||
<authorization>
|
||||
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</authorization>
|
||||
</security-realm>
|
||||
</security-realms>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface security-realm="ManagementRealm">
|
||||
<http-upgrade enabled="true"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak">
|
||||
<level name="${env.KEYCLOAK_LOGLEVEL:INFO}"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="${env.ROOT_LOGLEVEL:INFO}"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<pool>
|
||||
<max-pool-size>100</max-pool-size>
|
||||
</pool>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<drivers>
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:5.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:7.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="distributable" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote connector-ref="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:10.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:10.0">
|
||||
<cache-container name="keycloak" module="org.keycloak.keycloak-model-infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="realms">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="authorization">
|
||||
<object-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<object-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
<replicated-cache name="work"/>
|
||||
<distributed-cache name="sessions" owners="1"/>
|
||||
<distributed-cache name="authenticationSessions" owners="1"/>
|
||||
<distributed-cache name="offlineSessions" owners="1"/>
|
||||
<distributed-cache name="clientSessions" owners="1"/>
|
||||
<distributed-cache name="offlineClientSessions" owners="1"/>
|
||||
<distributed-cache name="loginFailures" owners="1"/>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<object-memory size="-1"/>
|
||||
<expiration interval="300000" max-idle="-1"/>
|
||||
</distributed-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="dist" module="org.wildfly.clustering.web.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="ejb" aliases="sfsb" default-cache="dist" module="org.wildfly.clustering.ejb.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" module="org.infinispan.hibernate-cache">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="local-query">
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<invalidation-cache name="entity">
|
||||
<transaction mode="NON_XA"/>
|
||||
<object-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</invalidation-cache>
|
||||
<replicated-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jgroups:8.0">
|
||||
<channels default="ee">
|
||||
<channel name="ee" stack="udp" cluster="ejb"/>
|
||||
</channels>
|
||||
<stacks>
|
||||
<stack name="udp">
|
||||
<transport type="UDP" socket-binding="jgroups-udp"/>
|
||||
<protocol type="PING"/>
|
||||
<protocol type="MERGE3"/>
|
||||
<socket-protocol type="FD_SOCK" socket-binding="jgroups-udp-fd"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS"/>
|
||||
<protocol type="UFC"/>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG3"/>
|
||||
</stack>
|
||||
<stack name="tcp">
|
||||
<transport type="TCP" socket-binding="jgroups-tcp"/>
|
||||
<socket-protocol type="MPING" socket-binding="jgroups-mping"/>
|
||||
<protocol type="MERGE3"/>
|
||||
<socket-protocol type="FD_SOCK" socket-binding="jgroups-tcp-fd"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS"/>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG3"/>
|
||||
</stack>
|
||||
</stacks>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-datasource="" default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>
|
||||
classpath:${jboss.home.dir}/providers/*
|
||||
</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>-1</staticMaxAge>
|
||||
<cacheThemes>false</cacheThemes>
|
||||
<cacheTemplates>false</cacheTemplates>
|
||||
<welcomeTheme>${env.KEYCLOAK_WELCOME_THEME:keycloak}</welcomeTheme>
|
||||
<default>${env.KEYCLOAK_DEFAULT_THEME:keycloak}</default>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>${keycloak.hostname.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="false"/>
|
||||
</properties>
|
||||
</provider>
|
||||
<provider name="fixed" enabled="true">
|
||||
<properties>
|
||||
<property name="hostname" value="${keycloak.hostname.fixed.hostname:localhost}"/>
|
||||
<property name="httpPort" value="${keycloak.hostname.fixed.httpPort:-1}"/>
|
||||
<property name="httpsPort" value="${keycloak.hostname.fixed.httpsPort:-1}"/>
|
||||
<property name="alwaysHttps" value="${keycloak.hostname.fixed.alwaysHttps:false}"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:4.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-config-smallrye:1.0"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-health-smallrye:2.0" security-enabled="false" empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}" empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}"/>
|
||||
<subsystem xmlns="urn:wildfly:microprofile-metrics-smallrye:2.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise-socket="modcluster" listener="ajp">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security:2.0">
|
||||
<security-domains>
|
||||
<security-domain name="other" cache-type="default">
|
||||
<authentication>
|
||||
<login-module code="Remoting" flag="optional">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
<login-module code="RealmDirect" flag="required">
|
||||
<module-option name="password-stacking" value="useFirstPass"/>
|
||||
</login-module>
|
||||
</authentication>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-web-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
<security-domain name="jaspitest" cache-type="default">
|
||||
<authentication-jaspi>
|
||||
<login-module-stack name="dummy">
|
||||
<login-module code="Dummy" flag="optional"/>
|
||||
</login-module-stack>
|
||||
<auth-module code="Dummy"/>
|
||||
</authentication-jaspi>
|
||||
</security-domain>
|
||||
<security-domain name="jboss-ejb-policy" cache-type="default">
|
||||
<authorization>
|
||||
<policy-module code="Delegating" flag="required"/>
|
||||
</authorization>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:5.0">
|
||||
<core-environment node-identifier="${jboss.tx.node.id:1}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:11.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" read-timeout="30000" socket-binding="http" redirect-socket="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
|
||||
<https-listener name="https" read-timeout="30000" socket-binding="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" security-realm="ApplicationRealm" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker security-realm="ApplicationRealm"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="private">
|
||||
<inet-address value="${jboss.bind.address.private:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
|
||||
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
|
||||
<socket-binding name="http" port="${jboss.http.port:8080}"/>
|
||||
<socket-binding name="https" port="${jboss.https.port:8443}"/>
|
||||
<socket-binding name="jgroups-mping" interface="private" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
|
||||
<socket-binding name="jgroups-tcp" interface="private" port="7600"/>
|
||||
<socket-binding name="jgroups-tcp-fd" interface="private" port="57600"/>
|
||||
<socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>
|
||||
<socket-binding name="jgroups-udp-fd" interface="private" port="54200"/>
|
||||
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
|
||||
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="localhost" port="25"/>
|
||||
</outbound-socket-binding>
|
||||
</socket-binding-group>
|
||||
</server>
|
@ -1,28 +0,0 @@
|
||||
|
||||
Object.defineProperty(
|
||||
Object,
|
||||
"deepAssign",
|
||||
{
|
||||
"value": function callee(target, source) {
|
||||
Object.keys(source).forEach(function (key) {
|
||||
var value = source[key];
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
return;
|
||||
}
|
||||
if (value instanceof Object) {
|
||||
if (value instanceof Array) {
|
||||
value.forEach(function (entry) {
|
||||
target[key].push(entry);
|
||||
});
|
||||
return;
|
||||
}
|
||||
callee(target[key], value);
|
||||
return;
|
||||
}
|
||||
target[key] = value;
|
||||
});
|
||||
return target;
|
||||
}
|
||||
}
|
||||
);
|
@ -1,26 +0,0 @@
|
||||
|
||||
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
|
||||
|
||||
var unes = {
|
||||
'&': '&',
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'>': '>',
|
||||
''': "'",
|
||||
''': "'",
|
||||
'"': '"',
|
||||
'"': '"'
|
||||
};
|
||||
var cape = function (m) { return unes[m]; };
|
||||
|
||||
Object.defineProperty(
|
||||
String,
|
||||
"htmlUnescape",
|
||||
{
|
||||
"value": function (un) {
|
||||
return String.prototype.replace.call(un, es, cape);
|
||||
}
|
||||
}
|
||||
);
|
@ -1,263 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"url": {
|
||||
"loginAction": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.loginAction?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"resourcesPath": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.resourcesPath?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"resourcesCommonPath": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.resourcesCommonPath?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"loginRestartFlowUrl": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.loginRestartFlowUrl?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"loginUrl": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.loginUrl?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
},
|
||||
"realm": {
|
||||
"displayName": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${realm.displayName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"displayNameHtml": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${realm.displayNameHtml!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"internationalizationEnabled": (function (){
|
||||
|
||||
<#attempt>
|
||||
return ${realm.internationalizationEnabled?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"registrationEmailAsUsername": (function (){
|
||||
|
||||
<#attempt>
|
||||
return ${realm.registrationEmailAsUsername?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
},
|
||||
"locale": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if realm.internationalizationEnabled>
|
||||
|
||||
return {
|
||||
"supported": (function(){
|
||||
|
||||
var out= [];
|
||||
|
||||
<#attempt>
|
||||
<#list locale.supported as lng>
|
||||
out.push({
|
||||
"url": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${lng.url?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"label": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${lng.label}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"languageTag": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${lng.languageTag}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
});
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
})(),
|
||||
"current": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${locale.current}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
};
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"auth": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if auth?has_content>
|
||||
|
||||
var out= {
|
||||
"showUsername": (function (){
|
||||
|
||||
<#attempt>
|
||||
return ${auth.showUsername()?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"showResetCredentials": (function (){
|
||||
|
||||
<#attempt>
|
||||
return ${auth.showResetCredentials()?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"showTryAnotherWayLink": (function(){
|
||||
|
||||
<#attempt>
|
||||
return ${auth.showTryAnotherWayLink()?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
};
|
||||
|
||||
<#attempt>
|
||||
<#if auth.showUsername() && !auth.showResetCredentials()>
|
||||
Object.assign(
|
||||
out,
|
||||
{
|
||||
"attemptedUsername": (function (){
|
||||
<#attempt>
|
||||
return "${auth.attemptedUsername}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
}
|
||||
);
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"scripts": (function(){
|
||||
|
||||
var out = [];
|
||||
|
||||
<#attempt>
|
||||
<#if scripts??>
|
||||
<#attempt>
|
||||
<#list scripts as script>
|
||||
out.push((function (){
|
||||
|
||||
<#attempt>
|
||||
return "${script}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})());
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
})(),
|
||||
"message": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if message?has_content>
|
||||
|
||||
return {
|
||||
"type": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${message.type}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"summary": (function (){
|
||||
|
||||
<#attempt>
|
||||
return String.htmlUnescape("${message.summary}");
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
};
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"isAppInitiatedAction": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if isAppInitiatedAction??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return false;
|
||||
|
||||
})()
|
||||
}
|
||||
</script>
|
@ -1,23 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"client": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if client??>
|
||||
return {
|
||||
"baseUrl": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${(client.baseUrl!'')?no_esc}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
};
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
}
|
||||
</script>
|
@ -1,193 +0,0 @@
|
||||
|
||||
|
||||
import cheerio from "cheerio";
|
||||
import {
|
||||
replaceImportsFromStaticInJsCode,
|
||||
replaceImportsInInlineCssCode,
|
||||
generateCssCodeToDefineGlobals
|
||||
} from "../replaceImportFromStatic";
|
||||
import fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
|
||||
export const pageIds = [
|
||||
"login.ftl", "register.ftl", "info.ftl",
|
||||
"error.ftl", "login-reset-password.ftl",
|
||||
"login-verify-email.ftl", "terms.ftl",
|
||||
"login-otp.ftl", "login-update-profile.ftl",
|
||||
"login-idp-link-confirm.ftl"
|
||||
] as const;
|
||||
|
||||
export type PageId = typeof pageIds[number];
|
||||
|
||||
function loadAdjacentFile(fileBasename: string) {
|
||||
return fs.readFileSync(pathJoin(__dirname, fileBasename))
|
||||
.toString("utf8");
|
||||
};
|
||||
|
||||
function loadFtlFile(ftlFileBasename: PageId | "common.ftl") {
|
||||
try {
|
||||
|
||||
return loadAdjacentFile(ftlFileBasename)
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
|
||||
|
||||
} catch {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function generateFtlFilesCodeFactory(
|
||||
params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
indexHtmlCode: string;
|
||||
urlPathname: string;
|
||||
urlOrigin: undefined | string;
|
||||
}
|
||||
) {
|
||||
|
||||
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
$("script:not([src])").each((...[, element]) => {
|
||||
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": $(element).html()!,
|
||||
urlOrigin
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
|
||||
});
|
||||
|
||||
$("style").each((...[, element]) => {
|
||||
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
"cssCode": $(element).html()!,
|
||||
"urlPathname": params.urlPathname,
|
||||
urlOrigin
|
||||
});
|
||||
|
||||
$(element).text(fixedCssCode);
|
||||
|
||||
});
|
||||
|
||||
([
|
||||
["link", "href"],
|
||||
["script", "src"],
|
||||
] as const).forEach(([selector, attrName]) =>
|
||||
$(selector).each((...[, element]) => {
|
||||
|
||||
const href = $(element).attr(attrName);
|
||||
|
||||
if (href === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(element).attr(
|
||||
attrName,
|
||||
urlOrigin !== undefined ?
|
||||
href.replace(/^\//, `${urlOrigin}/`) :
|
||||
href.replace(
|
||||
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
|
||||
"${url.resourcesPath}/build/"
|
||||
)
|
||||
);
|
||||
|
||||
})
|
||||
);
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const ftlCommonPlaceholders = {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("common.ftl"),
|
||||
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
|
||||
[
|
||||
'<#if scripts??>',
|
||||
' <#list scripts as script>',
|
||||
' <script src="${script}" type="text/javascript"></script>',
|
||||
' </#list>',
|
||||
'</#if>'
|
||||
].join("\n")
|
||||
};
|
||||
|
||||
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
|
||||
'',
|
||||
'<style>',
|
||||
generateCssCodeToDefineGlobals({
|
||||
cssGlobalsToDefine,
|
||||
urlPathname
|
||||
}).cssCodeToPrependInHead,
|
||||
'</style>',
|
||||
''
|
||||
]),
|
||||
...["Object.deepAssign.js", "String.htmlUnescape.js"].map(
|
||||
fileBasename => [
|
||||
"<script>",
|
||||
loadAdjacentFile(fileBasename),
|
||||
"</script>"
|
||||
].join("\n")
|
||||
),
|
||||
'<script>',
|
||||
` window.${ftlValuesGlobalName}= Object.assign(`,
|
||||
` {},`,
|
||||
` ${objectKeys(ftlCommonPlaceholders)[0]}`,
|
||||
' );',
|
||||
'</script>',
|
||||
'',
|
||||
pageSpecificCodePlaceholder,
|
||||
'',
|
||||
objectKeys(ftlCommonPlaceholders)[1]
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(
|
||||
params: {
|
||||
pageId: PageId;
|
||||
}
|
||||
): { ftlCode: string; } {
|
||||
|
||||
const { pageId } = params;
|
||||
|
||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||
|
||||
const ftlPlaceholders = {
|
||||
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageId),
|
||||
...ftlCommonPlaceholders
|
||||
};
|
||||
|
||||
let ftlCode = $.html()
|
||||
.replace(
|
||||
pageSpecificCodePlaceholder,
|
||||
[
|
||||
'<script>',
|
||||
` Object.deepAssign(`,
|
||||
` window.${ftlValuesGlobalName},`,
|
||||
` { "pageId": "${pageId}" }`,
|
||||
' );',
|
||||
` Object.deepAssign(`,
|
||||
` window.${ftlValuesGlobalName},`,
|
||||
` ${objectKeys(ftlPlaceholders)[0]}`,
|
||||
' );',
|
||||
'</script>'
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
objectKeys(ftlPlaceholders)
|
||||
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id]));
|
||||
|
||||
return { ftlCode };
|
||||
|
||||
}
|
||||
|
||||
return { generateFtlFilesCode };
|
||||
|
||||
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./generateFtl";
|
@ -1,82 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"messageHeader": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messageHeader!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"requiredActions": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if requiredActions??>
|
||||
|
||||
var out =[];
|
||||
|
||||
<#attempt>
|
||||
<#list requiredActions>
|
||||
<#attempt>
|
||||
<#items as reqActionItem>
|
||||
out.push((function (){
|
||||
|
||||
<#attempt>
|
||||
return "${reqActionItem}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})());
|
||||
</#items>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"skipLink": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if skipLink??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
return false;
|
||||
|
||||
})(),
|
||||
"pageRedirectUri": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${(pageRedirectUri!'')?no_esc}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"actionUri": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${(actionUri!'')?no_esc}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"client": {
|
||||
"baseUrl": (function(){
|
||||
|
||||
<#attempt>
|
||||
return "${(client.baseUrl!'')?no_esc}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,11 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"idpAlias": (function (){
|
||||
<#attempt>
|
||||
return "${idpAlias}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
return "";
|
||||
})()
|
||||
}
|
||||
</script>
|
@ -1,37 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"otpLogin": {
|
||||
"userOtpCredentials": (function(){
|
||||
|
||||
var out = [];
|
||||
|
||||
<#attempt>
|
||||
<#list otpLogin.userOtpCredentials as otpCredential>
|
||||
out.push({
|
||||
"id": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${otpCredential.id}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"userLabel": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${otpCredential.userLabel}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
});
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
})()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,14 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"realm": {
|
||||
"loginWithEmailAllowed": (function (){
|
||||
|
||||
<#attempt>
|
||||
return ${realm.loginWithEmailAllowed?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
}
|
||||
}
|
||||
</script>
|
@ -1,67 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"user": {
|
||||
"editUsernameAllowed": (function (){
|
||||
<#attempt>
|
||||
return ${user.editUsernameAllowed?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"username": (function (){
|
||||
<#attempt>
|
||||
return "${user.username!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"emal": (function (){
|
||||
<#attempt>
|
||||
return "${user.email!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"firstName": (function (){
|
||||
<#attempt>
|
||||
return "${user.firstName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"lastName": (function (){
|
||||
<#attempt>
|
||||
return "${user.lastName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
},
|
||||
"messagesPerField": {
|
||||
"printIfExists": function (key, x) {
|
||||
switch(key){
|
||||
case "username": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "email": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "firstName": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
case "lastName": return (function (){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
@ -1,160 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"url": {
|
||||
"loginResetCredentialsUrl": (function (){
|
||||
<#attempt>
|
||||
return "${url.loginResetCredentialsUrl?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"registrationUrl": (function (){
|
||||
<#attempt>
|
||||
return "${url.registrationUrl?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
},
|
||||
"realm": {
|
||||
"loginWithEmailAllowed": (function(){
|
||||
<#attempt>
|
||||
return ${realm.loginWithEmailAllowed?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"rememberMe": (function (){
|
||||
<#attempt>
|
||||
return ${realm.rememberMe?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"password": (function (){
|
||||
<#attempt>
|
||||
return ${realm.password?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"resetPasswordAllowed": (function (){
|
||||
<#attempt>
|
||||
return ${realm.resetPasswordAllowed?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"registrationAllowed": (function (){
|
||||
<#attempt>
|
||||
return ${realm.registrationAllowed?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
},
|
||||
"auth": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if auth?has_content>
|
||||
|
||||
return {
|
||||
"selectedCredential": (function (){
|
||||
<#attempt>
|
||||
return "${auth.selectedCredential!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
};
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"social": {
|
||||
"displayInfo": (function (){
|
||||
<#attempt>
|
||||
return ${social.displayInfo?c};
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"providers": (()=>{
|
||||
|
||||
<#attempt>
|
||||
<#if social.providers??>
|
||||
|
||||
var out= [];
|
||||
|
||||
<#attempt>
|
||||
<#list social.providers as p>
|
||||
out.push({
|
||||
"loginUrl": (function (){
|
||||
<#attempt>
|
||||
return "${p.loginUrl?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"alias": (function (){
|
||||
<#attempt>
|
||||
return "${p.alias}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"providerId": (function (){
|
||||
<#attempt>
|
||||
return "${p.providerId}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"displayName": (function (){
|
||||
<#attempt>
|
||||
return "${p.displayName}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})()
|
||||
});
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
},
|
||||
"usernameEditDisabled": (function () {
|
||||
|
||||
<#attempt>
|
||||
<#if usernameEditDisabled??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
return false;
|
||||
|
||||
})(),
|
||||
"login": {
|
||||
"username": (function (){
|
||||
<#attempt>
|
||||
return "${login.username!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
})(),
|
||||
"rememberMe": (function (){
|
||||
<#attempt>
|
||||
<#if login.rememberMe??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
return false;
|
||||
})()
|
||||
},
|
||||
"registrationDisabled": (function (){
|
||||
<#attempt>
|
||||
<#if registrationDisabled??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
return false;
|
||||
})()
|
||||
}
|
||||
</script>
|
@ -1,189 +0,0 @@
|
||||
<script>const _=
|
||||
{
|
||||
"url": {
|
||||
"registrationAction": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${url.registrationAction?no_esc}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
},
|
||||
"messagesPerField": {
|
||||
"printIfExists": function (key, x) {
|
||||
switch(key){
|
||||
case "userLabel": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "username": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "email": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "firstName": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "lastName": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "password": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
case "password-confirm": return (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})();
|
||||
}
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
"formData": {
|
||||
"firstName": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${register.formData.firstName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"displayName": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${register.formData.displayName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"lastName": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${register.formData.lastName!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"email": (function(){
|
||||
|
||||
<#attempt>
|
||||
return "${register.formData.email!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"username": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${register.formData.username!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})()
|
||||
}
|
||||
},
|
||||
"passwordRequired": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if passwordRequired??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return false;
|
||||
|
||||
})(),
|
||||
"recaptchaRequired": (function (){
|
||||
|
||||
<#attempt>
|
||||
<#if passwordRequired??>
|
||||
return true;
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return false;
|
||||
|
||||
})(),
|
||||
"recaptchaSiteKey": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${recaptchaSiteKey!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"authorizedMailDomains": (function (){
|
||||
|
||||
<#attempt>
|
||||
return "${authorizedMailDomains!''}" || undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})(),
|
||||
"authorizedMailDomains": (function(){
|
||||
|
||||
var out = undefined;
|
||||
|
||||
<#attempt>
|
||||
<#if authorizedMailDomains??>
|
||||
|
||||
out = [];
|
||||
|
||||
<#attempt>
|
||||
<#list authorizedMailDomains as authorizedMailDomain>
|
||||
out.push((function (){
|
||||
|
||||
<#attempt>
|
||||
return "${authorizedMailDomain}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
})());
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
return out;
|
||||
|
||||
})(),
|
||||
}
|
||||
</script>
|
@ -1,100 +0,0 @@
|
||||
|
||||
import * as url from "url";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
|
||||
export type ParsedPackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
homepage?: string;
|
||||
};
|
||||
|
||||
export function generateJavaStackFiles(
|
||||
params: {
|
||||
parsedPackageJson: ParsedPackageJson;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
}
|
||||
): { jarFilePath: string; } {
|
||||
|
||||
const {
|
||||
parsedPackageJson: { name, version, homepage },
|
||||
keycloakThemeBuildingDirPath
|
||||
} = params;
|
||||
|
||||
{
|
||||
|
||||
const { pomFileCode } = (function generatePomFileCode(): { pomFileCode: string; } {
|
||||
|
||||
|
||||
const groupId = (() => {
|
||||
|
||||
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${name}`;
|
||||
|
||||
return (!homepage ?
|
||||
fallbackGroupId :
|
||||
url.parse(homepage).host?.replace(/:[0-9]+$/,"")?.split(".").reverse().join(".") ?? fallbackGroupId
|
||||
) + ".keycloak";
|
||||
|
||||
})();
|
||||
|
||||
const artefactId = `${name}-keycloak-theme`;
|
||||
|
||||
const pomFileCode = [
|
||||
`<?xml version="1.0"?>`,
|
||||
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
||||
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
|
||||
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
||||
` <modelVersion>4.0.0</modelVersion>`,
|
||||
` <groupId>${groupId}</groupId>`,
|
||||
` <artifactId>${artefactId}</artifactId>`,
|
||||
` <version>${version}</version>`,
|
||||
` <name>${artefactId}</name>`,
|
||||
` <description />`,
|
||||
`</project>`
|
||||
].join("\n");
|
||||
|
||||
return { pomFileCode };
|
||||
|
||||
})();
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakThemeBuildingDirPath, "pom.xml"),
|
||||
Buffer.from(pomFileCode, "utf8")
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
const themeManifestFilePath = pathJoin(
|
||||
keycloakThemeBuildingDirPath, "src", "main",
|
||||
"resources", "META-INF", "keycloak-themes.json"
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
fs.mkdirSync(pathDirname(themeManifestFilePath));
|
||||
|
||||
} catch { }
|
||||
|
||||
fs.writeFileSync(
|
||||
themeManifestFilePath,
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
"themes": [
|
||||
{
|
||||
"name": name,
|
||||
"types": ["login"]
|
||||
}
|
||||
]
|
||||
}, null, 2),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return { "jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${name}-${version}.jar`) };
|
||||
|
||||
}
|
||||
|
@ -1,169 +0,0 @@
|
||||
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import {
|
||||
replaceImportsInCssCode,
|
||||
replaceImportsFromStaticInJsCode
|
||||
} from "./replaceImportFromStatic";
|
||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
||||
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
|
||||
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
|
||||
import * as child_process from "child_process";
|
||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/kcContextMocks/urlResourcesPath";
|
||||
import { isInside } from "../tools/isInside";
|
||||
|
||||
|
||||
export function generateKeycloakThemeResources(
|
||||
params: {
|
||||
themeName: string;
|
||||
reactAppBuildDirPath: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
urlPathname: string;
|
||||
//If urlOrigin is not undefined then it means --externals-assets
|
||||
urlOrigin: undefined | string;
|
||||
}
|
||||
) {
|
||||
|
||||
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname, urlOrigin } = params;
|
||||
|
||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
||||
|
||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
transformCodebase({
|
||||
"destDirPath":
|
||||
urlOrigin === undefined ?
|
||||
pathJoin(themeDirPath, "resources", "build") :
|
||||
reactAppBuildDirPath,
|
||||
"srcDirPath": reactAppBuildDirPath,
|
||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||
|
||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||
if (
|
||||
urlOrigin === undefined &&
|
||||
isInside({
|
||||
"dirPath": pathJoin(reactAppBuildDirPath, subDirOfPublicDirBasename),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
|
||||
|
||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode(
|
||||
{ "cssCode": sourceCode.toString("utf8") }
|
||||
);
|
||||
|
||||
allCssGlobalsToDefine = {
|
||||
...allCssGlobalsToDefine,
|
||||
...cssGlobalsToDefine
|
||||
};
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
urlOrigin
|
||||
});
|
||||
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
|
||||
}
|
||||
|
||||
return urlOrigin === undefined ?
|
||||
{ "modifiedSourceCode": sourceCode } :
|
||||
undefined;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||
"indexHtmlCode": fs.readFileSync(
|
||||
pathJoin(reactAppBuildDirPath, "index.html")
|
||||
).toString("utf8"),
|
||||
urlPathname,
|
||||
urlOrigin
|
||||
});
|
||||
|
||||
pageIds.forEach(pageId => {
|
||||
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.mkdirSync(themeDirPath, { "recursive": true });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
{
|
||||
|
||||
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
|
||||
|
||||
downloadAndUnzip({
|
||||
"url": builtinThemesUrl,
|
||||
"destDirPath": tmpDirPath
|
||||
});
|
||||
|
||||
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
|
||||
"destDirPath": themeResourcesDirPath
|
||||
});
|
||||
|
||||
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": themeResourcesDirPath,
|
||||
"destDirPath": pathJoin(
|
||||
reactAppPublicDirPath,
|
||||
resourcesPath
|
||||
)
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(
|
||||
reactAppPublicDirPath,
|
||||
resourcesCommonPath
|
||||
)
|
||||
});
|
||||
|
||||
const keycloakResourcesWithinPublicDirPath =
|
||||
pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename);
|
||||
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
|
||||
Buffer.from([
|
||||
"This is just a test folder that helps develop",
|
||||
"the login and register page without having to yarn build"
|
||||
].join(" "))
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"),
|
||||
Buffer.from("*", "utf8")
|
||||
);
|
||||
|
||||
child_process.execSync(`rm -r ${tmpDirPath}`);
|
||||
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeDirPath, "theme.properties"),
|
||||
Buffer.from("parent=keycloak", "utf8")
|
||||
);
|
||||
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
export * from "./build-keycloak-theme";
|
||||
import { main } from "./build-keycloak-theme";
|
||||
|
||||
if (require.main === module) {
|
||||
|
||||
main();
|
||||
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
|
||||
import * as crypto from "crypto";
|
||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
||||
|
||||
export function replaceImportsFromStaticInJsCode(
|
||||
params: {
|
||||
jsCode: string;
|
||||
urlOrigin: undefined | string;
|
||||
}
|
||||
): { fixedJsCode: string; } {
|
||||
|
||||
const { jsCode, urlOrigin } = params;
|
||||
|
||||
const fixedJsCode = jsCode.replace(
|
||||
/([a-z]+\.[a-z]+)\+"static\//g,
|
||||
(...[, group]) =>
|
||||
urlOrigin === undefined ?
|
||||
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
|
||||
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
|
||||
}
|
||||
|
||||
export function replaceImportsInInlineCssCode(
|
||||
params: {
|
||||
cssCode: string;
|
||||
urlPathname: string;
|
||||
urlOrigin: undefined | string;
|
||||
}
|
||||
): { fixedCssCode: string; } {
|
||||
|
||||
const { cssCode, urlPathname, urlOrigin } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
urlPathname === "/" ?
|
||||
/url\(\/([^/][^)]+)\)/g :
|
||||
new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"),
|
||||
(...[, group]) => `url(${urlOrigin === undefined ?
|
||||
"${url.resourcesPath}/build/" + group :
|
||||
params.urlOrigin + urlPathname + group})`
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
|
||||
}
|
||||
|
||||
export function replaceImportsInCssCode(
|
||||
params: {
|
||||
cssCode: string;
|
||||
}
|
||||
): {
|
||||
fixedCssCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
} {
|
||||
|
||||
const { cssCode } = params;
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? [])
|
||||
.forEach(match =>
|
||||
cssGlobalsToDefine[
|
||||
"url" + crypto
|
||||
.createHash("sha256")
|
||||
.update(match)
|
||||
.digest("hex")
|
||||
.substring(0, 15)
|
||||
] = match
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
fixedCssCode =
|
||||
fixedCssCode.split(cssGlobalsToDefine[cssVariableName])
|
||||
.join(`var(--${cssVariableName})`)
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(
|
||||
params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
urlPathname: string;
|
||||
}
|
||||
): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
|
||||
const { cssGlobalsToDefine, urlPathname } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName => [
|
||||
`--${cssVariableName}:`,
|
||||
cssGlobalsToDefine[cssVariableName]
|
||||
.replace(new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"), "url(${url.resourcesPath}/build/")
|
||||
].join(" "))
|
||||
.map(line => ` ${line};`),
|
||||
"}"
|
||||
].join("\n")
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
96
src/bin/copy-keycloak-resources-to-public.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
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: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "copy-keycloak-resources-to-public",
|
||||
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")
|
||||
);
|
||||
}
|
332
src/bin/eject-page.ts
Normal file
@ -0,0 +1,332 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
||||
import cliSelect from "cli-select";
|
||||
import {
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
const values = THEME_TYPES.filter(themeType => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
|
||||
assert(values.length > 0, "No theme is implemented in this project");
|
||||
|
||||
if (values.length === 1) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
return value;
|
||||
})();
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log("Use `npx keycloakify own` command instead, see documentation");
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
[
|
||||
"You are implementing a Single-Page Account theme.",
|
||||
"The eject-page command isn't applicable in this context"
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
|
||||
console.log(chalk.cyan("Select the page you want to customize:"));
|
||||
|
||||
const templateValue = "Template.tsx (Layout common to every page)";
|
||||
const userProfileFormFieldsValue =
|
||||
"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<
|
||||
| LoginThemePageId
|
||||
| AccountThemePageId
|
||||
| typeof templateValue
|
||||
| typeof userProfileFormFieldsValue
|
||||
| typeof otherPageValue
|
||||
>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return [
|
||||
templateValue,
|
||||
userProfileFormFieldsValue,
|
||||
...LOGIN_THEME_PAGE_IDS,
|
||||
otherPageValue
|
||||
];
|
||||
case "account":
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
}).catch(() => {
|
||||
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}`);
|
||||
|
||||
const componentBasename = (() => {
|
||||
if (pageIdOrComponent === templateValue) {
|
||||
return "Template.tsx";
|
||||
}
|
||||
|
||||
if (pageIdOrComponent === userProfileFormFieldsValue) {
|
||||
return "UserProfileFormFields.tsx";
|
||||
}
|
||||
|
||||
return capitalize(kebabCaseToCamelCase(pageIdOrComponent)).replace(/ftl$/, "tsx");
|
||||
})();
|
||||
|
||||
const pagesOrDot = (() => {
|
||||
if (
|
||||
pageIdOrComponent === templateValue ||
|
||||
pageIdOrComponent === userProfileFormFieldsValue
|
||||
) {
|
||||
return ".";
|
||||
}
|
||||
|
||||
return "pages";
|
||||
})();
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
pagesOrDot,
|
||||
componentBasename
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
console.log(
|
||||
`${pageIdOrComponent} is already ejected, ${pathRelative(
|
||||
process.cwd(),
|
||||
targetFilePath
|
||||
)} already exists`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
pagesOrDot,
|
||||
componentBasename
|
||||
)
|
||||
)
|
||||
.toString("utf8");
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
componentCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: componentCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
if (!fs.existsSync(targetDirPath)) {
|
||||
fs.mkdirSync(targetDirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} copy pasted from the Keycloakify source code into your project`
|
||||
);
|
||||
|
||||
edit_KcPage: {
|
||||
if (
|
||||
pageIdOrComponent !== templateValue &&
|
||||
pageIdOrComponent !== userProfileFormFieldsValue
|
||||
) {
|
||||
break edit_KcPage;
|
||||
}
|
||||
|
||||
const kcAppTsxPath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
"KcPage.tsx"
|
||||
);
|
||||
|
||||
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
|
||||
|
||||
const modifiedKcAppTsxCode = (() => {
|
||||
switch (pageIdOrComponent) {
|
||||
case templateValue:
|
||||
return kcAppTsxCode.replace(
|
||||
`keycloakify/${themeType}/Template`,
|
||||
"./Template"
|
||||
);
|
||||
case userProfileFormFieldsValue:
|
||||
return kcAppTsxCode.replace(
|
||||
`keycloakify/login/UserProfileFormFields`,
|
||||
"./UserProfileFormFields"
|
||||
);
|
||||
}
|
||||
assert<Equals<typeof pageIdOrComponent, never>>(false);
|
||||
})();
|
||||
|
||||
if (kcAppTsxCode === modifiedKcAppTsxCode) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
"Unable to automatically update KcPage.tsx, please update it manually"
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
fs.writeFileSync(kcAppTsxPath, Buffer.from(modifiedKcAppTsxCode, "utf8"));
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), kcAppTsxPath))
|
||||
)} Updated`
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const userProfileFormFieldComponentName = "UserProfileFormFields";
|
||||
|
||||
const componentName = componentBasename.replace(/.tsx$/, "");
|
||||
|
||||
console.log(
|
||||
[
|
||||
``,
|
||||
`You now need to update your page router:`,
|
||||
``,
|
||||
`${chalk.bold(
|
||||
pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), buildContext.themeSrcDirPath),
|
||||
themeType,
|
||||
"KcPage.tsx"
|
||||
)
|
||||
)}:`,
|
||||
chalk.grey("```"),
|
||||
`// ...`,
|
||||
``,
|
||||
chalk.green(
|
||||
`+const ${componentName} = lazy(() => import("./pages/${componentName}"));`
|
||||
),
|
||||
...[
|
||||
``,
|
||||
` export default function KcPage(props: { kcContext: KcContext; }) {`,
|
||||
``,
|
||||
` // ...`,
|
||||
``,
|
||||
` return (`,
|
||||
` <Suspense>`,
|
||||
` {(() => {`,
|
||||
` switch (kcContext.pageId) {`,
|
||||
` // ...`,
|
||||
`+ case "${pageIdOrComponent}": return (`,
|
||||
`+ <${componentName}`,
|
||||
`+ {...{ kcContext, i18n, classes }}`,
|
||||
`+ Template={Template}`,
|
||||
`+ doUseDefaultCss={true}`,
|
||||
...(!componentCode.includes(userProfileFormFieldComponentName)
|
||||
? []
|
||||
: [
|
||||
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`,
|
||||
`+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}`
|
||||
]),
|
||||
`+ />`,
|
||||
`+ );`,
|
||||
` default: return <Fallback /* .. */ />;`,
|
||||
` }`,
|
||||
` })()}`,
|
||||
` </Suspense>`,
|
||||
` );`,
|
||||
` }`
|
||||
].map(line => {
|
||||
if (line.startsWith("+")) {
|
||||
return chalk.green(line);
|
||||
}
|
||||
if (line.startsWith("-")) {
|
||||
return chalk.red(line);
|
||||
}
|
||||
return chalk.grey(line);
|
||||
}),
|
||||
chalk.grey("```")
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { crawl } from "./tools/crawl";
|
||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||
import { builtinThemesUrl } from "./install-builtin-keycloak-themes";
|
||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
||||
import * as child_process from "child_process";
|
||||
|
||||
//@ts-ignore
|
||||
const propertiesParser = require("properties-parser");
|
||||
|
||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||
|
||||
child_process.execSync(`rm -rf ${tmpDirPath}`);
|
||||
|
||||
downloadAndUnzip({
|
||||
"destDirPath": tmpDirPath,
|
||||
"url": builtinThemesUrl
|
||||
});
|
||||
|
||||
type Dictionary = { [idiomId: string]: string };
|
||||
|
||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||
|
||||
process.chdir(pathJoin(tmpDirPath, "base"));
|
||||
|
||||
crawl(".").forEach(filePath => {
|
||||
|
||||
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/);
|
||||
|
||||
if (match === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, typeOfPage, language] = match;
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] =
|
||||
Object.fromEntries(
|
||||
Object.entries(
|
||||
propertiesParser.parse(
|
||||
fs.readFileSync(filePath)
|
||||
.toString("utf8")
|
||||
)
|
||||
).map(([key, value]: any) => [key, value.replace(/''/g, "'")])
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
child_process.execSync(`rm -r ${tmpDirPath}`);
|
||||
|
||||
const targetDirPath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_kcMessages");
|
||||
|
||||
fs.mkdirSync(targetDirPath, { "recursive": true });
|
||||
|
||||
Object.keys(record).forEach(pageType => {
|
||||
|
||||
const filePath = pathJoin(targetDirPath, `${pageType}.ts`);
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
|
||||
'//PLEASE DO NOT EDIT MANUALLY',
|
||||
'',
|
||||
'/* spell-checker: disable */',
|
||||
`export const kcMessages= ${JSON.stringify(record[pageType], null, 2)};`,
|
||||
'/* spell-checker: enable */'
|
||||
].join("\n"), "utf8")
|
||||
);
|
||||
|
||||
console.log(`${filePath} wrote`);
|
||||
|
||||
|
||||
});
|
1
src/bin/initialize-account-theme/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./initialize-account-theme";
|
94
src/bin/initialize-account-theme/initialize-account-theme.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import cliSelect from "cli-select";
|
||||
import chalk from "chalk";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import * as fs from "fs";
|
||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||
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: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
const { value: accountThemeType } = await cliSelect({
|
||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
switch (accountThemeType) {
|
||||
case "Multi-Page":
|
||||
{
|
||||
if (
|
||||
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.`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"multi-page-boilerplate"
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
||||
break;
|
||||
case "Single-Page":
|
||||
{
|
||||
const { initializeSpa } = await import("../shared/initializeSpa");
|
||||
|
||||
await initializeSpa({
|
||||
themeType: "account",
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||
|
||||
await updateKcGenCommand({
|
||||
buildContext: {
|
||||
...buildContext,
|
||||
implementedThemeTypes: {
|
||||
...buildContext.implementedThemeTypes,
|
||||
account: {
|
||||
isImplemented: true,
|
||||
type: accountThemeType
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/account";
|
||||
import type { KcEnvName, ThemeName } from "../kc.gen";
|
||||
|
||||
export type KcContextExtension = {
|
||||
themeName: ThemeName;
|
||||
properties: Record<KcEnvName, string> & {};
|
||||
};
|
||||
|
||||
export type KcContextExtensionPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
|