Compare commits
868 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
57b056b388 | |||
9058e9ac9d | |||
ad3de8bff5 | |||
476b100b04 | |||
b2c7c86609 | |||
f8a8ec2e4d | |||
393a5ba125 | |||
466c2d3eb4 | |||
b325b3537f | |||
e429127313 | |||
2d05521789 | |||
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 |
142
.all-contributorsrc
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"contributorsPerLine": 7,
|
||||||
|
"skipCi": true,
|
||||||
|
"repoType": "github",
|
||||||
|
"repoHost": "https://github.com",
|
||||||
|
"projectName": "keycloakify",
|
||||||
|
"projectOwner": "keycloakify"
|
||||||
|
}
|
2
.gitattributes
vendored
@ -1,3 +1,3 @@
|
|||||||
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
||||||
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
src/bin/keycloakify/index.ts -linguist-detectable
|
||||||
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
||||||
|
4
.github/FUNDING.yaml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [garronej]
|
||||||
|
custom: ['https://www.ringerhq.com/experts/garronej']
|
25
.github/release.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- ignore-for-release
|
||||||
|
authors:
|
||||||
|
- octocat
|
||||||
|
categories:
|
||||||
|
- title: Breaking Changes 🛠
|
||||||
|
labels:
|
||||||
|
- breaking
|
||||||
|
- title: Exciting New Features 🎉
|
||||||
|
labels:
|
||||||
|
- feature
|
||||||
|
- title: Fixes 🔧
|
||||||
|
labels:
|
||||||
|
- fix
|
||||||
|
- title: Documentation 🔧
|
||||||
|
labels:
|
||||||
|
- docs
|
||||||
|
- title: CI 👷
|
||||||
|
labels:
|
||||||
|
- ci
|
||||||
|
- title: Other Changes
|
||||||
|
labels:
|
||||||
|
- '*'
|
147
.github/workflows/ci.yaml
vendored
@ -9,125 +9,117 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
test_formatting:
|
test_lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v2.1.3
|
- uses: actions/setup-node@v3
|
||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- name: If this step fails run 'yarn format' then commit again.
|
- name: If this step fails run 'yarn format' then commit again.
|
||||||
run: |
|
run: yarn format:check
|
||||||
PACKAGE_MANAGER=npm
|
|
||||||
if [ -f "./yarn.lock" ]; then
|
|
||||||
PACKAGE_MANAGER=yarn
|
|
||||||
fi
|
|
||||||
$PACKAGE_MANAGER run format:check
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
runs-on: macos-10.15
|
runs-on: ${{ matrix.os }}
|
||||||
needs: test_formatting
|
needs: test_lint
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [ '15', '14', '13' ]
|
node: [ '18' ]
|
||||||
name: Test with Node v${{ matrix.node }}
|
os: [ ubuntu-latest ]
|
||||||
|
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- name: Tell if project is using npm or yarn
|
- uses: actions/checkout@v3
|
||||||
id: step1
|
- uses: actions/setup-node@v3
|
||||||
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
|
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
- run: yarn build
|
||||||
run: |
|
- run: yarn test
|
||||||
yarn build
|
- run: yarn test:keycloakify-starter
|
||||||
yarn test
|
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
storybook:
|
||||||
run: |
|
runs-on: ubuntu-latest
|
||||||
npm run build
|
if: github.event_name == 'push'
|
||||||
npm test
|
needs: test
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: '16'
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
|
- run: yarn build-storybook -o ./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 ./build_storybook -u "github-actions-bot <actions@github.com>"
|
||||||
|
|
||||||
check_if_version_upgraded:
|
check_if_version_upgraded:
|
||||||
name: Check if version upgrade
|
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
|
runs-on: ubuntu-latest
|
||||||
needs: test
|
needs: test
|
||||||
outputs:
|
outputs:
|
||||||
from_version: ${{ steps.step1.outputs.from_version }}
|
from_version: ${{ steps.step1.outputs.from_version }}
|
||||||
to_version: ${{ steps.step1.outputs.to_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:
|
steps:
|
||||||
- uses: garronej/github_actions_toolkit@v2.2
|
- uses: garronej/ts-ci@v2.1.0
|
||||||
id: step1
|
id: step1
|
||||||
with:
|
with:
|
||||||
action_name: is_package_json_version_upgraded
|
action_name: is_package_json_version_upgraded
|
||||||
|
branch: ${{ github.head_ref || github.ref }}
|
||||||
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:
|
create_github_release:
|
||||||
runs-on: ubuntu-latest
|
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:
|
needs:
|
||||||
- update_changelog
|
|
||||||
- check_if_version_upgraded
|
- check_if_version_upgraded
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: softprops/action-gh-release@v1
|
||||||
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
|
|
||||||
with:
|
with:
|
||||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||||
target_commitish: ${{ github.ref }}
|
target_commitish: ${{ github.head_ref || github.ref }}
|
||||||
body: ${{ steps.step1.outputs.body }}
|
generate_release_notes: true
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: false
|
prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
publish_on_npm:
|
publish_on_npm:
|
||||||
runs-on: macos-10.15
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- update_changelog
|
- create_github_release
|
||||||
- check_if_version_upgraded
|
- check_if_version_upgraded
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.3.4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.ref }}
|
ref: ${{ github.ref }}
|
||||||
- uses: actions/setup-node@v2.1.3
|
- uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '15'
|
|
||||||
registry-url: https://registry.npmjs.org/
|
registry-url: https://registry.npmjs.org/
|
||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- run: |
|
- run: yarn build
|
||||||
PACKAGE_MANAGER=npm
|
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
|
||||||
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
|
|
||||||
env:
|
env:
|
||||||
DRY_RUN: "0"
|
DRY_RUN: "0"
|
||||||
|
- uses: garronej/ts-ci@v2.1.0
|
||||||
|
with:
|
||||||
|
action_name: remove_dark_mode_specific_images_from_readme
|
||||||
- name: Publishing on NPM
|
- name: Publishing on NPM
|
||||||
run: |
|
run: |
|
||||||
if [ "$(npm show . version)" = "$VERSION" ]; then
|
if [ "$(npm show . version)" = "$VERSION" ]; then
|
||||||
@ -138,7 +130,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"
|
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
|
false
|
||||||
fi
|
fi
|
||||||
npm publish
|
EXTRA_ARGS=""
|
||||||
|
if [ "$IS_PRE_RELEASE" = "true" ]; then
|
||||||
|
EXTRA_ARGS="--tag next"
|
||||||
|
fi
|
||||||
|
npm publish $EXTRA_ARGS
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
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 }}
|
||||||
|
13
.gitignore
vendored
@ -41,6 +41,17 @@ jspm_packages
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/dist
|
/dist
|
||||||
|
/keycloakify_starter_test/
|
||||||
|
/sample_custom_react_project/
|
||||||
/sample_react_project/
|
/sample_react_project/
|
||||||
/.yarn_home/
|
/.yarn_home/
|
||||||
|
|
||||||
|
.idea
|
||||||
|
|
||||||
|
/src/login/i18n/baseMessages/
|
||||||
|
/src/account/i18n/baseMessages/
|
||||||
|
|
||||||
|
# VS Code devcontainers
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
|
/stories/assets/fonts/
|
||||||
|
@ -3,4 +3,13 @@ node_modules/
|
|||||||
/CHANGELOG.md
|
/CHANGELOG.md
|
||||||
/.yarn_home/
|
/.yarn_home/
|
||||||
/src/test/apps/
|
/src/test/apps/
|
||||||
/src/tools/types/
|
/src/tools/types/
|
||||||
|
/build_keycloak/
|
||||||
|
/.vscode/
|
||||||
|
/src/login/i18n/baseMessages/
|
||||||
|
/src/account/i18n/baseMessages/
|
||||||
|
# Test Build Directories
|
||||||
|
/dist_test
|
||||||
|
/sample_react_project/
|
||||||
|
/sample_custom_react_project/
|
||||||
|
/keycloakify_starter_test/
|
@ -5,7 +5,7 @@
|
|||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"quoteProps": "preserve",
|
"quoteProps": "preserve",
|
||||||
"trailingComma": "all",
|
"trailingComma": "none",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
60
.storybook/DocsContainer.js
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
|
||||||
|
import { useDarkMode } from "storybook-dark-mode";
|
||||||
|
import { darkTheme, lightTheme } from "./customTheme";
|
||||||
|
|
||||||
|
export const DocsContainer = ({ children, context }) => {
|
||||||
|
const isStorybookUiDark = useDarkMode();
|
||||||
|
|
||||||
|
const theme = isStorybookUiDark ? darkTheme : lightTheme;
|
||||||
|
|
||||||
|
const backgroundColor = theme.appBg;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
body {
|
||||||
|
padding: 0 !important,
|
||||||
|
background-color: ${backgroundColor};
|
||||||
|
}
|
||||||
|
|
||||||
|
.docs-story {
|
||||||
|
background-color: ${backgroundColor};
|
||||||
|
}
|
||||||
|
[id^=story--] .container {
|
||||||
|
border: 1px dashed #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(3) {
|
||||||
|
visibility: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
`}</style>
|
||||||
|
<BaseContainer
|
||||||
|
context={{
|
||||||
|
...context,
|
||||||
|
"storyById": id => {
|
||||||
|
const storyContext = context.storyById(id);
|
||||||
|
return {
|
||||||
|
...storyContext,
|
||||||
|
"parameters": {
|
||||||
|
...storyContext?.parameters,
|
||||||
|
"docs": {
|
||||||
|
...storyContext?.parameters?.docs,
|
||||||
|
"theme": isStorybookUiDark ? darkTheme : lightTheme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</BaseContainer>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
35
.storybook/customTheme.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { create } from "@storybook/theming";
|
||||||
|
|
||||||
|
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 = create({
|
||||||
|
"base": "dark",
|
||||||
|
"appBg": "#1E1E1E",
|
||||||
|
"appContentBg": "#161616",
|
||||||
|
"barBg": "#161616",
|
||||||
|
"colorSecondary": "#8585F6",
|
||||||
|
"textColor": "#FFFFFF",
|
||||||
|
brandImage,
|
||||||
|
brandTitle,
|
||||||
|
brandUrl,
|
||||||
|
fontBase,
|
||||||
|
fontCode
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lightTheme = create({
|
||||||
|
"base": "light",
|
||||||
|
"appBg": "#F6F6F6",
|
||||||
|
"appContentBg": "#FFFFFF",
|
||||||
|
"barBg": "#FFFFFF",
|
||||||
|
"colorSecondary": "#000091",
|
||||||
|
"textColor": "#212121",
|
||||||
|
brandImage,
|
||||||
|
brandTitle,
|
||||||
|
brandUrl,
|
||||||
|
fontBase,
|
||||||
|
fontCode
|
||||||
|
});
|
17
.storybook/main.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module.exports = {
|
||||||
|
"stories": [
|
||||||
|
"../stories/*.stories.mdx",
|
||||||
|
"../stories/*.stories.@(ts|tsx)",
|
||||||
|
"../stories/**/*.stories.@(ts|tsx)"
|
||||||
|
],
|
||||||
|
"addons": [
|
||||||
|
"@storybook/addon-links",
|
||||||
|
"@storybook/addon-essentials",
|
||||||
|
"storybook-dark-mode",
|
||||||
|
"@storybook/addon-a11y"
|
||||||
|
],
|
||||||
|
"core": {
|
||||||
|
"builder": "webpack5"
|
||||||
|
},
|
||||||
|
"staticDirs": ["./static"]
|
||||||
|
};
|
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">
|
124
.storybook/preview.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import { darkTheme, lightTheme } from "./customTheme";
|
||||||
|
import { DocsContainer } from "./DocsContainer";
|
||||||
|
|
||||||
|
export const parameters = {
|
||||||
|
"actions": { "argTypesRegex": "^on[A-Z].*" },
|
||||||
|
"controls": {
|
||||||
|
"matchers": {
|
||||||
|
"color": /(background|color)$/i,
|
||||||
|
"date": /Date$/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"backgrounds": { "disable": true },
|
||||||
|
"darkMode": {
|
||||||
|
"light": lightTheme,
|
||||||
|
"dark": darkTheme,
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"container": DocsContainer
|
||||||
|
},
|
||||||
|
"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",
|
||||||
|
//"components/Header",
|
||||||
|
//"components/Footer",
|
||||||
|
"components/Alert",
|
||||||
|
"components/Tabs",
|
||||||
|
"components/Stepper",
|
||||||
|
"components/Button",
|
||||||
|
];
|
||||||
|
|
||||||
|
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 @@
|
|||||||
|
react-dsfr-components.etalab.studio
|
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 |
669
CHANGELOG.md
@ -1,669 +0,0 @@
|
|||||||
### **4.2.4** (2021-11-01)
|
|
||||||
|
|
||||||
- Better autoComplete typings
|
|
||||||
|
|
||||||
### **4.2.3** (2021-11-01)
|
|
||||||
|
|
||||||
- Make it more easy to understand that error in the log are expected
|
|
||||||
|
|
||||||
### **4.2.2** (2021-10-27)
|
|
||||||
|
|
||||||
- Replace 'path' by 'browserify-path' #47
|
|
||||||
|
|
||||||
### **4.2.1** (2021-10-26)
|
|
||||||
|
|
||||||
- useFormValidationSlice: update when params have changed
|
|
||||||
- Explains that the password can't be validated
|
|
||||||
|
|
||||||
## **4.2.0** (2021-10-26)
|
|
||||||
|
|
||||||
- Export types definitions for Attribue and Validator
|
|
||||||
|
|
||||||
## **4.1.0** (2021-10-26)
|
|
||||||
|
|
||||||
- Document what's new in v4
|
|
||||||
|
|
||||||
# **4.0.0** (2021-10-26)
|
|
||||||
|
|
||||||
- fix RegisterUserProfile password confirmation field
|
|
||||||
- Much better support for frontend field validation
|
|
||||||
- Fix css injection order
|
|
||||||
- Makes the download output predictable. This fixes the case where GitHub redirects and wget was trying to download a filename called "15.0.2", and then unzip wouldn't pick it up.
|
|
||||||
Changes wget to curl because curl is awesome. -L is to follow the GitHub redirects.
|
|
||||||
- Remove duplicates
|
|
||||||
|
|
||||||
### **3.0.2** (2021-10-18)
|
|
||||||
|
|
||||||
- Scan deeper to retreive user attribute
|
|
||||||
|
|
||||||
### **3.0.1** (2021-10-17)
|
|
||||||
|
|
||||||
- Add client.description in type kcContext type def
|
|
||||||
|
|
||||||
# **3.0.0** (2021-10-16)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **2.5.3** (2021-10-16)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **2.5.2** (2021-10-13)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **2.5.1** (2021-10-13)
|
|
||||||
|
|
||||||
- Update tss-react
|
|
||||||
|
|
||||||
## **2.5.0** (2021-10-12)
|
|
||||||
|
|
||||||
- register-user-profile.ftl tested working
|
|
||||||
- Make kcMessage more easily hackable
|
|
||||||
- fix useKcMessage
|
|
||||||
- Implement and type validators
|
|
||||||
- Remove syntax error in ftl and make it more directly debugable
|
|
||||||
- Support register-user-profile.ftl
|
|
||||||
|
|
||||||
## **2.4.0** (2021-10-08)
|
|
||||||
|
|
||||||
- #38: Implement messagesPerField existsError and get
|
|
||||||
|
|
||||||
## **2.3.0** (2021-10-07)
|
|
||||||
|
|
||||||
- #20: Support advancedMsg
|
|
||||||
|
|
||||||
## **2.2.0** (2021-10-07)
|
|
||||||
|
|
||||||
- Feat scrip: download-builtin-keycloak-theme for downloading any version of the builtin themes
|
|
||||||
- Use the latest version of keycloak for testing
|
|
||||||
- Test locally with 15.0.2 instead of 11.0.3
|
|
||||||
|
|
||||||
## **2.1.0** (2021-10-06)
|
|
||||||
|
|
||||||
- Support Hungarian and Danish (use Keycloak 15 language resources)
|
|
||||||
|
|
||||||
### **2.0.20** (2021-10-05)
|
|
||||||
|
|
||||||
- Update README.md
|
|
||||||
|
|
||||||
### **2.0.19** (2021-09-17)
|
|
||||||
|
|
||||||
- Fix kcContext type definitions
|
|
||||||
|
|
||||||
### **2.0.18** (2021-09-14)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **2.0.17** (2021-09-14)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **2.0.16** (2021-09-12)
|
|
||||||
|
|
||||||
- Add explaination about errors in logs
|
|
||||||
|
|
||||||
### **2.0.15** (2021-08-31)
|
|
||||||
|
|
||||||
- Update tss-react
|
|
||||||
|
|
||||||
### **2.0.14** (2021-08-20)
|
|
||||||
|
|
||||||
- Update tss-react
|
|
||||||
|
|
||||||
### **2.0.13** (2021-08-04)
|
|
||||||
|
|
||||||
- Merge pull request #28 from marcmrf/main
|
|
||||||
|
|
||||||
fix(mvn): scoped packages compatibility
|
|
||||||
- fix(mvn): scoped packages compatibility
|
|
||||||
|
|
||||||
### **2.0.12** (2021-07-28)
|
|
||||||
|
|
||||||
- Merge pull request #27 from jchn-codes/patch-1
|
|
||||||
|
|
||||||
add maven to requirements
|
|
||||||
- add maven to requirements
|
|
||||||
- Add #bluehats in the keyworks
|
|
||||||
|
|
||||||
### **2.0.11** (2021-07-21)
|
|
||||||
|
|
||||||
- Spaces in file path #22
|
|
||||||
- uptdate dependnecies
|
|
||||||
- Inport specific powerhooks files to reduce bundle size
|
|
||||||
|
|
||||||
### **2.0.10** (2021-07-16)
|
|
||||||
|
|
||||||
- Update dependencies
|
|
||||||
|
|
||||||
### **2.0.9** (2021-07-14)
|
|
||||||
|
|
||||||
- Fix #21
|
|
||||||
|
|
||||||
### **2.0.8** (2021-07-12)
|
|
||||||
|
|
||||||
- Fix previous release
|
|
||||||
- #20: Add def for clientId and name on kcContext.client
|
|
||||||
|
|
||||||
### **2.0.6** (2021-07-08)
|
|
||||||
|
|
||||||
- Merge pull request #18 from asashay/add-custom-props-to-theme-properties
|
|
||||||
|
|
||||||
Add possibility to add custom properties to theme.properties file
|
|
||||||
- add possibility to add custom properties to theme.properties file
|
|
||||||
|
|
||||||
### **2.0.5** (2021-07-05)
|
|
||||||
|
|
||||||
- Fix broken url for big stylesheet #16
|
|
||||||
|
|
||||||
### **2.0.4** (2021-07-03)
|
|
||||||
|
|
||||||
- Fix: #7
|
|
||||||
|
|
||||||
### **2.0.3** (2021-06-30)
|
|
||||||
|
|
||||||
- Escape double quote in ftl to js conversion #15
|
|
||||||
- Update readme
|
|
||||||
|
|
||||||
### **2.0.2** (2021-06-28)
|
|
||||||
|
|
||||||
- Updagte README for implementing non incuded pages
|
|
||||||
|
|
||||||
### **2.0.1** (2021-06-28)
|
|
||||||
|
|
||||||
- Update documentation for v2
|
|
||||||
|
|
||||||
# **2.0.0** (2021-06-28)
|
|
||||||
|
|
||||||
- Fix last bugs before relasing v2
|
|
||||||
- Implement a mechanism to overload kcContext
|
|
||||||
- Give the option in template to pull the default assets or not
|
|
||||||
- Enable possiblity to support custom pages (without forking keycloakify)
|
|
||||||
- Implement a getter for kcContext
|
|
||||||
- Update README.md
|
|
||||||
|
|
||||||
# **2.0.0** (2021-06-28)
|
|
||||||
|
|
||||||
- Fix last bugs before relasing v2
|
|
||||||
- Implement a mechanism to overload kcContext
|
|
||||||
- Give the option in template to pull the default assets or not
|
|
||||||
- Enable possiblity to support custom pages (without forking keycloakify)
|
|
||||||
- Implement a getter for kcContext
|
|
||||||
- Update README.md
|
|
||||||
|
|
||||||
### **1.2.1** (2021-06-22)
|
|
||||||
|
|
||||||
- Remove unessesary log
|
|
||||||
|
|
||||||
## **1.2.0** (2021-06-22)
|
|
||||||
|
|
||||||
- Generate kcContext automatically :rocket:
|
|
||||||
|
|
||||||
### **1.1.6** (2021-06-21)
|
|
||||||
|
|
||||||
- Fix: Alert messages sometimes includes HTML that is not rendered
|
|
||||||
- Update dist
|
|
||||||
|
|
||||||
### **1.1.5** (2021-06-15)
|
|
||||||
|
|
||||||
- #11: Provide socials in the register
|
|
||||||
|
|
||||||
### **1.1.4** (2021-06-15)
|
|
||||||
|
|
||||||
- Merge pull request #12 from InseeFrLab/email-typo
|
|
||||||
|
|
||||||
Fix typo on email
|
|
||||||
- Fix typo on email
|
|
||||||
|
|
||||||
### **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/contributing) that will help you get started.
|
568
README.md
@ -2,475 +2,233 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
<i>🔏 Create Keycloak themes using React 🔏</i>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=develop">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||||
<img src="https://img.shields.io/npm/dw/keycloakify">
|
</a>
|
||||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
<a href="https://www.npmjs.com/package/keycloakify">
|
||||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
<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/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||||
|
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||||
|
</a>
|
||||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||||
</a>
|
</a>
|
||||||
|
<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">Storybook</a>
|
||||||
|
-
|
||||||
|
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>Ultimately this build tool generates a Keycloak theme</i>
|
<i>Ultimately this build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
|
||||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
**NEW in v4**
|
## Sponsor 👼
|
||||||
|
|
||||||
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
|
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), a French company offering Keycloak as a service.
|
||||||
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
|
Their dedicated support helps us continue the development and maintenance of this project.
|
||||||
|
|
||||||
# Motivations
|
[Cloud IAM](https://www.cloud-iam.com/) provides the following services:
|
||||||
|
|
||||||
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.
|
- Perfectly configured and optimized Keycloak IAM, ready in seconds.
|
||||||
It involves, however, a lot of raw JS/CSS/[FTL]() hacking, and bundling the theme is not exactly straightforward.
|
- Custom theme building for your brand using Keycloakify.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>Without keycloakify, users suffers from a harsh context switch, no fronted form pre-validation</i><br>
|
<a href="https://www.cloud-iam.com/">
|
||||||
<img src="https://user-images.githubusercontent.com/6702424/134997335-a28b4a57-0884-47ec-9341-a0e49f835c4d.gif">
|
<img src="https://user-images.githubusercontent.com/6702424/232165752-17134e68-4a55-4d6e-8672-e9132ecac5d5.svg" alt="Cloud IAM Logo" width="200"/>
|
||||||
</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 🍸
|
|
||||||
|
|
||||||
<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)
|
|
||||||
- [Advanced pages configuration](#advanced-pages-configuration)
|
|
||||||
- [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)
|
|
||||||
- [User profile and frontend form validation](#user-profile-and-frontend-form-validation)
|
|
||||||
- [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)
|
|
||||||
- [About the errors related to `objectToJson` in Keycloak logs.](#about-the-errors-related-to-objecttojson-in-keycloak-logs)
|
|
||||||
- [Adding custom message (to `i18n/useKcMessage.tsx`)](#adding-custom-message-to-i18nusekcmessagetsx)
|
|
||||||
- [Email domain whitelist](#email-domain-whitelist)
|
|
||||||
- [Changelog highlights](#changelog-highlights)
|
|
||||||
- [v4](#v4)
|
|
||||||
- [v3](#v3)
|
|
||||||
- [v2.5](#v25)
|
|
||||||
- [v2](#v2)
|
|
||||||
|
|
||||||
# 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)
|
|
||||||
- [15.0.2](https://hub.docker.com/layers/jboss/keycloak/15.0.2/images/sha256-d8ed1ee5df42a178c341f924377da75db49eab08ea9f058ff39a8ed7ee05ec93?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)
|
|
||||||
|
|
||||||
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
|
|
||||||
|
|
||||||
- `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `curl`, `unzip` are assumed to be available.
|
|
||||||
- `docker` must be up and running when running `yarn keycloak`.
|
|
||||||
|
|
||||||
On Windows you'll have to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10).
|
|
||||||
|
|
||||||
## 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 @emotion/react tss-react powerhooks
|
|
||||||
```
|
|
||||||
|
|
||||||
[`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,
|
|
||||||
getKcContext
|
|
||||||
} from "keycloakify";
|
|
||||||
import { css } from "tss-react/@emotion/css";
|
|
||||||
|
|
||||||
const { kcContext } = getKcContext();
|
|
||||||
|
|
||||||
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, getKcContext } from "keycloakify";
|
|
||||||
import { css } from "tss-react/@emotion/css";
|
|
||||||
|
|
||||||
const { kcContext } = getKcContext();
|
|
||||||
|
|
||||||
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">
|
|
||||||
</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:
|
|
||||||
|
|
||||||
<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">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
### Advanced pages configuration
|
|
||||||
|
|
||||||
If you want to go beyond only customizing the CSS you can re-implement some of the
|
|
||||||
pages or even add new ones.
|
|
||||||
|
|
||||||
If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel).
|
|
||||||
If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/app/components/KcApp).
|
|
||||||
The web app is in production [here](https://datalab.sspcloud.fr).
|
|
||||||
|
|
||||||
Main takeaways are:
|
|
||||||
|
|
||||||
- You must declare your custom pages in the package.json. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/package.json#L17-L22)
|
|
||||||
- (TS only) You must declare theses page in the type argument of the getter
|
|
||||||
function for the `kcContext` in order to have the correct typings. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L16-L21)
|
|
||||||
- (TS only) If you use Keycloak plugins that defines non standard `.ftl` values
|
|
||||||
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
|
|
||||||
that define `authorizedMailDomains` in `register.ftl`) you should
|
|
||||||
declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13)
|
|
||||||
- You should provide sample data for all the non standard value if you want to be able
|
|
||||||
to debug the page outside of keycloak. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L28-L43)
|
|
||||||
|
|
||||||
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 you can mock a given `kcContext`:
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
import {
|
|
||||||
KcApp,
|
|
||||||
defaultKcProps,
|
|
||||||
getKcContext
|
|
||||||
} from "keycloakify";
|
|
||||||
|
|
||||||
const { kcContext } = getKcContext({
|
|
||||||
"mockPageId": "login.ftl"
|
|
||||||
});
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
# User profile and frontend form validation
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/keycloakify_fontend_validation.mp4">
|
|
||||||
<img src="https://user-images.githubusercontent.com/6702424/138880146-6fef3280-c4a5-46d2-bbb3-8b9598c057a5.gif">
|
|
||||||
</a>
|
</a>
|
||||||
|
<br/>
|
||||||
|
<i>Use promo code <code>keycloakify</code> </i>
|
||||||
|
<br/>
|
||||||
|
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
User Profile is a Keycloak feature that enables to
|
Thank you, [Cloud IAM](https://www.cloud-iam.com/), for your support!
|
||||||
[define, from the admin console](https://user-images.githubusercontent.com/6702424/136872461-1f5b64ef-d2ef-4c6b-bb8d-07d4729552b3.png),
|
|
||||||
what information you want to collect on your users in the register page and to validate inputs
|
|
||||||
[**on the frontend**, in realtime](https://github.com/InseeFrLab/keycloakify/blob/6dca6a93d8cfe634ee4d8574ad0c091641220092/src/lib/getKcContext/KcContextBase.ts#L225-L261)!
|
|
||||||
|
|
||||||
NOTE: User profile is only available in Keycloak 15 and it's a beta feature that
|
## Contributors ✨
|
||||||
[needs to be enabled when launching keycloak](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/build-keycloak-theme.ts#L116-L117)
|
|
||||||
and [enabled in the console](https://user-images.githubusercontent.com/6702424/136874428-b071d614-c7f7-440d-9b2e-670faadc0871.png).
|
|
||||||
|
|
||||||
Keycloakify, in [`register-user-profile.ftl`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/RegisterUserProfile.tsx),
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
provides frontend validation out of the box.
|
|
||||||
|
|
||||||
For implementing your own `register-user-profile.ftl` page, you can use [`import { useFormValidationSlice } from "keycloakify";`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/useFormValidationSlice.tsx).
|
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||||
Find usage example [`here`](https://github.com/InseeFrLab/keycloakify/blob/d3a07edfcb3739e30032dc96fc2a55944dfc3387/src/lib/components/RegisterUserProfile.tsx#L79-L112).
|
<!-- 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>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
As for right now [it's not possible to define a pattern for the password](https://keycloak.discourse.group/t/make-password-policies-available-to-freemarker/11632)
|
<!-- markdownlint-restore -->
|
||||||
from the admin console. You can however pass validators for it to the `useFormValidationSlice` function.
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
# Support for Terms and conditions
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
[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).
|
## Changelog highlights
|
||||||
|
|
||||||
First you need to enable the required action on the Keycloak server admin console:
|
## 7.0 🍾
|
||||||

|
|
||||||
|
|
||||||
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).
|
- Account theme support 🚀
|
||||||
|
- It's much easier to customize pages at the CSS level, you can now see in the browser dev tool the customizable classes.
|
||||||
|
- New interactive CLI tool `npx eject-keycloak-page`, that enables to select the page you want to customize at the component level.
|
||||||
|
- There is [a Storybook](https://storybook.keycloakify.dev)
|
||||||
|
- [Remember me is fixed](https://github.com/keycloakify/keycloakify/pull/272)
|
||||||
|
|
||||||
# Some pages still have the default theme. Why?
|
## 6.13
|
||||||
|
|
||||||
This project only support out of the box the most common user facing pages of Keycloak login.
|
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
|
||||||
[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 or if you need to implement some non standard `.ftl` pages please refer to [Advanced pages configuration](#advanced-pages-configuration).
|
|
||||||
|
|
||||||
# GitHub Actions
|
## 6.12
|
||||||
|
|
||||||

|
Massive improvement in the developer experience:
|
||||||
|
|
||||||
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
|
||||||
the building and publishing of the theme (the .jar file).
|
- A lot of comments have been added in the code of the starter to make it easier to get started.
|
||||||
|
- The doc has been updated: https://docs.keycloakify.dev
|
||||||
|
- A lot of improvements in the type system.
|
||||||
|
|
||||||
# Limitations
|
## 6.11.4
|
||||||
|
|
||||||
## `process.env.PUBLIC_URL` not supported.
|
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/239).
|
||||||
|
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
|
||||||
|
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
|
||||||
|
|
||||||
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).
|
## 6.10.0
|
||||||
(This isn't recommended anyway).
|
|
||||||
|
|
||||||
## `@font-face` importing fonts from the `src/` dir
|
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
|
||||||
|
|
||||||
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
|
## 6.8.4
|
||||||
this limitation doesn't apply, you can import fonts however you see fit.
|
|
||||||
|
|
||||||
### Example of setup that **won't** work
|
- `@emotion/react` is no longer a peer dependency of Keycloakify.
|
||||||
|
|
||||||
- We have a `fonts/` directory in `src/`
|
## 6.8.0
|
||||||
- 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
|
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
|
||||||
|
individual page (`<Login />`, `<RegisterUserProfile />`, ...) it enables to customize only the header and footer for
|
||||||
|
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/keycloakify/issues/191).
|
||||||
|
|
||||||
- Use [`--external-assets`](#enable-loading-in-a-blink-of-a-eye-of-login-pages-).
|
## 6.7.0
|
||||||
- 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)
|
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/185).
|
||||||
|
|
||||||
If, before logging in, a user has selected a specific language
|
## 6.6.0
|
||||||
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
|
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/184).
|
||||||
to show the login page with light themes.
|
|
||||||
|
|
||||||
The problem is that you are probably using `localStorage` to persist theses values across
|
## 6.5.0
|
||||||
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
|
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/183).
|
||||||
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`).
|
## 6.4.0
|
||||||
|
|
||||||
You can implement your own mechanism to pass the states in the URL and
|
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
|
||||||
restore it on the other side but we recommend using `powerhooks/useGlobalState`
|
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
|
||||||
from the library [`powerhooks`](https://www.powerhooks.dev) that provide an elegant
|
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
|
||||||
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
|
## 6.0.0
|
||||||
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
|
- Bundle size drastically reduced, locals and component dynamically loaded.
|
||||||
import keycloak_js from "keycloak-js";
|
- First print much quicker, use of React.lazy() everywhere.
|
||||||
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
|
- Real i18n API.
|
||||||
import { createKeycloakAdapter } from "keycloakify";
|
- Actual documentation for build options.
|
||||||
|
|
||||||
//...
|
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
|
||||||
|
|
||||||
const keycloakInstance = keycloak_js({
|
## 5.8.0
|
||||||
"url": "http://keycloak-server/auth",
|
|
||||||
"realm": "myrealm",
|
|
||||||
"clientId": "myapp",
|
|
||||||
});
|
|
||||||
|
|
||||||
keycloakInstance.init({
|
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
|
||||||
"onLoad": "check-sso",
|
|
||||||
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html",
|
|
||||||
"adapter": createKeycloakAdapter({
|
|
||||||
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
|
|
||||||
keycloakInstance,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
//...
|
## 5.7.0
|
||||||
```
|
|
||||||
|
|
||||||
If you really want to go the extra miles and avoid having the white
|
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
|
||||||
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
|
## 5.6.4
|
||||||
|
|
||||||
_NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded_
|
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
|
||||||
[](https://youtu.be/xTz0Rj7i2v8)
|
|
||||||
|
|
||||||
# About the errors related to `objectToJson` in Keycloak logs.
|
## v5.6.0
|
||||||
|
|
||||||
The logs of your keycloak server will always show this kind of errors every time a client request a page:
|
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
|
||||||
|
|
||||||
```log
|
## v5.3.0
|
||||||
FTL stack trace ("~" means nesting-related):
|
|
||||||
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 70, column 21]
|
|
||||||
- Reached through: @compress [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 36, column 5]
|
|
||||||
- Reached through: @objectToJson_please_ignore_errors object=value depth=(dep... [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 81, column 27]
|
|
||||||
- Reached through: @compress [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 36, column 5]
|
|
||||||
- Reached through: @objectToJson_please_ignore_errors object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
|
|
||||||
```
|
|
||||||
|
|
||||||
Theses are expected to show up in the log.
|
Rename `keycloak_theme_email` to `keycloak_email`.
|
||||||
Unfortunately, there is nothing I know of that can be done to avoid them or even mute them.
|
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
|
||||||
They can be, however, safely ignored.
|
|
||||||
|
|
||||||
To [converts the `.ftl` values into a JavaScript object](https://github.com/InseeFrLab/keycloakify/blob/main/src/bin/build-keycloak-theme/generateFtl/common.ftl)
|
## v5.0.0
|
||||||
without making assumptions on the `.data_model` we have to do things that throws.
|
|
||||||
It's all-right because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs.
|
|
||||||
|
|
||||||
# Adding custom message (to `i18n/useKcMessage.tsx`)
|
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
|
||||||
|
New i18n system.
|
||||||
|
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
|
||||||
|
|
||||||
You can reproduce [this approach](https://github.com/garronej/keycloakify-demo-app/blob/main/src/kcMessagesExtension.ts)
|
## v4.10.0
|
||||||
( don't forget to [evaluate the code](https://github.com/garronej/keycloakify-demo-app/blob/0a6d349dba89a5702f98ba48bca6c76ac7265e1f/src/index.tsx#L15) ).
|
|
||||||
This approach is a bit hacky as it doesn't provide type safety but it works.
|
|
||||||
|
|
||||||
# Email domain whitelist
|
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
|
||||||
|
|
||||||
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
|
## v4.8.0
|
||||||
and `kcRegisterContext["authorizedMailDomains"]` to validate on.
|
|
||||||
|
|
||||||
# Changelog highlights
|
[Email template customization.](#email-template-customization)
|
||||||
|
|
||||||
|
## v4.7.4
|
||||||
|
|
||||||
|
**M1 Mac** support (for testing locally with a dockerized Keycloak).
|
||||||
|
|
||||||
|
## v4.7.2
|
||||||
|
|
||||||
|
> WARNING: This is broken.
|
||||||
|
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
|
||||||
|
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||||
|
|
||||||
|
## v4.7.0
|
||||||
|
|
||||||
|
Register with user profile enabled: Out of the box `options` validator support.
|
||||||
|
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
|
||||||
|
|
||||||
|
## v4.6.0
|
||||||
|
|
||||||
|
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
|
||||||
|
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
|
||||||
|
|
||||||
|
## v4.5.3
|
||||||
|
|
||||||
|
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
|
||||||
|
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
|
||||||
|
with this new method your theme wont break on minor Keycloakify update.
|
||||||
|
|
||||||
|
## v4.3.0
|
||||||
|
|
||||||
|
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
|
||||||
|
Every time a page is added it's a breaking change for non CSS-only theme.
|
||||||
|
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
|
||||||
|
|
||||||
## v4
|
## v4
|
||||||
|
|
||||||
@ -481,12 +239,12 @@ and `kcRegisterContext["authorizedMailDomains"]` to validate on.
|
|||||||
|
|
||||||
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
||||||
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
||||||
[when passing params from the app to the login page](https://github.com/InseeFrLab/keycloakify#implement-context-persistence-optional).
|
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
|
||||||
|
|
||||||
## v2.5
|
## v2.5
|
||||||
|
|
||||||
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||||
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
||||||
- Test container now uses Keycloak version `15.0.2`.
|
- Test container now uses Keycloak version `15.0.2`.
|
||||||
|
|
||||||
## v2
|
## v2
|
||||||
|
91
keycloakify-json-schema.json
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
{
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "https://json.schemastore.org/package.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "keycloakifyPackageJsonSchema"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$ref": "#/definitions/keycloakifyPackageJsonSchema",
|
||||||
|
"definitions": {
|
||||||
|
"keycloakifyPackageJsonSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"homepage": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"keycloakify": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"extraPages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extraLoginPages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extraAccountPages": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"extraThemeProperties": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"artifactId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"groupId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"bundler": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["mvn", "keycloakify", "none"]
|
||||||
|
},
|
||||||
|
"keycloakVersionDefaultAssets": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"reactAppBuildDirPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"keycloakifyBuildDirPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"customUserAttributes": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"themeName": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "version"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||||
|
}
|
101
package.json
Executable file → Normal file
@ -1,28 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "4.2.4",
|
"version": "7.6.8",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/garronej/keycloakify.git"
|
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||||
},
|
},
|
||||||
"main": "dist/lib/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/lib/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist/",
|
"prepare": "yarn generate-i18n-messages && yarn copy-fonts",
|
||||||
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/tsconfig.json && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
|
||||||
|
"build:watch": "tsc -p src/tsconfig.json && (concurrently \"tsc -p src/tsconfig.json -w\" \"tsc-alias -p src/tsconfig.json\")",
|
||||||
|
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"test": "node dist/test/bin/main && node dist/test/lib",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
"test": "yarn test:types && vitest run",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js",
|
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
|
||||||
"link_in_test_app": "node dist/bin/link_in_test_app.js",
|
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||||
"format": "yarn _format --write",
|
"format": "yarn _format --write",
|
||||||
"format:check": "yarn _format --list-different"
|
"format:check": "yarn _format --list-different",
|
||||||
|
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
|
||||||
|
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
|
||||||
|
"link-in-starter": "yarn link-in-app keycloakify-starter",
|
||||||
|
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w",
|
||||||
|
"storybook": "start-storybook -p 6006",
|
||||||
|
"build-storybook": "build-storybook",
|
||||||
|
"copy-fonts": "copyfiles -u 2 .storybook/static/fonts/**/* stories/assets"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
|
"keycloakify": "dist/bin/keycloakify/index.js",
|
||||||
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
|
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
|
||||||
|
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
|
||||||
|
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,json,md}": [
|
"*.{ts,tsx,json,md}": [
|
||||||
@ -38,10 +49,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"src/",
|
"src/",
|
||||||
"!src/test/",
|
|
||||||
"dist/",
|
"dist/",
|
||||||
"!dist/test/",
|
"!dist/tsconfig.tsbuildinfo",
|
||||||
"!dist/tsconfig.tsbuildinfo"
|
"!dist/bin/tsconfig.tsbuildinfo"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bluehats",
|
"bluehats",
|
||||||
@ -53,35 +63,60 @@
|
|||||||
"login",
|
"login",
|
||||||
"register"
|
"register"
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/garronej/keycloakify",
|
"homepage": "https://www.keycloakify.dev",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
"powerhooks": "^0.10.0",
|
|
||||||
"react": "^16.8.0 || ^17.0.0",
|
|
||||||
"tss-react": "^1.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@babel/core": "^7.0.0",
|
||||||
"@types/node": "^10.0.0",
|
"@storybook/addon-a11y": "^6.5.16",
|
||||||
"@types/react": "^17.0.0",
|
"@storybook/addon-actions": "^6.5.13",
|
||||||
|
"@storybook/addon-essentials": "^6.5.13",
|
||||||
|
"@storybook/addon-interactions": "^6.5.13",
|
||||||
|
"@storybook/addon-links": "^6.5.13",
|
||||||
|
"@storybook/builder-webpack5": "^6.5.13",
|
||||||
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
|
"@storybook/react": "^6.5.13",
|
||||||
|
"@storybook/testing-library": "^0.0.13",
|
||||||
|
"@types/make-fetch-happen": "^10.0.1",
|
||||||
|
"@types/minimist": "^1.2.2",
|
||||||
|
"@types/node": "^18.15.3",
|
||||||
|
"@types/react": "^18.0.35",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@types/yauzl": "^2.10.0",
|
||||||
|
"concurrently": "^7.6.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
|
"eslint-plugin-storybook": "^0.6.7",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"powerhooks": "^0.11.0",
|
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^17.0.1",
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"tss-react": "^1.1.0",
|
"scripting-tools": "^0.19.13",
|
||||||
"typescript": "^4.2.3"
|
"storybook-dark-mode": "^1.1.2",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsc-alias": "^1.8.3",
|
||||||
|
"typescript": "^5.0.4",
|
||||||
|
"vitest": "^0.29.8",
|
||||||
|
"zod-to-json-schema": "^3.20.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@octokit/rest": "^18.12.0",
|
||||||
|
"@types/yazl": "^2.4.2",
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"evt": "2.0.0-beta.38",
|
"cli-select": "^1.1.2",
|
||||||
"minimal-polyfills": "^2.2.1",
|
"evt": "^2.4.18",
|
||||||
|
"make-fetch-happen": "^11.0.3",
|
||||||
|
"minimal-polyfills": "^2.2.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"rfc4648": "^1.5.2",
|
||||||
"tsafe": "^0.8.1"
|
"tsafe": "^1.6.0",
|
||||||
|
"yauzl": "^2.10.0",
|
||||||
|
"yazl": "^2.5.1",
|
||||||
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
renovate.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"baseBranches": ["main", "landingpage"],
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
122
scripts/generate-i18n-messages.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
|
||||||
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
|
||||||
|
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||||
|
import { getCliOptions } from "../src/bin/tools/cliOptions";
|
||||||
|
import { getLogger } from "../src/bin/tools/logger";
|
||||||
|
|
||||||
|
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
|
// update the version array for generating for newer version.
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const propertiesParser = require("properties-parser");
|
||||||
|
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const keycloakVersion = "21.0.1";
|
||||||
|
|
||||||
|
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||||
|
|
||||||
|
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
type Dictionary = { [idiomId: string]: string };
|
||||||
|
|
||||||
|
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
|
{
|
||||||
|
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
||||||
|
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
|
||||||
|
|
||||||
|
crawl(baseThemeDirPath).forEach(filePath => {
|
||||||
|
const match = filePath.match(re);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, typeOfPage, language] = match;
|
||||||
|
|
||||||
|
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||||
|
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
||||||
|
([key, value]: any) => [key, value.replace(/''/g, "'")]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(tmpDirPath, { recursive: true, force: true });
|
||||||
|
|
||||||
|
Object.keys(record).forEach(themeType => {
|
||||||
|
const recordForPageType = record[themeType];
|
||||||
|
|
||||||
|
if (themeType !== "login" && themeType !== "account") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
|
||||||
|
|
||||||
|
const languages = Object.keys(recordForPageType);
|
||||||
|
|
||||||
|
const generatedFileHeader = [
|
||||||
|
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
|
||||||
|
"//PLEASE DO NOT EDIT MANUALLY",
|
||||||
|
""
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
languages.forEach(language => {
|
||||||
|
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
|
||||||
|
|
||||||
|
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
generatedFileHeader,
|
||||||
|
"/* spell-checker: disable */",
|
||||||
|
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||||
|
"",
|
||||||
|
"export default messages;",
|
||||||
|
"/* spell-checker: enable */"
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log(`${filePath} wrote`);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(baseMessagesDirPath, "index.ts"),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
generatedFileHeader,
|
||||||
|
"export async function getMessages(currentLanguageTag: string) {",
|
||||||
|
" const { default: messages } = await (() => {",
|
||||||
|
" switch (currentLanguageTag) {",
|
||||||
|
...languages.map(language => ` case "${language}": return import("./${language}");`),
|
||||||
|
' default: return { "default": {} };',
|
||||||
|
" }",
|
||||||
|
" })();",
|
||||||
|
" return messages;",
|
||||||
|
"}"
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
14
scripts/generate-json-schema.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import zodToJsonSchema from "zod-to-json-schema";
|
||||||
|
import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
|
||||||
|
|
||||||
|
const jsonSchemaName = "keycloakifyPackageJsonSchema";
|
||||||
|
const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
|
||||||
|
|
||||||
|
const baseProperties = {
|
||||||
|
// merges package.json schema with keycloakify properties
|
||||||
|
"allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));
|
143
scripts/link-in-app.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||||
|
|
||||||
|
const rootDirPath = getProjectRoot();
|
||||||
|
|
||||||
|
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(rootDirPath, "dist", "package.json"),
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
(() => {
|
||||||
|
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...packageJsonParsed,
|
||||||
|
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||||
|
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||||
|
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||||
|
"exports": !("exports" in packageJsonParsed)
|
||||||
|
? undefined
|
||||||
|
: Object.fromEntries(
|
||||||
|
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
(value as string).replace(/^\.\/dist\//, "./")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
|
||||||
|
|
||||||
|
const commonThirdPartyDeps = (() => {
|
||||||
|
// For example [ "@emotion" ] it's more convenient than
|
||||||
|
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||||
|
// in singletonDependencies
|
||||||
|
const namespaceSingletonDependencies: string[] = [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...namespaceSingletonDependencies
|
||||||
|
.map(namespaceModuleName =>
|
||||||
|
fs
|
||||||
|
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||||
|
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||||
|
)
|
||||||
|
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||||
|
...singletonDependencies
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
|
||||||
|
|
||||||
|
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||||
|
|
||||||
|
execSync(cmd, {
|
||||||
|
cwd,
|
||||||
|
"env": {
|
||||||
|
...process.env,
|
||||||
|
"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 => execSync("yarn install", { "cwd": testAppPath }));
|
||||||
|
|
||||||
|
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"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export {};
|
29
scripts/test-keycloakify-starter.ts
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const testDir = "keycloakify_starter_test";
|
||||||
|
|
||||||
|
if (existsSync(path.join(process.cwd(), testDir))) {
|
||||||
|
rmSync(path.join(process.cwd(), testDir), { recursive: true });
|
||||||
|
}
|
||||||
|
// Build and link package
|
||||||
|
execSync("yarn build");
|
||||||
|
const pkgJSON = JSON.parse(readFileSync(path.join(process.cwd(), "package.json")).toString("utf8"));
|
||||||
|
pkgJSON.main = "./index.js";
|
||||||
|
pkgJSON.types = "./index.d.ts";
|
||||||
|
pkgJSON.scripts.prepare = undefined;
|
||||||
|
writeFileSync(path.join(process.cwd(), "dist", "package.json"), JSON.stringify(pkgJSON));
|
||||||
|
// Wrapped in a try/catch because unlink errors if the package isn't linked
|
||||||
|
try {
|
||||||
|
execSync("yarn unlink");
|
||||||
|
} catch {}
|
||||||
|
execSync("yarn link", { "cwd": path.join(process.cwd(), "dist") });
|
||||||
|
|
||||||
|
// Clone latest keycloakify-starter and link to keycloakify output
|
||||||
|
execSync(`git clone https://github.com/keycloakify/keycloakify-starter.git ${testDir}`);
|
||||||
|
execSync("yarn install", { "cwd": path.join(process.cwd(), testDir) });
|
||||||
|
execSync("yarn link keycloakify", { "cwd": path.join(process.cwd(), testDir) });
|
||||||
|
|
||||||
|
//Ensure keycloak theme can be built
|
||||||
|
execSync("yarn build-keycloak-theme", { "cwd": path.join(process.cwd(), testDir) });
|
26
src/account/Fallback.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import type { I18n } from "keycloakify/account/i18n";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
|
||||||
|
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||||
|
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||||
|
|
||||||
|
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||||
|
const { kcContext, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
{(() => {
|
||||||
|
switch (kcContext.pageId) {
|
||||||
|
case "password.ftl":
|
||||||
|
return <Password kcContext={kcContext} {...rest} />;
|
||||||
|
case "account.ftl":
|
||||||
|
return <Account kcContext={kcContext} {...rest} />;
|
||||||
|
}
|
||||||
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
|
})()}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
131
src/account/Template.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||||
|
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import type { I18n } from "./i18n";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
|
||||||
|
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||||
|
|
||||||
|
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||||
|
|
||||||
|
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||||
|
|
||||||
|
const { isReady } = usePrepareTemplate({
|
||||||
|
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||||
|
url,
|
||||||
|
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
||||||
|
"styles": ["css/account.css"],
|
||||||
|
"htmlClassName": undefined,
|
||||||
|
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
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">
|
||||||
|
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||||
|
<li>
|
||||||
|
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a href="#" id="kc-current-locale-link">
|
||||||
|
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
{locale.supported.map(({ languageTag }) => (
|
||||||
|
<li key={languageTag} className="kc-dropdown-item">
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||||
|
{labelBySupportedLanguageTag[languageTag]}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{referrer?.url !== undefined && (
|
||||||
|
<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">{message.summary}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
14
src/account/TemplateProps.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import type { I18n } from "./i18n";
|
||||||
|
|
||||||
|
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18nExtended;
|
||||||
|
doUseDefaultCss: boolean;
|
||||||
|
active: string;
|
||||||
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
229
src/account/i18n/i18n.tsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import fallbackMessages from "./baseMessages/en";
|
||||||
|
import { getMessages } from "./baseMessages";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { KcContext } from "../kcContext/KcContext";
|
||||||
|
import { Markdown } from "keycloakify/tools/Markdown";
|
||||||
|
|
||||||
|
export const fallbackLanguageTag = "en";
|
||||||
|
|
||||||
|
export type KcContextLike = {
|
||||||
|
locale?: {
|
||||||
|
currentLanguageTag: string;
|
||||||
|
supported: { languageTag: string; url: string; label: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<KcContext extends KcContextLike ? true : false>();
|
||||||
|
|
||||||
|
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||||
|
|
||||||
|
export type GenericI18n<MessageKey extends string> = {
|
||||||
|
/**
|
||||||
|
* e.g: "en", "fr", "zh-CN"
|
||||||
|
*
|
||||||
|
* The current language
|
||||||
|
*/
|
||||||
|
currentLanguageTag: string;
|
||||||
|
/**
|
||||||
|
* To call when the user switch language.
|
||||||
|
* This will cause the page to be reloaded,
|
||||||
|
* on next load currentLanguageTag === newLanguageTag
|
||||||
|
*/
|
||||||
|
changeLocale: (newLanguageTag: string) => never;
|
||||||
|
/**
|
||||||
|
* e.g. "en" => "English", "fr" => "Français", ...
|
||||||
|
*
|
||||||
|
* Used to render a select that enable user to switch language.
|
||||||
|
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||||
|
* */
|
||||||
|
labelBySupportedLanguageTag: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
*
|
||||||
|
* msg("access-denied") === <span>Access denied</span>
|
||||||
|
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
|
||||||
|
*/
|
||||||
|
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
|
||||||
|
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
|
||||||
|
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
||||||
|
*/
|
||||||
|
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||||
|
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
||||||
|
*/
|
||||||
|
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||||
|
*/
|
||||||
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type I18n = GenericI18n<MessageKey>;
|
||||||
|
|
||||||
|
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||||
|
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||||
|
}) {
|
||||||
|
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
||||||
|
const { kcContext } = params;
|
||||||
|
|
||||||
|
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||||
|
|
||||||
|
const refHasStartedFetching = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refHasStartedFetching.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refHasStartedFetching.current = true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||||
|
|
||||||
|
setI18n({
|
||||||
|
...createI18nTranslationFunctions({
|
||||||
|
"fallbackMessages": {
|
||||||
|
...fallbackMessages,
|
||||||
|
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||||
|
} as any,
|
||||||
|
"messages": {
|
||||||
|
...(await getMessages(currentLanguageTag)),
|
||||||
|
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[currentLanguageTag] ?? {})
|
||||||
|
} as any
|
||||||
|
}),
|
||||||
|
currentLanguageTag,
|
||||||
|
"changeLocale": newLanguageTag => {
|
||||||
|
const { locale } = kcContext;
|
||||||
|
|
||||||
|
assert(locale !== undefined, "Internationalization not enabled");
|
||||||
|
|
||||||
|
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||||
|
|
||||||
|
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||||
|
|
||||||
|
window.location.href = targetSupportedLocale.url;
|
||||||
|
|
||||||
|
assert(false, "never");
|
||||||
|
},
|
||||||
|
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||||
|
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return i18n ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { useI18n };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||||
|
fallbackMessages: Record<MessageKey, string>;
|
||||||
|
messages: Record<MessageKey, string>;
|
||||||
|
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
|
const { fallbackMessages, messages } = params;
|
||||||
|
|
||||||
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||||
|
|
||||||
|
if (messageOrUndefined === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = messageOrUndefined;
|
||||||
|
|
||||||
|
const messageWithArgsInjectedIfAny = (() => {
|
||||||
|
const startIndex = message
|
||||||
|
.match(/{[0-9]+}/g)
|
||||||
|
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||||
|
.map(indexStr => parseInt(indexStr))
|
||||||
|
.sort((a, b) => a - b)[0];
|
||||||
|
|
||||||
|
if (startIndex === undefined) {
|
||||||
|
// No {0} in message (no arguments expected)
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageWithArgsInjected = message;
|
||||||
|
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (arg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageWithArgsInjected;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return doRenderMarkdown ? (
|
||||||
|
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||||
|
{messageWithArgsInjectedIfAny}
|
||||||
|
</Markdown>
|
||||||
|
) : (
|
||||||
|
messageWithArgsInjectedIfAny
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||||
|
|
||||||
|
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||||
|
|
||||||
|
const out = resolveMsg({
|
||||||
|
"key": keyUnwrappedFromCurlyBraces,
|
||||||
|
args,
|
||||||
|
doRenderMarkdown
|
||||||
|
});
|
||||||
|
|
||||||
|
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
||||||
|
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages = {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
/* spell-checker: disable */
|
||||||
|
"shouldBeEqual": "{0} doit être égal à {1}",
|
||||||
|
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||||
|
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Doit être un nombre entier",
|
||||||
|
"notAValidOption": "N'est pas une option valide",
|
||||||
|
|
||||||
|
"logoutConfirmTitle": "Déconnexion",
|
||||||
|
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||||
|
"doLogout": "Se déconnecter"
|
||||||
|
/* spell-checker: enable */
|
||||||
|
}
|
||||||
|
};
|
1
src/account/i18n/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type { I18n } from "./i18n";
|
9
src/account/index.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import Fallback from "keycloakify/account/Fallback";
|
||||||
|
|
||||||
|
export default Fallback;
|
||||||
|
|
||||||
|
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||||
|
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
|
||||||
|
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||||
|
|
||||||
|
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
85
src/account/kcContext/KcContext.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
|
export type KcContext = KcContext.Password | KcContext.Account;
|
||||||
|
|
||||||
|
export declare namespace KcContext {
|
||||||
|
export type Common = {
|
||||||
|
keycloakifyVersion: 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;
|
||||||
|
resourceUrl: string;
|
||||||
|
resourcesCommonPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
getLogoutUrl: () => string;
|
||||||
|
};
|
||||||
|
features: {
|
||||||
|
passwordUpdateSupported: boolean;
|
||||||
|
identityFederation: boolean;
|
||||||
|
log: boolean;
|
||||||
|
authorization: boolean;
|
||||||
|
};
|
||||||
|
realm: {
|
||||||
|
internationalizationEnabled: boolean;
|
||||||
|
userManagedAccessAllowed: boolean;
|
||||||
|
};
|
||||||
|
message?: {
|
||||||
|
type: "success" | "warning" | "error" | "info";
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
referrer?: {
|
||||||
|
url?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
messagesPerField: {
|
||||||
|
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
|
||||||
|
existsError: (fieldName: string) => boolean;
|
||||||
|
get: (fieldName: string) => string;
|
||||||
|
exists: (fieldName: string) => boolean;
|
||||||
|
};
|
||||||
|
account: {
|
||||||
|
email?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Password = Common & {
|
||||||
|
pageId: "password.ftl";
|
||||||
|
password: {
|
||||||
|
passwordSet: boolean;
|
||||||
|
};
|
||||||
|
stateChecker: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Account = Common & {
|
||||||
|
pageId: "account.ftl";
|
||||||
|
url: {
|
||||||
|
referrerURI: string;
|
||||||
|
accountUrl: string;
|
||||||
|
};
|
||||||
|
realm: {
|
||||||
|
registrationEmailAsUsername: boolean;
|
||||||
|
editUsernameAllowed: boolean;
|
||||||
|
};
|
||||||
|
stateChecker: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert<Equals<KcContext["pageId"], AccountThemePageId>>();
|
110
src/account/kcContext/createGetKcContext.ts
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
|
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||||
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
|
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||||
|
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
||||||
|
|
||||||
|
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
|
}) {
|
||||||
|
const { mockData } = params ?? {};
|
||||||
|
|
||||||
|
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||||
|
mockPageId?: PageId;
|
||||||
|
storyParams?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||||
|
}): {
|
||||||
|
kcContext: PageId extends undefined
|
||||||
|
? ExtendKcContext<KcContextExtension> | undefined
|
||||||
|
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
|
||||||
|
} {
|
||||||
|
const { mockPageId, storyParams } = params ?? {};
|
||||||
|
|
||||||
|
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||||
|
|
||||||
|
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||||
|
//TODO maybe trow if no mock fo custom page
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||||
|
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||||
|
].join(" "),
|
||||||
|
"background: red; color: yellow; font-size: medium"
|
||||||
|
);
|
||||||
|
|
||||||
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
const partialKcContextCustomMock = (() => {
|
||||||
|
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||||
|
|
||||||
|
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (mockDataPick !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": mockDataPick
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storyParams !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": out,
|
||||||
|
"source": storyParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(out).length === 0 ? undefined : out;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
|
||||||
|
console.warn(
|
||||||
|
[
|
||||||
|
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||||
|
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||||
|
`Please check the documentation of the getKcContext function`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kcContext: any = {};
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realKcContext === undefined) {
|
||||||
|
return { "kcContext": undefined as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "kcContext": realKcContext as any };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getKcContext };
|
||||||
|
}
|
19
src/account/kcContext/getKcContext.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
|
import { createGetKcContext } from "./createGetKcContext";
|
||||||
|
|
||||||
|
/** @deprecated: Use createGetKcContext instead */
|
||||||
|
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
|
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||||
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
|
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||||
|
const { mockPageId, mockData } = params ?? {};
|
||||||
|
|
||||||
|
const { getKcContext } = createGetKcContext({
|
||||||
|
mockData
|
||||||
|
});
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({ mockPageId });
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
}
|
11
src/account/kcContext/getKcContextFromWindow.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||||
|
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
|
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||||
|
? KcContext
|
||||||
|
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
|
||||||
|
|
||||||
|
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
|
||||||
|
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
|
||||||
|
}
|
1
src/account/kcContext/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type { KcContext } from "./KcContext";
|
176
src/account/kcContext/kcContextMocks.ts
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
|
export const kcContextCommonMock: KcContext.Common = {
|
||||||
|
"keycloakifyVersion": "0.0.0",
|
||||||
|
"url": {
|
||||||
|
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
|
||||||
|
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
|
||||||
|
"resourceUrl": "#",
|
||||||
|
"accountUrl": "#",
|
||||||
|
"applicationsUrl": "#",
|
||||||
|
"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 */
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||||
|
"label": "Deutsch",
|
||||||
|
"languageTag": "de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||||
|
"label": "Norsk",
|
||||||
|
"languageTag": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||||
|
"label": "Русский",
|
||||||
|
"languageTag": "ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||||
|
"label": "Svenska",
|
||||||
|
"languageTag": "sv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||||
|
"label": "Português (Brasil)",
|
||||||
|
"languageTag": "pt-BR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||||
|
"label": "Lietuvių",
|
||||||
|
"languageTag": "lt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||||
|
"label": "English",
|
||||||
|
"languageTag": "en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||||
|
"label": "Italiano",
|
||||||
|
"languageTag": "it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||||
|
"label": "Français",
|
||||||
|
"languageTag": "fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||||
|
"label": "中文简体",
|
||||||
|
"languageTag": "zh-CN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||||
|
"label": "Español",
|
||||||
|
"languageTag": "es"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||||
|
"label": "Čeština",
|
||||||
|
"languageTag": "cs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||||
|
"label": "日本語",
|
||||||
|
"languageTag": "ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||||
|
"label": "Slovenčina",
|
||||||
|
"languageTag": "sk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||||
|
"label": "Polski",
|
||||||
|
"languageTag": "pl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||||
|
"label": "Català",
|
||||||
|
"languageTag": "ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||||
|
"label": "Nederlands",
|
||||||
|
"languageTag": "nl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||||
|
"label": "Türkçe",
|
||||||
|
"languageTag": "tr"
|
||||||
|
}
|
||||||
|
/* spell-checker: enable */
|
||||||
|
],
|
||||||
|
"currentLanguageTag": "en"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "success",
|
||||||
|
"summary": "This is a test message"
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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": ""
|
||||||
|
})
|
||||||
|
];
|
12
src/account/lib/useGetClassName.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
||||||
|
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
|
|
||||||
|
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||||
|
"defaultClasses": {
|
||||||
|
"kcBodyClass": undefined,
|
||||||
|
"kcButtonClass": "btn",
|
||||||
|
"kcButtonPrimaryClass": "btn-primary",
|
||||||
|
"kcButtonLargeClass": "btn-lg",
|
||||||
|
"kcButtonDefaultClass": "btn-default"
|
||||||
|
}
|
||||||
|
});
|
133
src/account/pages/Account.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
"classes": {
|
||||||
|
...classes,
|
||||||
|
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, realm, messagesPerField, stateChecker, account } = 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}
|
||||||
|
value={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 value={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" value={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" value={account.lastName ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||||
|
<div>
|
||||||
|
{url.referrerURI !== undefined && <a href={url.referrerURI}>${msg("backToApplication")}</a>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Save"
|
||||||
|
>
|
||||||
|
{msg("doSave")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonDefaultClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Cancel"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
11
src/account/pages/PageProps.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { LazyExoticComponent } from "react";
|
||||||
|
import type { I18n } from "keycloakify/account/i18n";
|
||||||
|
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
|
|
||||||
|
export type PageProps<KcContext, I18nExtended extends I18n> = {
|
||||||
|
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18nExtended;
|
||||||
|
doUseDefaultCss: boolean;
|
||||||
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
|
};
|
105
src/account/pages/Password.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
"classes": {
|
||||||
|
...classes,
|
||||||
|
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, password, account, stateChecker } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, 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" />
|
||||||
|
</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" />
|
||||||
|
</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" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Save"
|
||||||
|
>
|
||||||
|
{msg("doSave")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
export const keycloakVersions = ["11.0.3", "15.0.2"] as const;
|
|
||||||
|
|
||||||
export type KeycloakVersion = typeof keycloakVersions[number];
|
|
@ -1,132 +0,0 @@
|
|||||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
|
||||||
import { generateJavaStackFiles } 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";
|
|
||||||
|
|
||||||
type ParsedPackageJson = {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
homepage?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
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");
|
|
||||||
|
|
||||||
function sanitizeThemeName(name: string) {
|
|
||||||
return name
|
|
||||||
.replace(/^@(.*)/, "$1")
|
|
||||||
.split("/")
|
|
||||||
.join("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function main() {
|
|
||||||
console.log("🔏 Building the keycloak theme...⌚");
|
|
||||||
|
|
||||||
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
|
|
||||||
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
|
|
||||||
const themeName = sanitizeThemeName(parsedPackageJson.name);
|
|
||||||
|
|
||||||
generateKeycloakThemeResources({
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
|
||||||
themeName,
|
|
||||||
...(() => {
|
|
||||||
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;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
//We have to leave it at that otherwise we break our default theme.
|
|
||||||
//Problem is that we can't guarantee that the the old resources common
|
|
||||||
//will still be available on the newer keycloak version.
|
|
||||||
"keycloakVersion": "11.0.3",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
|
||||||
version: parsedPackageJson.version,
|
|
||||||
themeName,
|
|
||||||
homepage: parsedPackageJson.homepage,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.execSync("mvn package", {
|
|
||||||
"cwd": keycloakThemeBuildingDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
generateDebugFiles({
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
themeName,
|
|
||||||
"keycloakVersion": "15.0.2",
|
|
||||||
});
|
|
||||||
|
|
||||||
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.`,
|
|
||||||
"",
|
|
||||||
"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",
|
|
||||||
" extraEnv: |",
|
|
||||||
" - name: KEYCLOAK_USER",
|
|
||||||
" value: admin",
|
|
||||||
" - name: KEYCLOAK_PASSWORD",
|
|
||||||
" value: xxxxxxxxx",
|
|
||||||
" - name: JAVA_OPTS",
|
|
||||||
" value: -Dkeycloak.profile=preview",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"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 ${themeName}.`,
|
|
||||||
`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,84 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, dirname as pathDirname } 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: { keycloakVersion: "11.0.3" | "15.0.2"; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
|
||||||
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
`FROM jboss/keycloak:${keycloakVersion}`,
|
|
||||||
"",
|
|
||||||
"USER root",
|
|
||||||
"",
|
|
||||||
"WORKDIR /",
|
|
||||||
"",
|
|
||||||
"ADD configuration /opt/jboss/keycloak/standalone/configuration/",
|
|
||||||
"",
|
|
||||||
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
|
|
||||||
].join("\n"),
|
|
||||||
"utf8",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const dockerImage = `${themeName}/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 \\",
|
|
||||||
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
|
|
||||||
` -v ${pathJoin(
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName,
|
|
||||||
)}:/opt/jboss/keycloak/themes/${themeName}: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, `standalone-ha_${keycloakVersion}.xml`))
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(
|
|
||||||
new RegExp(
|
|
||||||
["<staticMaxAge>2592000</staticMaxAge>", "<cacheThemes>true</cacheThemes>", "<cacheTemplates>true</cacheTemplates>"].join("\\s*"),
|
|
||||||
"g",
|
|
||||||
),
|
|
||||||
["<staticMaxAge>-1</staticMaxAge>", "<cacheThemes>false</cacheThemes>", "<cacheTemplates>false</cacheTemplates>"].join("\n"),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
@ -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>2592000</staticMaxAge>
|
|
||||||
<cacheThemes>true</cacheThemes>
|
|
||||||
<cacheTemplates>true</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,693 +0,0 @@
|
|||||||
<?xml version='1.0' encoding='UTF-8'?>
|
|
||||||
|
|
||||||
<server xmlns="urn:jboss:domain:16.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.health"/>
|
|
||||||
<extension module="org.wildfly.extension.io"/>
|
|
||||||
<extension module="org.wildfly.extension.metrics"/>
|
|
||||||
<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>
|
|
||||||
<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:6.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-termination-period="0" 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-termination-period="0" 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:9.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 cluster="ejb" connectors="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:13.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 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>
|
|
||||||
<tls>
|
|
||||||
<key-stores>
|
|
||||||
<key-store name="applicationKS">
|
|
||||||
<credential-reference clear-text="password"/>
|
|
||||||
<implementation type="JKS"/>
|
|
||||||
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
|
|
||||||
</key-store>
|
|
||||||
</key-stores>
|
|
||||||
<key-managers>
|
|
||||||
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
|
|
||||||
<credential-reference clear-text="password"/>
|
|
||||||
</key-manager>
|
|
||||||
</key-managers>
|
|
||||||
<server-ssl-contexts>
|
|
||||||
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
|
|
||||||
</server-ssl-contexts>
|
|
||||||
</tls>
|
|
||||||
</subsystem>
|
|
||||||
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
|
|
||||||
<subsystem xmlns="urn:jboss:domain:infinispan:12.0">
|
|
||||||
<cache-container name="ejb" default-cache="dist" aliases="sfsb" modules="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="keycloak" modules="org.keycloak.keycloak-model-infinispan">
|
|
||||||
<transport lock-timeout="60000"/>
|
|
||||||
<local-cache name="realms">
|
|
||||||
<heap-memory size="10000"/>
|
|
||||||
</local-cache>
|
|
||||||
<local-cache name="users">
|
|
||||||
<heap-memory size="10000"/>
|
|
||||||
</local-cache>
|
|
||||||
<local-cache name="authorization">
|
|
||||||
<heap-memory size="10000"/>
|
|
||||||
</local-cache>
|
|
||||||
<local-cache name="keys">
|
|
||||||
<heap-memory size="1000"/>
|
|
||||||
<expiration max-idle="3600000"/>
|
|
||||||
</local-cache>
|
|
||||||
<replicated-cache name="work">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</replicated-cache>
|
|
||||||
<distributed-cache name="sessions" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="authenticationSessions" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="offlineSessions" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="clientSessions" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="offlineClientSessions" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="loginFailures" owners="1">
|
|
||||||
<expiration lifespan="900000000000000000"/>
|
|
||||||
</distributed-cache>
|
|
||||||
<distributed-cache name="actionTokens" owners="2">
|
|
||||||
<heap-memory size="-1"/>
|
|
||||||
<expiration interval="300000" lifespan="900000000000000000" max-idle="-1"/>
|
|
||||||
</distributed-cache>
|
|
||||||
</cache-container>
|
|
||||||
<cache-container name="server" default-cache="default" aliases="singleton cluster" modules="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" modules="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="hibernate" modules="org.infinispan.hibernate-cache">
|
|
||||||
<transport lock-timeout="60000"/>
|
|
||||||
<local-cache name="local-query">
|
|
||||||
<heap-memory size="10000"/>
|
|
||||||
<expiration max-idle="100000"/>
|
|
||||||
</local-cache>
|
|
||||||
<invalidation-cache name="entity">
|
|
||||||
<transaction mode="NON_XA"/>
|
|
||||||
<heap-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-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>2592000</staticMaxAge>
|
|
||||||
<cacheThemes>true</cacheThemes>
|
|
||||||
<cacheTemplates>true</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:metrics:1.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:6.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:12.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" socket-binding="http" redirect-socket="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
|
|
||||||
<https-listener name="https" 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="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.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,208 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
<#macro objectToJson_please_ignore_errors object depth>
|
|
||||||
<@compress>
|
|
||||||
|
|
||||||
<#local isHash = false>
|
|
||||||
<#attempt>
|
|
||||||
<#local isHash = object?is_hash || object?is_hash_ex>
|
|
||||||
<#recover>
|
|
||||||
/* can't evaluate if object is hash */
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
<#if isHash>
|
|
||||||
|
|
||||||
<#local keys = "">
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#local keys = object?keys>
|
|
||||||
<#recover>
|
|
||||||
/* can't list keys of object */
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
{${'\n'}
|
|
||||||
|
|
||||||
<#list keys as key>
|
|
||||||
|
|
||||||
<#if key == "class">
|
|
||||||
/* skipping "class" property of object */
|
|
||||||
<#continue>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
<#local value = "">
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#local value = object[key]>
|
|
||||||
<#recover>
|
|
||||||
/* couldn't dereference ${key} of object */
|
|
||||||
<#continue>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
<#if depth gt 7>
|
|
||||||
/* Avoid calling recustively too many times depth: ${depth}, key: ${key} */
|
|
||||||
<#continue>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
"${key}": <@objectToJson_please_ignore_errors object=value depth=depth+1/>,
|
|
||||||
|
|
||||||
</#list>
|
|
||||||
|
|
||||||
}${'\n'}
|
|
||||||
|
|
||||||
<#return>
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
|
|
||||||
<#local isMethod = "">
|
|
||||||
<#attempt>
|
|
||||||
<#local isMethod = object?is_method>
|
|
||||||
<#recover>
|
|
||||||
/* can't test if object is a method */
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
<#if isMethod>
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<#local isBoolean = "">
|
|
||||||
<#attempt>
|
|
||||||
<#local isBoolean = object?is_boolean>
|
|
||||||
<#recover>
|
|
||||||
/* can't test if object is a boolean */
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
<#if isBoolean>
|
|
||||||
${object?c}
|
|
||||||
<#return>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
|
|
||||||
<#local isEnumerable = "">
|
|
||||||
<#attempt>
|
|
||||||
<#local isEnumerable = object?is_enumerable>
|
|
||||||
<#recover>
|
|
||||||
/* can't test if object is enumerable */
|
|
||||||
undefined
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
<#if isEnumerable>
|
|
||||||
|
|
||||||
[${'\n'}
|
|
||||||
|
|
||||||
<#list object as item>
|
|
||||||
|
|
||||||
<@objectToJson_please_ignore_errors object=item depth=depth+1/>,
|
|
||||||
|
|
||||||
</#list>
|
|
||||||
|
|
||||||
]${'\n'}
|
|
||||||
|
|
||||||
<#return>
|
|
||||||
</#if>
|
|
||||||
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
"${object?replace('"', '\\"')?no_esc}"
|
|
||||||
<#recover>
|
|
||||||
/* couldn't convert into string non hash, non method, non boolean, non enumerable object */
|
|
||||||
undefined;
|
|
||||||
<#return>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
|
|
||||||
</@compress>
|
|
||||||
</#macro>
|
|
||||||
|
|
||||||
(()=>{
|
|
||||||
|
|
||||||
const nonAutomaticallyConvertible = {
|
|
||||||
"messagesPerField": {
|
|
||||||
|
|
||||||
<#assign fieldNames = ["global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm"]>
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list profile.attributes as attribute>
|
|
||||||
<#assign fieldNames += [attribute.name]>
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
"printIfExists": function (fieldName, x) {
|
|
||||||
<#list fieldNames as fieldName>
|
|
||||||
if(fieldName === "${fieldName}" ){
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
}
|
|
||||||
</#list>
|
|
||||||
throw new Error("There is no " + fieldName + " field");
|
|
||||||
},
|
|
||||||
"existsError": function (fieldName) {
|
|
||||||
<#list fieldNames as fieldName>
|
|
||||||
if(fieldName === "${fieldName}" ){
|
|
||||||
<#attempt>
|
|
||||||
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
}
|
|
||||||
</#list>
|
|
||||||
throw new Error("There is no " + fieldName + " field");
|
|
||||||
},
|
|
||||||
"get": function (fieldName) {
|
|
||||||
<#list fieldNames as fieldName>
|
|
||||||
if(fieldName === "${fieldName}" ){
|
|
||||||
<#attempt>
|
|
||||||
<#if messagesPerField.existsError('${fieldName}')>
|
|
||||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
}
|
|
||||||
</#list>
|
|
||||||
throw new Error("There is no " + fieldName + " field");
|
|
||||||
},
|
|
||||||
"exists": function (fieldName) {
|
|
||||||
<#list fieldNames as fieldName>
|
|
||||||
if(fieldName === "${fieldName}" ){
|
|
||||||
<#attempt>
|
|
||||||
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
}
|
|
||||||
</#list>
|
|
||||||
throw new Error("There is no " + fieldName + " field");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"msg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
|
|
||||||
"advancedMsg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }
|
|
||||||
};
|
|
||||||
|
|
||||||
const out = {};
|
|
||||||
|
|
||||||
Object.deepAssign(
|
|
||||||
out,
|
|
||||||
//Removing all the undefined
|
|
||||||
JSON.parse(JSON.stringify(<@objectToJson_please_ignore_errors object=.data_model depth=0 />))
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.deepAssign(
|
|
||||||
out,
|
|
||||||
nonAutomaticallyConvertible
|
|
||||||
);
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
})()
|
|
||||||
</script>
|
|
@ -1,150 +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 "tsafe/objectKeys";
|
|
||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export const pageIds = [
|
|
||||||
"login.ftl",
|
|
||||||
"register.ftl",
|
|
||||||
"register-user-profile.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");
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ftlPlaceholders = {
|
|
||||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl").match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
|
||||||
"<!-- 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>",
|
|
||||||
"",
|
|
||||||
]),
|
|
||||||
"<script>",
|
|
||||||
loadAdjacentFile("Object.deepAssign.js"),
|
|
||||||
"</script>",
|
|
||||||
"<script>",
|
|
||||||
` window.${ftlValuesGlobalName}= Object.assign(`,
|
|
||||||
` {},`,
|
|
||||||
` ${objectKeys(ftlPlaceholders)[0]}`,
|
|
||||||
" );",
|
|
||||||
"</script>",
|
|
||||||
"",
|
|
||||||
pageSpecificCodePlaceholder,
|
|
||||||
"",
|
|
||||||
objectKeys(ftlPlaceholders)[1],
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const partiallyFixedIndexHtmlCode = $.html();
|
|
||||||
|
|
||||||
function generateFtlFilesCode(params: { pageId: string }): {
|
|
||||||
ftlCode: string;
|
|
||||||
} {
|
|
||||||
const { pageId } = params;
|
|
||||||
|
|
||||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
|
||||||
|
|
||||||
let ftlCode = $.html().replace(
|
|
||||||
pageSpecificCodePlaceholder,
|
|
||||||
[
|
|
||||||
"<script>",
|
|
||||||
` Object.deepAssign(`,
|
|
||||||
` window.${ftlValuesGlobalName},`,
|
|
||||||
` { "pageId": "${pageId}" }`,
|
|
||||||
" );",
|
|
||||||
"</script>",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
objectKeys(ftlPlaceholders).forEach(id => (ftlCode = ftlCode.replace(id, ftlPlaceholders[id])));
|
|
||||||
|
|
||||||
return { ftlCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { generateFtlFilesCode };
|
|
||||||
}
|
|
@ -1,140 +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 { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/getKcContext/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;
|
|
||||||
extraPagesId: string[];
|
|
||||||
extraThemeProperties: string[];
|
|
||||||
keycloakVersion: "11.0.3" | "15.0.2";
|
|
||||||
}) {
|
|
||||||
const {
|
|
||||||
themeName,
|
|
||||||
reactAppBuildDirPath,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
keycloakVersion,
|
|
||||||
} = 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, ...extraPagesId].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");
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
"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".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
export * from "./build-keycloak-theme";
|
|
||||||
import { main } from "./build-keycloak-theme";
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
main();
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import * as crypto from "crypto";
|
|
||||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOrigin: undefined | string }): { fixedJsCode: string } {
|
|
||||||
/*
|
|
||||||
NOTE:
|
|
||||||
|
|
||||||
When we have urlOrigin defined it means that
|
|
||||||
we are building with --external-assets
|
|
||||||
so we have to make sur that the fixed js code will run
|
|
||||||
inside and outside keycloak.
|
|
||||||
|
|
||||||
When urlOrigin isn't defined we can assume the fixedJsCode
|
|
||||||
will always run in keycloak context.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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/`,
|
|
||||||
)
|
|
||||||
.replace(/".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/, (...[, group1, group2, group3]) =>
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
|
||||||
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
|
|
||||||
);
|
|
||||||
|
|
||||||
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"),
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,39 +1,51 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
|
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||||
import type { KeycloakVersion } from "./KeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||||
|
|
||||||
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: KeycloakVersion; destDirPath: string }) {
|
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||||
const { keycloakVersion, destDirPath } = params;
|
const { keycloakVersion, destDirPath } = params;
|
||||||
|
|
||||||
for (const ext of ["", "-community"]) {
|
await Promise.all(
|
||||||
downloadAndUnzip({
|
["", "-community"].map(ext =>
|
||||||
"destDirPath": destDirPath,
|
downloadAndUnzip({
|
||||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
"destDirPath": destDirPath,
|
||||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||||
});
|
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
const { keycloakVersion } = await promptKeycloakVersion();
|
||||||
|
|
||||||
|
const destDirPath = pathJoin(
|
||||||
|
readBuildOptions({
|
||||||
|
"isSilent": true,
|
||||||
|
"isExternalAssetsCliParamProvided": false,
|
||||||
|
"projectDirPath": process.cwd()
|
||||||
|
}).keycloakifyBuildDirPath,
|
||||||
|
"src",
|
||||||
|
"main",
|
||||||
|
"resources",
|
||||||
|
"theme"
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
destDirPath,
|
||||||
|
isSilent
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
const keycloakVersion = (() => {
|
main();
|
||||||
const keycloakVersion = process.argv[2] as KeycloakVersion | undefined;
|
|
||||||
|
|
||||||
if (keycloakVersion === undefined) {
|
|
||||||
return "15.0.2";
|
|
||||||
}
|
|
||||||
|
|
||||||
return keycloakVersion;
|
|
||||||
})();
|
|
||||||
|
|
||||||
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
|
|
||||||
|
|
||||||
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
destDirPath,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
69
src/bin/eject-keycloak-page.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { getProjectRoot } from "./tools/getProjectRoot";
|
||||||
|
import cliSelect from "cli-select";
|
||||||
|
import {
|
||||||
|
loginThemePageIds,
|
||||||
|
accountThemePageIds,
|
||||||
|
type LoginThemePageId,
|
||||||
|
type AccountThemePageId,
|
||||||
|
themeTypes,
|
||||||
|
type ThemeType
|
||||||
|
} from "./keycloakify/generateFtl";
|
||||||
|
import { capitalize } from "tsafe/capitalize";
|
||||||
|
import { readFile, writeFile } from "fs/promises";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
|
import { assert, Equals } from "tsafe/assert";
|
||||||
|
import { getThemeSrcDirPath } from "./getSrcDirPath";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
console.log("Select a theme type");
|
||||||
|
|
||||||
|
const { value: themeType } = await cliSelect<ThemeType>({
|
||||||
|
"values": [...themeTypes]
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Aborting");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Select a page you would like to eject");
|
||||||
|
|
||||||
|
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||||
|
"values": (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return [...loginThemePageIds];
|
||||||
|
case "account":
|
||||||
|
return [...accountThemePageIds];
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
|
})()
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Aborting");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
||||||
|
|
||||||
|
const { themeSrcDirPath } = getThemeSrcDirPath({ "projectDirPath": process.cwd() });
|
||||||
|
|
||||||
|
if (themeSrcDirPath === undefined) {
|
||||||
|
throw new Error("Couldn't locate your theme sources");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
|
||||||
|
|
||||||
|
if (existsSync(targetFilePath)) {
|
||||||
|
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
|
||||||
|
|
||||||
|
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
|
||||||
|
})();
|
@ -1,75 +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 { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
|
||||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
|
||||||
import { rm_rf, rm_r } from "./tools/rm";
|
|
||||||
import { keycloakVersions } from "./KeycloakVersion";
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const propertiesParser = require("properties-parser");
|
|
||||||
|
|
||||||
for (const keycloakVersion of keycloakVersions) {
|
|
||||||
console.log({ keycloakVersion });
|
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
|
||||||
|
|
||||||
rm_rf(tmpDirPath);
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
"destDirPath": tmpDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
|
||||||
|
|
||||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
|
||||||
|
|
||||||
{
|
|
||||||
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
|
||||||
|
|
||||||
crawl(baseThemeDirPath).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(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
|
||||||
([key, value]: any) => [key, value.replace(/''/g, "'")],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rm_r(tmpDirPath);
|
|
||||||
|
|
||||||
const targetDirPath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_kcMessages", keycloakVersion);
|
|
||||||
|
|
||||||
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`);
|
|
||||||
});
|
|
||||||
}
|
|
43
src/bin/getSrcDirPath.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { exclude } from "tsafe";
|
||||||
|
import { crawl } from "./tools/crawl";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
|
const themeSrcDirBasename = "keycloak-theme";
|
||||||
|
|
||||||
|
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
|
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||||
|
|
||||||
|
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
|
||||||
|
.map(fileRelativePath => {
|
||||||
|
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||||
|
|
||||||
|
if (split.length !== 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))[0];
|
||||||
|
|
||||||
|
if (themeSrcDirPath === undefined) {
|
||||||
|
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
|
||||||
|
return { "themeSrcDirPath": srcDirPath };
|
||||||
|
}
|
||||||
|
return { "themeSrcDirPath": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { themeSrcDirPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEmailThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
|
||||||
|
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
|
||||||
|
|
||||||
|
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
|
||||||
|
|
||||||
|
return { emailThemeSrcDirPath };
|
||||||
|
}
|
60
src/bin/initialize-email-theme.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
import { getEmailThemeSrcDirPath } from "./getSrcDirPath";
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
|
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({
|
||||||
|
"projectDirPath": process.cwd()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (emailThemeSrcDirPath === undefined) {
|
||||||
|
logger.warn("Couldn't locate your theme source directory");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||||
|
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { keycloakVersion } = await promptKeycloakVersion();
|
||||||
|
|
||||||
|
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||||
|
isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||||
|
"destDirPath": emailThemeSrcDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||||
|
|
||||||
|
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
|
||||||
|
|
||||||
|
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
226
src/bin/keycloakify/BuildOptions.ts
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { parse as urlParse } from "url";
|
||||||
|
import { typeGuard } from "tsafe/typeGuard";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, sep as pathSep } from "path";
|
||||||
|
|
||||||
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
|
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptions {
|
||||||
|
export type Common = {
|
||||||
|
isSilent: boolean;
|
||||||
|
themeVersion: string;
|
||||||
|
themeName: string;
|
||||||
|
extraLoginPages: string[] | undefined;
|
||||||
|
extraAccountPages: string[] | undefined;
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
bundler: Bundler;
|
||||||
|
keycloakVersionDefaultAssets: string;
|
||||||
|
/** Directory of your built react project. Defaults to {cwd}/build */
|
||||||
|
reactAppBuildDirPath: string;
|
||||||
|
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||||
|
keycloakifyBuildDirPath: string;
|
||||||
|
customUserAttributes: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBuildOptions(params: { projectDirPath: string; isExternalAssetsCliParamProvided: boolean; isSilent: boolean }): BuildOptions {
|
||||||
|
const { projectDirPath, isExternalAssetsCliParamProvided, isSilent } = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
|
||||||
|
|
||||||
|
const url = (() => {
|
||||||
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
let url: URL | undefined = undefined;
|
||||||
|
|
||||||
|
if (homepage !== undefined) {
|
||||||
|
url = new URL(homepage);
|
||||||
|
}
|
||||||
|
|
||||||
|
const CNAME = (() => {
|
||||||
|
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
|
||||||
|
|
||||||
|
if (!fs.existsSync(cnameFilePath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (CNAME !== undefined) {
|
||||||
|
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"origin": url.origin,
|
||||||
|
"pathname": (() => {
|
||||||
|
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||||
|
|
||||||
|
return out === "/" ? undefined : out;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const common: BuildOptions.Common = (() => {
|
||||||
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } =
|
||||||
|
keycloakify ?? {};
|
||||||
|
|
||||||
|
const themeName =
|
||||||
|
keycloakify.themeName ??
|
||||||
|
name
|
||||||
|
.replace(/^@(.*)/, "$1")
|
||||||
|
.split("/")
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeName,
|
||||||
|
"bundler": (() => {
|
||||||
|
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
typeGuard<Bundler | undefined>(
|
||||||
|
KEYCLOAKIFY_BUNDLER,
|
||||||
|
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
|
||||||
|
),
|
||||||
|
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||||
|
})(),
|
||||||
|
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||||
|
"groupId": (() => {
|
||||||
|
const fallbackGroupId = `${themeName}.keycloak`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||||
|
groupId ??
|
||||||
|
(!homepage
|
||||||
|
? fallbackGroupId
|
||||||
|
: urlParse(homepage)
|
||||||
|
.host?.replace(/:[0-9]+$/, "")
|
||||||
|
?.split(".")
|
||||||
|
.reverse()
|
||||||
|
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
||||||
|
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
|
||||||
|
extraAccountPages,
|
||||||
|
extraThemeProperties,
|
||||||
|
isSilent,
|
||||||
|
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
||||||
|
"reactAppBuildDirPath": (() => {
|
||||||
|
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||||
|
|
||||||
|
if (reactAppBuildDirPath === undefined) {
|
||||||
|
return pathJoin(projectDirPath, "build");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathSep === "\\") {
|
||||||
|
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||||
|
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return reactAppBuildDirPath;
|
||||||
|
})(),
|
||||||
|
"keycloakifyBuildDirPath": (() => {
|
||||||
|
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||||
|
|
||||||
|
if (keycloakifyBuildDirPath === undefined) {
|
||||||
|
return pathJoin(projectDirPath, "build_keycloak");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathSep === "\\") {
|
||||||
|
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||||
|
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keycloakifyBuildDirPath;
|
||||||
|
})(),
|
||||||
|
"customUserAttributes": keycloakify.customUserAttributes ?? []
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isExternalAssetsCliParamProvided) {
|
||||||
|
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return id<BuildOptions.ExternalAssets.SameDomain>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
url !== undefined,
|
||||||
|
[
|
||||||
|
"Can't compile in external assets mode if we don't know where",
|
||||||
|
"the app will be hosted.",
|
||||||
|
"You should provide a homepage field in the package.json (or create a",
|
||||||
|
"public/CNAME file.",
|
||||||
|
"Alternatively, if your app and the Keycloak server are on the same domain, ",
|
||||||
|
"eg https://example.com is your app and https://example.com/auth is the keycloak",
|
||||||
|
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
|
||||||
|
"in your package.json"
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
return id<BuildOptions.ExternalAssets.DifferentDomains>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": false,
|
||||||
|
"urlOrigin": url.origin,
|
||||||
|
"urlPathname": url.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<BuildOptions.Standalone>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": true,
|
||||||
|
"urlPathname": url?.pathname
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,382 @@
|
|||||||
|
<script>const _=
|
||||||
|
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||||
|
(()=>{
|
||||||
|
|
||||||
|
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||||
|
|
||||||
|
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||||
|
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||||
|
|
||||||
|
out["messagesPerField"]= {
|
||||||
|
<#assign fieldNames = [
|
||||||
|
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
|
||||||
|
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
|
||||||
|
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
|
||||||
|
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
|
||||||
|
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
|
||||||
|
]>
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
|
||||||
|
<#list profile.attributes as attribute>
|
||||||
|
<#if fieldNames?seq_contains(attribute.name)>
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
<#assign fieldNames += [attribute.name]>
|
||||||
|
</#list>
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
"printIfExists": function (fieldName, x) {
|
||||||
|
<#if !messagesPerField?? >
|
||||||
|
return undefined;
|
||||||
|
<#else>
|
||||||
|
<#list fieldNames as fieldName>
|
||||||
|
if(fieldName === "${fieldName}" ){
|
||||||
|
<#attempt>
|
||||||
|
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
|
||||||
|
return <#if messagesPerField.existsError('username', 'password')>x<#else>undefined</#if>;
|
||||||
|
<#else>
|
||||||
|
return <#if messagesPerField.existsError('${fieldName}')>x<#else>undefined</#if>;
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
}
|
||||||
|
</#list>
|
||||||
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
|
},
|
||||||
|
"existsError": function (fieldName) {
|
||||||
|
<#if !messagesPerField?? >
|
||||||
|
return false;
|
||||||
|
<#else>
|
||||||
|
<#list fieldNames as fieldName>
|
||||||
|
if(fieldName === "${fieldName}" ){
|
||||||
|
<#attempt>
|
||||||
|
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
|
||||||
|
return <#if messagesPerField.existsError('username', 'password')>true<#else>false</#if>;
|
||||||
|
<#else>
|
||||||
|
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
}
|
||||||
|
</#list>
|
||||||
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
|
},
|
||||||
|
"get": function (fieldName) {
|
||||||
|
<#if !messagesPerField?? >
|
||||||
|
return '';
|
||||||
|
<#else>
|
||||||
|
<#list fieldNames as fieldName>
|
||||||
|
if(fieldName === "${fieldName}" ){
|
||||||
|
<#attempt>
|
||||||
|
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
|
||||||
|
<#if messagesPerField.existsError('username', 'password')>
|
||||||
|
return 'Invalid username or password.';
|
||||||
|
</#if>
|
||||||
|
<#else>
|
||||||
|
<#if messagesPerField.existsError('${fieldName}')>
|
||||||
|
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||||
|
</#if>
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
}
|
||||||
|
</#list>
|
||||||
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
|
},
|
||||||
|
"exists": function (fieldName) {
|
||||||
|
<#if !messagesPerField?? >
|
||||||
|
return false;
|
||||||
|
<#else>
|
||||||
|
<#list fieldNames as fieldName>
|
||||||
|
if(fieldName === "${fieldName}" ){
|
||||||
|
<#attempt>
|
||||||
|
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
|
||||||
|
return <#if messagesPerField.exists('username') || messagesPerField.exists('password')>true<#else>false</#if>;
|
||||||
|
<#else>
|
||||||
|
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
}
|
||||||
|
</#list>
|
||||||
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
<#if account??>
|
||||||
|
out["url"]["getLogoutUrl"] = function () {
|
||||||
|
<#attempt>
|
||||||
|
return "${url.getLogoutUrl()}";
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
};
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
|
||||||
|
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
|
||||||
|
out["pageId"] = "${pageId}";
|
||||||
|
|
||||||
|
return out;
|
||||||
|
|
||||||
|
})()
|
||||||
|
<#function ftl_object_to_js_code_declaring_an_object object path>
|
||||||
|
|
||||||
|
<#local isHash = "">
|
||||||
|
<#attempt>
|
||||||
|
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#if isHash>
|
||||||
|
|
||||||
|
<#if path?size gt 10>
|
||||||
|
<#return "ABORT: Too many recursive calls">
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local keys = "">
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#local keys = object?keys>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: We can't list keys on this object">
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
|
||||||
|
<#local out_seq = []>
|
||||||
|
|
||||||
|
<#list keys as key>
|
||||||
|
|
||||||
|
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if
|
||||||
|
(
|
||||||
|
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
|
||||||
|
are_same_path(path, ["url"])
|
||||||
|
) || (
|
||||||
|
key == "updateProfileCtx" &&
|
||||||
|
are_same_path(path, [])
|
||||||
|
) || (
|
||||||
|
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||||
|
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||||
|
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||||
|
key == "loginAction" &&
|
||||||
|
are_same_path(path, ["url"]) &&
|
||||||
|
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
|
||||||
|
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||||
|
) || (
|
||||||
|
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
|
||||||
|
are_same_path(path, ["brokerContext"]) &&
|
||||||
|
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||||
|
) || (
|
||||||
|
key == "identityProviderBrokerCtx" &&
|
||||||
|
are_same_path(path, []) &&
|
||||||
|
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||||
|
) || (
|
||||||
|
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||||
|
are_same_path(path, ["realm"])
|
||||||
|
) || (
|
||||||
|
"error.ftl" == pageId &&
|
||||||
|
are_same_path(path, ["realm"]) &&
|
||||||
|
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||||
|
)
|
||||||
|
>
|
||||||
|
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||||
|
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#if !object[key]??>
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
<#recover>
|
||||||
|
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
|
||||||
|
<#continue>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#local propertyValue = "">
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#local propertyValue = object[key]>
|
||||||
|
<#recover>
|
||||||
|
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
|
||||||
|
<#continue>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
|
||||||
|
|
||||||
|
<#if rec_out?starts_with("ABORT:")>
|
||||||
|
|
||||||
|
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||||
|
|
||||||
|
<#if errorMessage != " It's a method" >
|
||||||
|
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
|
||||||
|
|
||||||
|
</#list>
|
||||||
|
|
||||||
|
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local isMethod = "">
|
||||||
|
<#attempt>
|
||||||
|
<#local isMethod = object?is_method>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Can't test if it'sa method.">
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#if isMethod>
|
||||||
|
|
||||||
|
<#if are_same_path(path, ["auth", "showUsername"])>
|
||||||
|
<#attempt>
|
||||||
|
<#return auth.showUsername()?c>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Couldn't evaluate auth.showUsername()">
|
||||||
|
</#attempt>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if are_same_path(path, ["auth", "showResetCredentials"])>
|
||||||
|
<#attempt>
|
||||||
|
<#return auth.showResetCredentials()?c>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
|
||||||
|
</#attempt>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
|
||||||
|
<#attempt>
|
||||||
|
<#return auth.showTryAnotherWayLink()?c>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
|
||||||
|
</#attempt>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#return "ABORT: It's a method">
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local isBoolean = "">
|
||||||
|
<#attempt>
|
||||||
|
<#local isBoolean = object?is_boolean>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Can't test if it's a boolean">
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#if isBoolean>
|
||||||
|
<#return object?c>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local isEnumerable = "">
|
||||||
|
<#attempt>
|
||||||
|
<#local isEnumerable = object?is_enumerable>
|
||||||
|
<#recover>
|
||||||
|
<#return "ABORT: Can't test if it's an enumerable">
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
|
||||||
|
<#if isEnumerable>
|
||||||
|
|
||||||
|
<#local out_seq = []>
|
||||||
|
|
||||||
|
<#local i = 0>
|
||||||
|
|
||||||
|
<#list object as array_item>
|
||||||
|
|
||||||
|
<#if !array_item??>
|
||||||
|
<#local out_seq += ["null,"]>
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
|
||||||
|
|
||||||
|
<#local i = i + 1>
|
||||||
|
|
||||||
|
<#if rec_out?starts_with("ABORT:")>
|
||||||
|
|
||||||
|
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||||
|
|
||||||
|
<#if errorMessage != " It's a method" >
|
||||||
|
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local out_seq += [rec_out + ","]>
|
||||||
|
|
||||||
|
</#list>
|
||||||
|
|
||||||
|
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
|
||||||
|
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#attempt>
|
||||||
|
<#return '"' + object?js_string + '"'>;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
|
||||||
|
|
||||||
|
</#function>
|
||||||
|
<#function are_same_path path searchedPath>
|
||||||
|
|
||||||
|
<#if path?size != searchedPath?size>
|
||||||
|
<#return false>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local i=0>
|
||||||
|
|
||||||
|
<#list path as property>
|
||||||
|
|
||||||
|
<#local searchedProperty=searchedPath[i]>
|
||||||
|
|
||||||
|
<#if searchedProperty?is_string && searchedProperty == "*">
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if searchedProperty?is_string && !property?is_string>
|
||||||
|
<#return false>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if searchedProperty?is_number && !property?is_number>
|
||||||
|
<#return false>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#if searchedProperty?string != property?string>
|
||||||
|
<#return false>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
<#local i+= 1>
|
||||||
|
|
||||||
|
</#list>
|
||||||
|
|
||||||
|
<#return true>
|
||||||
|
|
||||||
|
</#function>
|
||||||
|
</script>
|
175
src/bin/keycloakify/generateFtl/generateFtl.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import cheerio from "cheerio";
|
||||||
|
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
||||||
|
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
|
||||||
|
export const themeTypes = ["login", "account"] as const;
|
||||||
|
|
||||||
|
export type ThemeType = (typeof themeTypes)[number];
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
customUserAttributes: string[];
|
||||||
|
themeVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = Common &
|
||||||
|
CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = Common &
|
||||||
|
CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
|
export function generateFtlFilesCodeFactory(params: {
|
||||||
|
indexHtmlCode: string;
|
||||||
|
//NOTE: Expected to be an empty object if external assets mode is enabled.
|
||||||
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
keycloakifyVersion: string;
|
||||||
|
}) {
|
||||||
|
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(indexHtmlCode);
|
||||||
|
|
||||||
|
fix_imports_statements: {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
break fix_imports_statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("script:not([src])").each((...[, element]) => {
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": $(element).html()!,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedJsCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("style").each((...[, element]) => {
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
"cssCode": $(element).html()!,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
$(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,
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||||
|
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Object.keys(cssGlobalsToDefine).length !== 0) {
|
||||||
|
$("head").prepend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"<style>",
|
||||||
|
generateCssCodeToDefineGlobals({
|
||||||
|
cssGlobalsToDefine,
|
||||||
|
buildOptions
|
||||||
|
}).cssCodeToPrependInHead,
|
||||||
|
"</style>",
|
||||||
|
""
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||||
|
const replaceValueBySearchValue = {
|
||||||
|
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||||
|
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||||
|
.toString("utf8")
|
||||||
|
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
|
||||||
|
.replace(
|
||||||
|
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
|
||||||
|
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
|
||||||
|
)
|
||||||
|
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||||
|
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
|
||||||
|
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||||
|
"<#if scripts??>",
|
||||||
|
" <#list scripts as script>",
|
||||||
|
' <script src="${script}" type="text/javascript"></script>',
|
||||||
|
" </#list>",
|
||||||
|
"</#if>"
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
$("head").prepend(
|
||||||
|
[
|
||||||
|
"<script>",
|
||||||
|
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
objectKeys(replaceValueBySearchValue)[1]
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
const partiallyFixedIndexHtmlCode = $.html();
|
||||||
|
|
||||||
|
function generateFtlFilesCode(params: { pageId: string }): {
|
||||||
|
ftlCode: string;
|
||||||
|
} {
|
||||||
|
const { pageId } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||||
|
|
||||||
|
let ftlCode = $.html();
|
||||||
|
|
||||||
|
Object.entries({
|
||||||
|
...replaceValueBySearchValue,
|
||||||
|
"PAGE_ID_xIgLsPgGId9D8e": pageId
|
||||||
|
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||||
|
|
||||||
|
return { ftlCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { generateFtlFilesCode };
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
export * from "./generateFtl";
|
export * from "./generateFtl";
|
||||||
|
export * from "./pageId";
|
30
src/bin/keycloakify/generateFtl/pageId.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
export const loginThemePageIds = [
|
||||||
|
"login.ftl",
|
||||||
|
"login-username.ftl",
|
||||||
|
"login-password.ftl",
|
||||||
|
"webauthn-authenticate.ftl",
|
||||||
|
"register.ftl",
|
||||||
|
"register-user-profile.ftl",
|
||||||
|
"info.ftl",
|
||||||
|
"error.ftl",
|
||||||
|
"login-reset-password.ftl",
|
||||||
|
"login-verify-email.ftl",
|
||||||
|
"terms.ftl",
|
||||||
|
"login-otp.ftl",
|
||||||
|
"login-update-profile.ftl",
|
||||||
|
"login-update-password.ftl",
|
||||||
|
"login-idp-link-confirm.ftl",
|
||||||
|
"login-idp-link-email.ftl",
|
||||||
|
"login-page-expired.ftl",
|
||||||
|
"login-config-totp.ftl",
|
||||||
|
"logout-confirm.ftl",
|
||||||
|
"update-user-profile.ftl",
|
||||||
|
"idp-review-user-profile.ftl",
|
||||||
|
"update-email.ftl",
|
||||||
|
"select-authenticator.ftl"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
||||||
|
|
||||||
|
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||||
|
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
@ -1,33 +1,40 @@
|
|||||||
import * as url from "url";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import { themeTypes } from "./generateFtl/generateFtl";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
export function generateJavaStackFiles(params: { version: string; themeName: string; homepage?: string; keycloakThemeBuildingDirPath: string }): {
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
groupId: string;
|
||||||
|
artifactId?: string;
|
||||||
|
themeVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateJavaStackFiles(params: {
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
doBundlesEmailTemplate: boolean;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}): {
|
||||||
jarFilePath: string;
|
jarFilePath: string;
|
||||||
} {
|
} {
|
||||||
const { themeName, version, homepage, keycloakThemeBuildingDirPath } = params;
|
const {
|
||||||
|
buildOptions: { groupId, themeName, themeVersion, artifactId },
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
doBundlesEmailTemplate
|
||||||
|
} = params;
|
||||||
|
|
||||||
{
|
{
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
pomFileCode: string;
|
pomFileCode: string;
|
||||||
} {
|
} {
|
||||||
const groupId = (() => {
|
|
||||||
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
(!homepage
|
|
||||||
? fallbackGroupId
|
|
||||||
: url
|
|
||||||
.parse(homepage)
|
|
||||||
.host?.replace(/:[0-9]+$/, "")
|
|
||||||
?.split(".")
|
|
||||||
.reverse()
|
|
||||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const artefactId = `${themeName}-keycloak-theme`;
|
|
||||||
|
|
||||||
const pomFileCode = [
|
const pomFileCode = [
|
||||||
`<?xml version="1.0"?>`,
|
`<?xml version="1.0"?>`,
|
||||||
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
|
||||||
@ -35,11 +42,11 @@ export function generateJavaStackFiles(params: { version: string; themeName: str
|
|||||||
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
||||||
` <modelVersion>4.0.0</modelVersion>`,
|
` <modelVersion>4.0.0</modelVersion>`,
|
||||||
` <groupId>${groupId}</groupId>`,
|
` <groupId>${groupId}</groupId>`,
|
||||||
` <artifactId>${artefactId}</artifactId>`,
|
` <artifactId>${artifactId}</artifactId>`,
|
||||||
` <version>${version}</version>`,
|
` <version>${themeVersion}</version>`,
|
||||||
` <name>${artefactId}</name>`,
|
` <name>${artifactId}</name>`,
|
||||||
` <description />`,
|
` <description />`,
|
||||||
`</project>`,
|
`</project>`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
return { pomFileCode };
|
return { pomFileCode };
|
||||||
@ -63,19 +70,19 @@ export function generateJavaStackFiles(params: { version: string; themeName: str
|
|||||||
"themes": [
|
"themes": [
|
||||||
{
|
{
|
||||||
"name": themeName,
|
"name": themeName,
|
||||||
"types": ["login"],
|
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
),
|
),
|
||||||
"utf8",
|
"utf8"
|
||||||
),
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`),
|
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
|
||||||
};
|
};
|
||||||
}
|
}
|
241
src/bin/keycloakify/generateKeycloakThemeResources.ts
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import { transformCodebase } from "../tools/transformCodebase";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, basename as pathBasename } from "path";
|
||||||
|
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
|
||||||
|
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "./generateFtl";
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
||||||
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
|
||||||
|
import { isInside } from "../tools/isInside";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
themeName: string;
|
||||||
|
extraLoginPages?: string[];
|
||||||
|
extraAccountPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
isSilent: boolean;
|
||||||
|
customUserAttributes: string[];
|
||||||
|
themeVersion: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
|
||||||
|
export async function generateKeycloakThemeResources(params: {
|
||||||
|
reactAppBuildDirPath: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
emailThemeSrcDirPath: string | undefined;
|
||||||
|
keycloakVersion: string;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
keycloakifyVersion: string;
|
||||||
|
}): Promise<{ doBundlesEmailTemplate: boolean }> {
|
||||||
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, keycloakVersion, buildOptions, keycloakifyVersion } = params;
|
||||||
|
|
||||||
|
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
||||||
|
|
||||||
|
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
|
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
|
||||||
|
|
||||||
|
for (const themeType of themeTypes) {
|
||||||
|
const themeDirPath = getThemeDirPath(themeType);
|
||||||
|
|
||||||
|
copy_app_resources_to_theme_path: {
|
||||||
|
const isFirstPass = themeType.indexOf(themeType) === 0;
|
||||||
|
|
||||||
|
if (!isFirstPass && !buildOptions.isStandalone) {
|
||||||
|
break copy_app_resources_to_theme_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||||
|
"srcDirPath": reactAppBuildDirPath,
|
||||||
|
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||||
|
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||||
|
if (
|
||||||
|
buildOptions.isStandalone &&
|
||||||
|
isInside({
|
||||||
|
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.css?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||||
|
"cssCode": sourceCode.toString("utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
register_css_variables: {
|
||||||
|
if (!isFirstPass) {
|
||||||
|
break register_css_variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
allCssGlobalsToDefine = {
|
||||||
|
...allCssGlobalsToDefine,
|
||||||
|
...cssGlobalsToDefine
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.js?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": sourceCode.toString("utf8"),
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateFtlFilesCode = (() => {
|
||||||
|
if (generateFtlFilesCode_glob !== undefined) {
|
||||||
|
return generateFtlFilesCode_glob;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
|
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||||
|
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||||
|
buildOptions,
|
||||||
|
keycloakifyVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
return generateFtlFilesCode;
|
||||||
|
})();
|
||||||
|
|
||||||
|
[
|
||||||
|
...(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return loginThemePageIds;
|
||||||
|
case "account":
|
||||||
|
return accountThemePageIds;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
...((() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return buildOptions.extraLoginPages;
|
||||||
|
case "account":
|
||||||
|
return buildOptions.extraAccountPages;
|
||||||
|
}
|
||||||
|
})() ?? [])
|
||||||
|
].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");
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent: buildOptions.isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||||
|
"destDirPath": themeResourcesDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||||
|
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": themeResourcesDirPath,
|
||||||
|
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
|
||||||
|
});
|
||||||
|
|
||||||
|
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
|
||||||
|
|
||||||
|
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 run a Keycloak container"].join(
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||||
|
fs.rmSync(tmpDirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeDirPath, "theme.properties"),
|
||||||
|
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doBundlesEmailTemplate: boolean;
|
||||||
|
|
||||||
|
email: {
|
||||||
|
if (emailThemeSrcDirPath === undefined) {
|
||||||
|
doBundlesEmailTemplate = false;
|
||||||
|
break email;
|
||||||
|
}
|
||||||
|
|
||||||
|
doBundlesEmailTemplate = true;
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": emailThemeSrcDirPath,
|
||||||
|
"destDirPath": getThemeDirPath("email")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { doBundlesEmailTemplate };
|
||||||
|
}
|
61
src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
|
const containerName = "keycloak-testing-container";
|
||||||
|
|
||||||
|
/** Files for being able to run a hot reload keycloak container */
|
||||||
|
export function generateStartKeycloakTestingContainer(params: {
|
||||||
|
keycloakVersion: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
keycloakVersion,
|
||||||
|
buildOptions: { themeName }
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const keycloakThemePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(/\\/g, "/");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
||||||
|
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"",
|
||||||
|
`docker rm ${containerName} || true`,
|
||||||
|
"",
|
||||||
|
`cd "${keycloakThemeBuildingDirPath.replace(/\\/g, "/")}"`,
|
||||||
|
"",
|
||||||
|
"docker run \\",
|
||||||
|
" -p 8080:8080 \\",
|
||||||
|
` --name ${containerName} \\`,
|
||||||
|
" -e KEYCLOAK_ADMIN=admin \\",
|
||||||
|
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
|
||||||
|
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
|
||||||
|
` -v "${keycloakThemePath}":"/opt/keycloak/themes/${themeName}":rw \\`,
|
||||||
|
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
||||||
|
` start-dev`,
|
||||||
|
""
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
),
|
||||||
|
{ "mode": 0o755 }
|
||||||
|
);
|
||||||
|
}
|
8
src/bin/keycloakify/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
export * from "./keycloakify";
|
||||||
|
import { main } from "./keycloakify";
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
153
src/bin/keycloakify/keycloakify.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||||
|
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||||
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { readBuildOptions } from "./BuildOptions";
|
||||||
|
import { getLogger } from "../tools/logger";
|
||||||
|
import { getCliOptions } from "../tools/cliOptions";
|
||||||
|
import jar from "../tools/jar";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Equals } from "tsafe";
|
||||||
|
import { getEmailThemeSrcDirPath } from "../getSrcDirPath";
|
||||||
|
import { getProjectRoot } from "../tools/getProjectRoot";
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
logger.log("🔏 Building the keycloak theme...⌚");
|
||||||
|
|
||||||
|
const projectDirPath = process.cwd();
|
||||||
|
|
||||||
|
const buildOptions = readBuildOptions({
|
||||||
|
projectDirPath,
|
||||||
|
"isExternalAssetsCliParamProvided": hasExternalAssets,
|
||||||
|
"isSilent": isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
|
||||||
|
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||||
|
"emailThemeSrcDirPath": (() => {
|
||||||
|
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath });
|
||||||
|
|
||||||
|
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailThemeSrcDirPath;
|
||||||
|
})(),
|
||||||
|
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
|
||||||
|
buildOptions,
|
||||||
|
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||||
|
"keycloakifyVersion": (() => {
|
||||||
|
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"];
|
||||||
|
|
||||||
|
assert(typeof version === "string");
|
||||||
|
|
||||||
|
return version;
|
||||||
|
})()
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
|
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||||
|
doBundlesEmailTemplate,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (buildOptions.bundler) {
|
||||||
|
case "none":
|
||||||
|
logger.log("😱 Skipping bundling step, there will be no jar");
|
||||||
|
break;
|
||||||
|
case "keycloakify":
|
||||||
|
logger.log("🫶 Let keycloakify do its thang");
|
||||||
|
await jar({
|
||||||
|
"rootPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"),
|
||||||
|
"version": buildOptions.themeVersion,
|
||||||
|
"groupId": buildOptions.groupId,
|
||||||
|
"artifactId": buildOptions.artifactId,
|
||||||
|
"targetPath": jarFilePath
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mvn":
|
||||||
|
logger.log("🫙 Run maven to deliver a jar");
|
||||||
|
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakifyBuildDirPath });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert<Equals<typeof buildOptions.bundler, never>>(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want, however, to test in a container running the latest Keycloak version
|
||||||
|
const containerKeycloakVersion = "20.0.1";
|
||||||
|
|
||||||
|
generateStartKeycloakTestingContainer({
|
||||||
|
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||||
|
"keycloakVersion": containerKeycloakVersion,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(projectDirPath, jarFilePath)} 🚀`,
|
||||||
|
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
|
||||||
|
"",
|
||||||
|
//TODO: Restore when we find a good Helm chart for Keycloak.
|
||||||
|
//"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/keycloak/providers",
|
||||||
|
" extraEnv: |",
|
||||||
|
" - name: KEYCLOAK_USER",
|
||||||
|
" value: admin",
|
||||||
|
" - name: KEYCLOAK_PASSWORD",
|
||||||
|
" value: xxxxxxxxx",
|
||||||
|
" - name: JAVA_OPTS",
|
||||||
|
" value: -Dkeycloak.profile=preview",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
|
||||||
|
"",
|
||||||
|
`👉 $ .${pathSep}${pathRelative(
|
||||||
|
projectDirPath,
|
||||||
|
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
|
||||||
|
)} 👈`,
|
||||||
|
"",
|
||||||
|
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
|
||||||
|
``,
|
||||||
|
`Once your container is up and running: `,
|
||||||
|
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||||
|
`- Create a realm: myrealm`,
|
||||||
|
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
|
||||||
|
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
|
||||||
|
`- Create a client id myclient`,
|
||||||
|
` Root URL: https://www.keycloak.org/app/`,
|
||||||
|
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
|
||||||
|
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
|
||||||
|
` Web origins: *`,
|
||||||
|
` Login Theme: ${buildOptions.themeName}`,
|
||||||
|
` Save (button at the bottom of the page)`,
|
||||||
|
``,
|
||||||
|
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||||
|
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,
|
||||||
|
``,
|
||||||
|
`Video tutorial: https://youtu.be/WMyGZNHQkjU`,
|
||||||
|
``
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
64
src/bin/keycloakify/parsedPackageJson.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { assert } from "tsafe";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { pathJoin } from "../tools/pathJoin";
|
||||||
|
|
||||||
|
export const bundlers = ["mvn", "keycloakify", "none"] as const;
|
||||||
|
export type Bundler = (typeof bundlers)[number];
|
||||||
|
export type ParsedPackageJson = {
|
||||||
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
homepage?: string;
|
||||||
|
keycloakify?: {
|
||||||
|
/** @deprecated: use extraLoginPages instead */
|
||||||
|
extraPages?: string[];
|
||||||
|
extraLoginPages?: string[];
|
||||||
|
extraAccountPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
||||||
|
artifactId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
bundler?: Bundler;
|
||||||
|
keycloakVersionDefaultAssets?: string;
|
||||||
|
reactAppBuildDirPath?: string;
|
||||||
|
keycloakifyBuildDirPath?: string;
|
||||||
|
customUserAttributes?: string[];
|
||||||
|
themeName?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zParsedPackageJson = z.object({
|
||||||
|
"name": z.string(),
|
||||||
|
"version": z.string().optional(),
|
||||||
|
"homepage": z.string().optional(),
|
||||||
|
"keycloakify": z
|
||||||
|
.object({
|
||||||
|
"extraPages": z.array(z.string()).optional(),
|
||||||
|
"extraLoginPages": z.array(z.string()).optional(),
|
||||||
|
"extraAccountPages": z.array(z.string()).optional(),
|
||||||
|
"extraThemeProperties": z.array(z.string()).optional(),
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
||||||
|
"artifactId": z.string().optional(),
|
||||||
|
"groupId": z.string().optional(),
|
||||||
|
"bundler": z.enum(bundlers).optional(),
|
||||||
|
"keycloakVersionDefaultAssets": z.string().optional(),
|
||||||
|
"reactAppBuildDirPath": z.string().optional(),
|
||||||
|
"keycloakifyBuildDirPath": z.string().optional(),
|
||||||
|
"customUserAttributes": z.array(z.string()).optional(),
|
||||||
|
"themeName": z.string().optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
||||||
|
|
||||||
|
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
|
||||||
|
export function getParsedPackageJson(params: { projectDirPath: string }) {
|
||||||
|
const { projectDirPath } = params;
|
||||||
|
if (parsedPackageJson) {
|
||||||
|
return parsedPackageJson;
|
||||||
|
}
|
||||||
|
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")));
|
||||||
|
return parsedPackageJson;
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
|
||||||
|
/*
|
||||||
|
NOTE:
|
||||||
|
|
||||||
|
When we have urlOrigin defined it means that
|
||||||
|
we are building with --external-assets
|
||||||
|
so we have to make sur that the fixed js code will run
|
||||||
|
inside and outside keycloak.
|
||||||
|
|
||||||
|
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||||
|
will always run in keycloak context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { jsCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||||
|
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||||
|
(...[, n, u, e]) => `
|
||||||
|
${n}[(function(){
|
||||||
|
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||||
|
if( pd === undefined || pd.configurable ){
|
||||||
|
${
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||||
|
set: function (){}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
var p= "";
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
|
||||||
|
set: function (value){ p = value;}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "${u}";
|
||||||
|
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixedJsCode = jsCode
|
||||||
|
.replace(...getReplaceArgs("js"))
|
||||||
|
.replace(...getReplaceArgs("css"))
|
||||||
|
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||||
|
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
|
||||||
|
)
|
||||||
|
//TODO: Write a test case for this
|
||||||
|
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||||
|
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedJsCode };
|
||||||
|
}
|
64
src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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>; buildOptions: BuildOptionsLike }): {
|
||||||
|
cssCodeToPrependInHead: string;
|
||||||
|
} {
|
||||||
|
const { cssGlobalsToDefine, buildOptions } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cssCodeToPrependInHead": [
|
||||||
|
":root {",
|
||||||
|
...Object.keys(cssGlobalsToDefine)
|
||||||
|
.map(cssVariableName =>
|
||||||
|
[
|
||||||
|
`--${cssVariableName}:`,
|
||||||
|
cssGlobalsToDefine[cssVariableName].replace(
|
||||||
|
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||||
|
"url(${url.resourcesPath}/build/"
|
||||||
|
)
|
||||||
|
].join(" ")
|
||||||
|
)
|
||||||
|
.map(line => ` ${line};`),
|
||||||
|
"}"
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||||
|
fixedCssCode: string;
|
||||||
|
} {
|
||||||
|
const { cssCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const fixedCssCode = cssCode.replace(
|
||||||
|
buildOptions.urlPathname === undefined
|
||||||
|
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||||
|
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||||
|
(...[, group]) =>
|
||||||
|
`url(${
|
||||||
|
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode };
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakifyDirPath, "dist", "package.json"),
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify(
|
|
||||||
(() => {
|
|
||||||
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...packageJsonParsed,
|
|
||||||
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
|
|
||||||
"types": packageJsonParsed["types"].replace(/^dist\//, ""),
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
"utf8",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonThirdPartyDeps = (() => {
|
|
||||||
const namespaceModuleNames = ["@emotion"];
|
|
||||||
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"];
|
|
||||||
|
|
||||||
return [
|
|
||||||
...namespaceModuleNames
|
|
||||||
.map(namespaceModuleName =>
|
|
||||||
fs
|
|
||||||
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
|
|
||||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`),
|
|
||||||
)
|
|
||||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
|
||||||
...standaloneModuleNames,
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
|
|
||||||
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
|
|
||||||
|
|
||||||
execSync(["rm -rf", "mkdir"].map(cmd => `${cmd} ${yarnHomeDirPath}`).join(" && "));
|
|
||||||
|
|
||||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
|
||||||
const { targetModuleName, cwd } = params;
|
|
||||||
|
|
||||||
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : [])].join(" ");
|
|
||||||
|
|
||||||
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`);
|
|
||||||
|
|
||||||
execSync(cmd, {
|
|
||||||
cwd,
|
|
||||||
"env": {
|
|
||||||
...process.env,
|
|
||||||
"HOME": yarnHomeDirPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testAppNames = ["keycloakify-demo-app"] as const;
|
|
||||||
|
|
||||||
const getTestAppPath = (testAppName: typeof testAppNames[number]) => pathJoin(keycloakifyDirPath, "..", testAppName);
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName => execSync("yarn install", { "cwd": getTestAppPath(testAppName) }));
|
|
||||||
|
|
||||||
console.log("=== Linking common dependencies ===");
|
|
||||||
|
|
||||||
const total = commonThirdPartyDeps.length;
|
|
||||||
let current = 0;
|
|
||||||
|
|
||||||
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
|
||||||
current++;
|
|
||||||
|
|
||||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
|
||||||
|
|
||||||
const localInstallPath = pathJoin(
|
|
||||||
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])],
|
|
||||||
);
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": localInstallPath });
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": getTestAppPath(testAppName),
|
|
||||||
"targetModuleName": commonThirdPartyDep,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("=== Linking in house dependencies ===");
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": getTestAppPath(testAppName),
|
|
||||||
"targetModuleName": "keycloakify",
|
|
||||||
}),
|
|
||||||
);
|
|
5
src/bin/mockTestingResourcesPath.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { pathJoin } from "./tools/pathJoin";
|
||||||
|
|
||||||
|
export const mockTestingSubDirOfPublicDirBasename = "keycloak_static";
|
||||||
|
export const mockTestingResourcesPath = pathJoin(mockTestingSubDirOfPublicDirBasename, "resources");
|
||||||
|
export const mockTestingResourcesCommonPath = pathJoin(mockTestingResourcesPath, "resources_common");
|
47
src/bin/promptKeycloakVersion.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import cliSelect from "cli-select";
|
||||||
|
|
||||||
|
export async function promptKeycloakVersion() {
|
||||||
|
const { getLatestsSemVersionedTag } = (() => {
|
||||||
|
const { octokit } = (() => {
|
||||||
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
|
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
|
||||||
|
|
||||||
|
return { octokit };
|
||||||
|
})();
|
||||||
|
|
||||||
|
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
|
||||||
|
|
||||||
|
return { getLatestsSemVersionedTag };
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log("Initialize the directory with email template from which keycloak version?");
|
||||||
|
|
||||||
|
const tags = [
|
||||||
|
...(await getLatestsSemVersionedTag({
|
||||||
|
"count": 10,
|
||||||
|
"doIgnoreBeta": true,
|
||||||
|
"owner": "keycloak",
|
||||||
|
"repo": "keycloak"
|
||||||
|
}).then(arr => arr.map(({ tag }) => tag))),
|
||||||
|
"11.0.3"
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.env["GITHUB_ACTIONS"] === "true") {
|
||||||
|
return { "keycloakVersion": tags[0] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value: keycloakVersion } = await cliSelect<string>({
|
||||||
|
"values": tags
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Aborting");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(keycloakVersion);
|
||||||
|
|
||||||
|
return { keycloakVersion };
|
||||||
|
}
|
73
src/bin/tools/NpmModuleVersion.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
export type NpmModuleVersion = {
|
||||||
|
major: number;
|
||||||
|
minor: number;
|
||||||
|
patch: number;
|
||||||
|
betaPreRelease?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export namespace NpmModuleVersion {
|
||||||
|
export function parse(versionStr: string): NpmModuleVersion {
|
||||||
|
const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error(`${versionStr} is not a valid NPM version`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"major": parseInt(match[1]),
|
||||||
|
"minor": parseInt(match[2]),
|
||||||
|
"patch": parseInt(match[3]),
|
||||||
|
...(() => {
|
||||||
|
const str = match[4];
|
||||||
|
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stringify(v: NpmModuleVersion) {
|
||||||
|
return `${v.major}.${v.minor}.${v.patch}${v.betaPreRelease === undefined ? "" : `-beta.${v.betaPreRelease}`}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* v1 < v2 => -1
|
||||||
|
* v1 === v2 => 0
|
||||||
|
* v1 > v2 => 1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
|
||||||
|
const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
|
||||||
|
const noUndefined = (n: number | undefined) => n ?? Infinity;
|
||||||
|
|
||||||
|
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
|
||||||
|
if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
|
||||||
|
return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0")) === -1 )
|
||||||
|
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0-beta.4")) === -1 )
|
||||||
|
console.log(compare(parse("3.0.0-beta.3"), parse("4.0.0")) === -1 )
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function bumpType(params: { versionBehindStr: string; versionAheadStr: string }): "major" | "minor" | "patch" | "betaPreRelease" | "same" {
|
||||||
|
const versionAhead = parse(params.versionAheadStr);
|
||||||
|
const versionBehind = parse(params.versionBehindStr);
|
||||||
|
|
||||||
|
if (compare(versionBehind, versionAhead) === 1) {
|
||||||
|
throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
|
||||||
|
if (versionBehind[level] !== versionAhead[level]) {
|
||||||
|
return level;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "same";
|
||||||
|
}
|
||||||
|
}
|
15
src/bin/tools/cliOptions.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import parseArgv from "minimist";
|
||||||
|
|
||||||
|
export type CliOptions = {
|
||||||
|
isSilent: boolean;
|
||||||
|
hasExternalAssets: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliOptions = (processArgv: string[]): CliOptions => {
|
||||||
|
const argv = parseArgv(processArgv);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||||
|
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||||
|
};
|
||||||
|
};
|
55
src/bin/tools/crc32.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
const crc32tab = [
|
||||||
|
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
||||||
|
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||||
|
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||||
|
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||||
|
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||||
|
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||||
|
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||||
|
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||||
|
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||||
|
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||||
|
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
||||||
|
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||||
|
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
||||||
|
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||||
|
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||||
|
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||||
|
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
||||||
|
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||||
|
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
||||||
|
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||||
|
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||||
|
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
||||||
|
* @returns a promise for a checksum (uint32)
|
||||||
|
*/
|
||||||
|
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Buffer) {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Readable) {
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
let crc = ~0;
|
||||||
|
input.setMaxListeners(Infinity);
|
||||||
|
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
||||||
|
input.on("error", e => reject(e));
|
||||||
|
input.on("data", (chunk: Buffer) => {
|
||||||
|
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported input " + typeof input);
|
||||||
|
}
|
||||||
|
}
|
61
src/bin/tools/deflate.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
||||||
|
import { pipeline } from "stream/promises";
|
||||||
|
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { crc32 } from "./crc32";
|
||||||
|
import tee from "./tee";
|
||||||
|
|
||||||
|
const deflateRaw = promisify(deflateRawCb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream transformer that records the number of bytes
|
||||||
|
* passed in its `size` property.
|
||||||
|
*/
|
||||||
|
class ByteCounter extends PassThrough {
|
||||||
|
size: number = 0;
|
||||||
|
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
||||||
|
if ("length" in chunk) this.size += chunk.length;
|
||||||
|
super._transform(chunk, encoding, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data buffer containing the data to be compressed
|
||||||
|
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
||||||
|
* of the source data
|
||||||
|
*/
|
||||||
|
export async function deflateBuffer(data: Buffer) {
|
||||||
|
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
||||||
|
return { deflated, crc32: checksum };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param input a byte stream, containing data to be compressed
|
||||||
|
* @param sink a method that will accept chunks of compressed data; We don't pass
|
||||||
|
* a writable here, since we don't want the writablestream to be closed after
|
||||||
|
* a single file
|
||||||
|
* @returns a promise, which will resolve with the crc32 checksum and the
|
||||||
|
* compressed size
|
||||||
|
*/
|
||||||
|
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
||||||
|
const deflateWriter = new Writable({
|
||||||
|
write(chunk, _, callback) {
|
||||||
|
sink(chunk);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tee the input stream, so we can compress and calc crc32 in parallel
|
||||||
|
const [rs1, rs2] = tee(input);
|
||||||
|
const byteCounter = new ByteCounter();
|
||||||
|
const [_, crc] = await Promise.all([
|
||||||
|
// pipe input into zip compressor, count the bytes
|
||||||
|
// returned and pass compressed data to the sink
|
||||||
|
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
||||||
|
// calc checksum
|
||||||
|
crc32(rs2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { crc32: crc, compressedSize: byteCounter.size };
|
||||||
|
}
|
@ -1,32 +1,87 @@
|
|||||||
import { basename as pathBasename, join as pathJoin } from "path";
|
import { exec as execCallback } from "child_process";
|
||||||
import { execSync } from "child_process";
|
import { createHash } from "crypto";
|
||||||
import fs from "fs";
|
import { mkdir, stat, writeFile } from "fs/promises";
|
||||||
|
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||||
|
import { dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { assert } from "tsafe";
|
||||||
|
import { promisify } from "util";
|
||||||
|
import { getProjectRoot } from "./getProjectRoot";
|
||||||
import { transformCodebase } from "./transformCodebase";
|
import { transformCodebase } from "./transformCodebase";
|
||||||
import { rm_rf, rm, rm_r } from "./rm";
|
import { unzip } from "./unzip";
|
||||||
|
|
||||||
/** assert url ends with .zip */
|
const exec = promisify(execCallback);
|
||||||
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
|
||||||
|
function hash(s: string) {
|
||||||
|
return createHash("sha256").update(s).digest("hex");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exists(path: string) {
|
||||||
|
try {
|
||||||
|
await stat(path);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get npm configuration as map
|
||||||
|
*/
|
||||||
|
async function getNmpConfig(): Promise<Record<string, string>> {
|
||||||
|
const { stdout } = await exec("npm config get", { encoding: "utf8" });
|
||||||
|
return stdout
|
||||||
|
.split("\n")
|
||||||
|
.filter(line => !line.startsWith(";"))
|
||||||
|
.map(line => line.trim())
|
||||||
|
.map(line => line.split("=", 2))
|
||||||
|
.reduce((cfg, [key, value]) => ({ ...cfg, [key]: value }), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proxy configuration from npm config files. Note that we don't care about
|
||||||
|
* proxy config in env vars, because make-fetch-happen will do that for us.
|
||||||
|
*
|
||||||
|
* @returns proxy configuration
|
||||||
|
*/
|
||||||
|
async function getNpmProxyConfig(): Promise<Pick<FetchOptions, "proxy" | "noProxy">> {
|
||||||
|
const cfg = await getNmpConfig();
|
||||||
|
|
||||||
|
const proxy = cfg["https-proxy"] ?? cfg["proxy"];
|
||||||
|
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||||
|
|
||||||
|
return { proxy, noProxy };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
||||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
|
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
|
||||||
const zipFilePath = pathBasename(url);
|
const projectRoot = getProjectRoot();
|
||||||
|
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
|
||||||
|
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
|
||||||
|
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
|
||||||
|
|
||||||
rm_rf(tmpDirPath);
|
if (!(await exists(zipFilePath))) {
|
||||||
|
const proxyOpts = await getNpmProxyConfig();
|
||||||
|
const response = await fetch(url, proxyOpts);
|
||||||
|
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||||
|
/**
|
||||||
|
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||||
|
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||||
|
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||||
|
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||||
|
* octokit upgrades.
|
||||||
|
*/
|
||||||
|
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||||
|
assert(typeof response.body !== "undefined" && response.body != null);
|
||||||
|
await writeFile(zipFilePath, response.body);
|
||||||
|
}
|
||||||
|
|
||||||
fs.mkdirSync(tmpDirPath, { "recursive": true });
|
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
|
||||||
|
|
||||||
execSync(`curl -L ${url} -o ${zipFilePath}`, { "cwd": tmpDirPath });
|
|
||||||
|
|
||||||
execSync(`unzip ${zipFilePath}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/*"`}`, {
|
|
||||||
"cwd": tmpDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
rm(pathBasename(url), { "cwd": tmpDirPath });
|
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": pathOfDirToExtractInArchive === undefined ? tmpDirPath : pathJoin(tmpDirPath, pathOfDirToExtractInArchive),
|
"srcDirPath": extractDirPath,
|
||||||
destDirPath,
|
"destDirPath": destDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
rm_r(tmpDirPath);
|
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { getProjectRoot } from "./getProjectRoot";
|
import { getProjectRoot } from "./getProjectRoot";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import child_process from "child_process";
|
import { constants } from "fs";
|
||||||
|
import { chmod, stat } from "fs/promises";
|
||||||
|
|
||||||
Object.entries<string>(require(pathJoin(getProjectRoot(), "package.json"))["bin"]).forEach(([, scriptPath]) =>
|
(async () => {
|
||||||
child_process.execSync(`chmod +x ${scriptPath}`, {
|
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
|
||||||
"cwd": getProjectRoot(),
|
|
||||||
}),
|
const promises = Object.values<string>(bin).map(async scriptPath => {
|
||||||
);
|
const fullPath = pathJoin(getProjectRoot(), scriptPath);
|
||||||
|
const oldMode = (await stat(fullPath)).mode;
|
||||||
|
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
|
||||||
|
await chmod(fullPath, newMode);
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
})();
|
||||||
|
87
src/bin/tools/jar.ts
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import { dirname, relative, sep } from "path";
|
||||||
|
import { createWriteStream } from "fs";
|
||||||
|
|
||||||
|
import walk from "./walk";
|
||||||
|
import { ZipFile } from "yazl";
|
||||||
|
import { mkdir } from "fs/promises";
|
||||||
|
import trimIndent from "./trimIndent";
|
||||||
|
|
||||||
|
export type ZipEntry = { zipPath: string } & ({ fsPath: string } | { buffer: Buffer });
|
||||||
|
export type ZipEntryGenerator = AsyncGenerator<ZipEntry, void, unknown>;
|
||||||
|
|
||||||
|
type CommonJarArgs = {
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JarStreamArgs = CommonJarArgs & {
|
||||||
|
asyncPathGeneratorFn(): ZipEntryGenerator;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type JarArgs = CommonJarArgs & {
|
||||||
|
targetPath: string;
|
||||||
|
rootPath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function jarStream({ groupId, artifactId, version, asyncPathGeneratorFn }: JarStreamArgs) {
|
||||||
|
const manifestPath = "META-INF/MANIFEST.MF";
|
||||||
|
const manifestData = Buffer.from(trimIndent`
|
||||||
|
Manifest-Version: 1.0
|
||||||
|
Archiver-Version: Plexus Archiver
|
||||||
|
Created-By: Keycloakify
|
||||||
|
Built-By: unknown
|
||||||
|
Build-Jdk: 19.0.0
|
||||||
|
`);
|
||||||
|
|
||||||
|
const pomPropsPath = `META-INF/maven/${groupId}/${artifactId}/pom.properties`;
|
||||||
|
const pomPropsData = Buffer.from(trimIndent`
|
||||||
|
# Generated by keycloakify
|
||||||
|
# ${new Date()}
|
||||||
|
artifactId=${artifactId}
|
||||||
|
groupId=${groupId}
|
||||||
|
version=${version}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const zipFile = new ZipFile();
|
||||||
|
|
||||||
|
for await (const entry of asyncPathGeneratorFn()) {
|
||||||
|
if ("buffer" in entry) {
|
||||||
|
zipFile.addBuffer(entry.buffer, entry.zipPath);
|
||||||
|
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
|
||||||
|
zipFile.addFile(entry.fsPath, entry.zipPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
zipFile.addBuffer(manifestData, manifestPath);
|
||||||
|
zipFile.addBuffer(pomPropsData, pomPropsPath);
|
||||||
|
|
||||||
|
zipFile.end();
|
||||||
|
|
||||||
|
return zipFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
|
||||||
|
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
|
||||||
|
* the contents of the pom.properties file which is going to be added to the archive.
|
||||||
|
*/
|
||||||
|
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
|
||||||
|
await mkdir(dirname(targetPath), { recursive: true });
|
||||||
|
|
||||||
|
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
|
||||||
|
for await (const fsPath of walk(rootPath)) {
|
||||||
|
const zipPath = relative(rootPath, fsPath).split(sep).join("/");
|
||||||
|
yield { fsPath, zipPath };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });
|
||||||
|
|
||||||
|
await new Promise<void>(async (resolve, reject) => {
|
||||||
|
zipFile.outputStream
|
||||||
|
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
|
||||||
|
.on("close", () => resolve())
|
||||||
|
.on("error", e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
7
src/bin/tools/kebabCaseToSnakeCase.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { capitalize } from "tsafe/capitalize";
|
||||||
|
|
||||||
|
export function kebabCaseToCamelCase(kebabCaseString: string): string {
|
||||||
|
const [first, ...rest] = kebabCaseString.split("-");
|
||||||
|
|
||||||
|
return [first, ...rest.map(capitalize)].join("");
|
||||||
|
}
|