Compare commits

...

304 Commits

Author SHA1 Message Date
984ef63661 Update changelog v1.0.3 2021-05-23 20:22:50 +00:00
a8daf175ea Instuction about how to integrate with non CRA projects 2021-05-23 22:19:51 +02:00
055263a3da Add mention to awesome list 2021-05-15 22:41:30 +02:00
9990b0ab05 fmt (changelog ignore) 2021-05-01 18:05:40 +02:00
423397ce3e Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-05-01 18:04:07 +02:00
954567712c Give hint about where to find the ftl files (changelog ignore) 2021-05-01 18:03:35 +02:00
9f52eb8123 Update changelog v1.0.2 2021-05-01 14:17:28 +00:00
744b198fb4 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-05-01 16:15:44 +02:00
15eab797c3 Add key for child in a list (changelog ignore) 2021-05-01 16:15:40 +02:00
8ff86b1e29 Update changelog v1.0.1 2021-05-01 14:12:41 +00:00
e1b8760ee3 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-05-01 16:10:57 +02:00
bd0d890b2c Fix: LoginOtp (and not otc) 2021-05-01 16:10:52 +02:00
2a2118d769 Update changelog v1.0.0 2021-05-01 13:52:33 +00:00
9839b64650 Bump version (changelog ignore) 2021-05-01 15:50:45 +02:00
2bf55e12f9 Guide for implementing a missing page (fix, changelog ignore) 2021-05-01 15:50:12 +02:00
2249fa9232 #4: Guide for implementing a missing page 2021-05-01 15:48:49 +02:00
f673a65304 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-05-01 14:56:01 +02:00
0163459ad6 Support OTP #4 2021-05-01 14:55:58 +02:00
b21123cc9d Update changelog v0.4.4 2021-04-29 21:33:33 +00:00
7800d125b2 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-04-29 23:31:12 +02:00
89ea648f18 Fix previous release 2021-04-29 23:31:05 +02:00
ab7ac3c2d0 Update changelog v0.4.3 2021-04-29 17:07:48 +00:00
b16319d962 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-04-29 19:02:16 +02:00
f8012d5dfb Bump version (changelog ignore) 2021-04-29 19:00:56 +02:00
45a2015597 Add infos about the plugin that defines authorizedMailDomains 2021-04-29 19:00:38 +02:00
524ab000be Update changelog v0.4.2 2021-04-29 16:47:44 +00:00
d73cfb8765 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-04-29 18:42:29 +02:00
8164f5373f Client side validation of allowed email domains 2021-04-29 18:42:14 +02:00
824b0c275e fmt (changelog ignore) 2021-04-21 20:45:03 +02:00
f8d83d7a37 Update README (changelog ignore) 2021-04-20 11:44:57 +02:00
b291526b13 Support email whitlisting 2021-04-15 12:20:08 +02:00
e1c310d383 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-04-12 23:46:29 +02:00
242777a8eb Restore kickstart video in the readme 2021-04-12 23:46:24 +02:00
10a6b70fe9 Update README.md 2021-04-12 19:27:54 +02:00
c829f5969c Update README.md 2021-04-12 18:51:16 +02:00
ba6a5047b1 Add note (changelog ignore) 2021-04-12 04:59:39 +02:00
852f48c05f update gif (changelog ignore) 2021-04-12 04:47:53 +02:00
c342f04a92 Update video (changelog ignore) 2021-04-12 04:21:18 +02:00
42eb8147c6 fmt (changelog ignore) 2021-04-12 02:29:24 +02:00
ebcdbd782f fmt (changelog ignore) 2021-04-12 02:28:22 +02:00
d2059e08d1 fmt (changelog ignore) 2021-04-12 02:25:48 +02:00
4f075882d5 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-04-12 02:24:17 +02:00
044ec1a2da Important readme update 2021-04-12 02:24:11 +02:00
a49a32703d Update changelog v0.4.1 2021-04-11 16:24:36 +00:00
46ec832767 Quietly re-introduce --external-assets 2021-04-11 18:18:52 +02:00
fc858b3db6 Update screenshot (changelog ignore) 2021-04-11 01:21:01 +02:00
3cd8843157 Update screenshot in readme for therms and conditions (changelog ignore) 2021-04-10 20:20:26 +02:00
c9358ea8dd Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-09 04:02:30 +02:00
354a4db0f6 Give example of customization 2021-04-09 04:02:23 +02:00
90d435d96b Update changelog v0.4.0 2021-04-09 01:31:07 +00:00
2d804f0f0f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-09 03:26:06 +02:00
1c9acedac0 Bump version (changelog ignore) 2021-04-09 03:26:01 +02:00
6e914e4ea3 Acual support of Therms of services 2021-04-09 03:25:39 +02:00
f0c4786267 Update changelog v0.3.24 2021-04-08 16:14:10 +00:00
0b16159312 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-08 18:00:04 +02:00
ea8a91e069 Add missing dependency: markdown 2021-04-08 17:59:58 +02:00
59db202fe4 Update changelog v0.3.23 2021-04-08 15:43:34 +00:00
09927afd43 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-08 17:40:54 +02:00
13c6122b9b Bump version (changelog ignore) 2021-04-08 17:06:26 +02:00
1bb19f65a2 Allow to lazily load therms 2021-04-08 17:06:09 +02:00
918a80cfb6 Update changelog v0.3.22 2021-04-08 13:48:41 +00:00
ed7d5eabcb Bump version (changelog ignore) 2021-04-08 15:43:12 +02:00
2795109162 update powerhooks 2021-04-08 15:42:41 +02:00
966f277628 Support terms and condition 2021-04-08 15:41:40 +02:00
36d60411f9 Fix info.ftl 2021-04-08 12:54:29 +02:00
60fe33f3fd Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-08 12:20:14 +02:00
1df685df92 For useKcMessage we prefer returning callbacks with a changing references 2021-04-08 12:20:06 +02:00
7894d95ace Update changelog v0.3.21 2021-04-04 19:20:27 +00:00
a8b4493aa1 update yarn.lock (changelog ignore) 2021-04-04 21:17:35 +02:00
715a7399cf Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-04 21:12:39 +02:00
a1e59bae23 Update powerhooks 2021-04-04 21:12:30 +02:00
b0819314a1 Update changelog v0.3.20 2021-04-01 22:37:41 +00:00
0099442543 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-01 23:43:14 +02:00
66a0b07228 Bump version (changelog ignore) 2021-04-01 23:43:08 +02:00
85f9544754 Always catch freemarker errors 2021-04-01 23:42:31 +02:00
2f16a09ab8 Update changelog v0.3.19 2021-04-01 15:21:10 +00:00
183ae98c30 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-01 17:16:01 +02:00
ba15e63879 Bump version (changelog ignore) 2021-04-01 17:15:55 +02:00
654277feda Fix previous release 2021-04-01 17:15:28 +02:00
81279a5cc5 Update changelog v0.3.18 2021-04-01 15:04:08 +00:00
59f0a843b0 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-04-01 16:52:53 +02:00
c094f70171 Bump version (changelog ignore) 2021-04-01 16:51:50 +02:00
0858fe6319 Fix error.ftt, Adopt best effort strategy to convert ftl values into JS 2021-04-01 16:51:28 +02:00
5012ec0ccc Update changelog v0.3.17 2021-03-29 04:33:07 +00:00
990a24fab2 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-29 06:27:19 +02:00
036b6bf82a Use push instead of replace in keycloak-js adapter to enable going back 2021-03-29 06:27:12 +02:00
8272a02b52 Update changelog v0.3.15 2021-03-28 12:18:37 +00:00
e346b1d9d2 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-28 14:14:52 +02:00
2309bd21c6 Remove all reference to --external-assets, broken feature 2021-03-28 14:14:43 +02:00
7d6476c1b5 Update changelog v0.3.14 2021-03-28 11:39:36 +00:00
e892a0e7e6 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-28 13:37:07 +02:00
ca5b41e730 Fix standalone mode: imports from js 2021-03-28 13:37:02 +02:00
9b18234112 Update changelog v0.3.13 2021-03-26 16:54:38 +00:00
5274368f47 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-26 17:49:36 +01:00
1415c24028 Bump version (changelog ignore) 2021-03-26 17:49:31 +01:00
4a084f5859 Fix previous release (changelog ignore) 2021-03-26 17:49:09 +01:00
a30c9eb0cd Update changelog v0.3.12 2021-03-26 16:35:41 +00:00
85d3b40b8e Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-26 17:31:06 +01:00
335afec230 Bump version (changelog ignore) 2021-03-26 17:31:01 +01:00
69fa49848a Fix mocksContext 2021-03-26 17:30:04 +01:00
7a09051127 Update changelog v0.3.11 2021-03-26 14:36:48 +00:00
07ee0ecb8b Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-26 15:29:23 +01:00
6f133428f8 Fix previous build, improve README 2021-03-26 15:29:17 +01:00
4f733736db fmt (changelog ignore) 2021-03-26 14:06:14 +01:00
d96ff13a67 Update changelog v0.3.10 2021-03-26 13:05:25 +00:00
2c1351ce47 fmt (changelog ignore) 2021-03-26 14:04:45 +01:00
96cd56ec77 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-26 14:02:46 +01:00
e5b2096d65 Bump version (changelog ignore) 2021-03-26 14:02:40 +01:00
3aa140335f Handle <style> tag, improve documentation 2021-03-26 14:02:14 +01:00
4cafaa2492 Update changelog v0.3.9 2021-03-25 11:54:13 +00:00
9c633a7521 Update readme 2021-03-25 12:48:40 +01:00
e27845ba91 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-25 12:01:15 +01:00
2a8708a45b Document --external-assets 2021-03-25 12:01:11 +01:00
6874fa4c24 Update README.md 2021-03-24 09:12:50 +01:00
ba531a4927 Update README.md 2021-03-24 09:12:04 +01:00
20175b57cf Update README.md 2021-03-24 09:10:44 +01:00
ad275e4c34 Update changelog v0.3.8 2021-03-22 22:36:17 +00:00
060b9fe0de Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-22 23:34:13 +01:00
17b24d14ed Make standalone mode the default 2021-03-22 23:34:07 +01:00
2d278b0680 Update changelog v0.3.7 2021-03-22 19:57:34 +00:00
fb5975e4f1 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-22 20:54:35 +01:00
24fccaf513 (test) external asset mode by default 2021-03-22 20:54:28 +01:00
293953aa1b Update changelog v0.3.6 2021-03-22 19:02:53 +00:00
1049e312f9 Fix previous release 2021-03-22 20:00:58 +01:00
a2db250600 Update changelog v0.3.5 2021-03-22 18:43:18 +00:00
cf7fe8c337 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-22 19:41:08 +01:00
f5350097bf Bump version (changelog ignore) 2021-03-22 19:40:58 +01:00
1cb5dd461b support homepage with urlPath 2021-03-22 19:40:38 +01:00
845599a5e8 Update changelog v0.3.4 2021-03-22 06:23:40 +00:00
0cc02c292f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-22 07:21:36 +01:00
1919702326 Bugfix: Import assets from CSS 2021-03-22 07:21:31 +01:00
0c0052e1cd Update changelog v0.3.3 2021-03-22 04:27:01 +00:00
78622770ec Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-22 05:21:22 +01:00
7b86727394 Fix submit not receving correct text 2021-03-22 05:21:05 +01:00
0965f8648e Update changelog v0.3.2 2021-03-21 21:31:01 +00:00
98974b4367 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-21 22:25:49 +01:00
597bcadd9e Fix broken previous release 2021-03-21 22:25:47 +01:00
4d9aabcb91 Update changelog v0.3.1 2021-03-21 21:13:36 +00:00
1606c2884d kcHeaderClass can be updated after initial mount 2021-03-21 22:10:33 +01:00
12f69b593f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-20 05:07:26 +01:00
1ca45f90d0 Update readme (changelog ignore) 2021-03-20 05:07:22 +01:00
a91a5616f9 Update changelog v0.3.0 2021-03-20 04:02:59 +00:00
c525e09368 Bump version 2021-03-20 05:00:14 +01:00
f5bba4a6a0 Feat: Cary over states using URL search params 2021-03-20 04:59:18 +01:00
77a37fb573 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-20 02:54:21 +01:00
6b24c5878c Bugfix: with kcHtmlClass 2021-03-20 02:54:15 +01:00
f4414e1249 Update changelog v0.2.10 2021-03-19 23:19:03 +00:00
b72971f4ce Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-03-20 00:17:06 +01:00
b9af4e6804 Bump version (changelog ingnore) 2021-03-20 00:16:58 +01:00
2fd1d42d1e Remove dependency to denoify 2021-03-20 00:16:34 +01:00
3cfc7d7fa9 Update changelog v0.2.9 2021-03-19 23:14:27 +00:00
b5d9055fcf Bump version (changelog ignore) 2021-03-20 00:09:37 +01:00
63d644d95f Update deps and CI workflow 2021-03-20 00:09:12 +01:00
e16192b416 Update changelog v0.2.8 2021-03-19 21:42:31 +00:00
505e018448 Bump version (changelog ignore) 2021-03-19 22:39:48 +01:00
5ced0e2809 Bugfix: keycloak_build that grow and grow in size 2021-03-19 22:39:32 +01:00
0e1d919f7e Add disclaimer about maitainment strategy 2021-03-15 16:22:13 +01:00
a009db998e Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-15 16:05:28 +01:00
d6c6bd933b Add a note for tested version support 2021-03-15 16:05:21 +01:00
859cc03f35 Update changelog v0.2.7 2021-03-13 11:56:59 +00:00
1a3b8ae3b8 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-13 12:51:41 +01:00
863a08abf3 Bump version 2021-03-13 12:51:34 +01:00
fd9c6afa5e Update README.md 2021-03-11 00:10:34 +01:00
8f3797407b Update README.md 2021-03-10 23:55:37 +01:00
7eedb23285 Update changelog v0.2.6 2021-03-10 22:04:25 +00:00
e4a2c95dd8 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-10 23:02:30 +01:00
9429228b71 Bump version (changelog ignore) 2021-03-10 23:02:25 +01:00
aafbc60f12 Fix generated gitignore 2021-03-10 23:02:03 +01:00
7170611791 Update changelog v0.2.5 2021-03-10 21:51:07 +00:00
59e57f3dd5 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-10 22:46:06 +01:00
fd0d25b081 Bump version (changelog ignore) 2021-03-10 22:46:01 +01:00
fa529c911a Fix generated .gitignore 2021-03-10 22:45:09 +01:00
dc997b7ef4 Update changelog v0.2.4 2021-03-10 20:33:36 +00:00
0168d32f96 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-10 21:27:48 +01:00
d6fc0c779c Bump version (changelog ignore) 2021-03-10 21:27:43 +01:00
08be36edfa Update deps (changelog ignore) 2021-03-10 21:27:23 +01:00
990d287953 Update README.md 2021-03-09 05:12:45 +01:00
a629d4ab45 Update readme 2021-03-09 04:56:45 +01:00
78b48af886 Update changelog v0.2.3 2021-03-09 02:07:07 +00:00
72d267853c fix gitignore generation 2021-03-09 03:04:59 +01:00
66a218c2ec Update changelog v0.2.2 2021-03-08 02:02:53 +00:00
d4f3ec2245 Add table of content 2021-03-08 02:59:22 +01:00
7bdc19bf4b Update README.md 2021-03-08 02:55:41 +01:00
103ef788fb Update README.md 2021-03-08 02:54:43 +01:00
b6f6d1f3cc Update README.md 2021-03-08 02:52:58 +01:00
ef3d2e4e04 Update publish.yaml 2021-03-08 02:51:50 +01:00
8ec8b91ead Update changelog v0.2.1 2021-03-08 01:49:47 +00:00
819a1d473d Update ci.yaml 2021-03-08 02:48:02 +01:00
c930337255 Update readme 2021-03-08 02:40:25 +01:00
57bb4a9d96 Bump version (changelog ignore) 2021-03-08 02:36:21 +01:00
1776341242 Update readme 2021-03-08 02:35:58 +01:00
983eec6941 Bump version (changelog ignore) 2021-03-08 02:34:31 +01:00
86d390ee1a update deps 2021-03-08 02:33:52 +01:00
91703409d9 Update readme 2021-03-08 02:29:54 +01:00
3322d0e4a5 Add all mocks for testing 2021-03-08 01:02:06 +01:00
d09038fde2 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-08 00:10:57 +01:00
71966deaac Bump version (changelog ignore) 2021-03-08 00:10:51 +01:00
12e83c9468 many small fixes 2021-03-08 00:09:52 +01:00
fe27357dbb Update changelog v0.1.6 2021-03-07 16:09:08 +00:00
b93003e76d Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-07 17:07:20 +01:00
d6c0e9f783 Bump version (changelog ignore) 2021-03-07 17:05:28 +01:00
18a1baae59 Fix Turkish 2021-03-07 17:05:10 +01:00
2330788995 Update changelog v0.1.5 2021-03-07 15:43:11 +00:00
40c146022a Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-07 16:41:32 +01:00
9844e7554f Bump version changelog ignore 2021-03-07 16:41:27 +01:00
b064b8cbe6 Fix getKcLanguageLabel 2021-03-07 16:41:07 +01:00
997941fbf7 Update changelog v0.1.4 2021-03-07 14:46:12 +00:00
8d5e080bd6 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-07 15:44:38 +01:00
1c1ca25287 Bump version (changelog ignore) 2021-03-07 15:44:33 +01:00
5195b3bc1e Update changelog v0.1.3 2021-03-07 14:39:29 +00:00
3ff56cfea7 Bump version (changelog ignore) 2021-03-07 15:37:58 +01:00
adc6d69201 Implement LoginVerifyEmail 2021-03-07 15:37:37 +01:00
438ca4595f Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-07 14:57:59 +01:00
740d9b7af5 Implement login-reset-password.ftl 2021-03-07 14:57:53 +01:00
919a6947bc Update changelog v0.1.2 2021-03-07 01:17:32 +00:00
85fdaa2f22 Bump version (changelog version) 2021-03-07 02:15:51 +01:00
6ade3d6375 Fix build 2021-03-07 02:15:21 +01:00
73e9ebbe40 Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-07 01:47:10 +01:00
ad78221025 Fix build 2021-03-07 01:47:03 +01:00
714fec0bf3 Update changelog v0.1.1 2021-03-06 22:06:48 +00:00
de312c60b1 Bump version (changelog ignore) 2021-03-06 23:05:15 +01:00
1d07fd7675 Implement Error page 2021-03-06 23:03:03 +01:00
307650aaea rename pageBasename by pageId 2021-03-06 22:41:36 +01:00
6eccd313b6 Implement reactive programing for language switching 2021-03-06 15:16:21 +01:00
b8751d67db Merge branch 'develop' of https://github.com/garronej/keycloakify into develop 2021-03-06 14:43:03 +01:00
25d9d3dc26 Add Info page, refactor 2021-03-06 14:42:56 +01:00
68e6c9faaf Update changelog v0.1.0 2021-03-05 19:44:56 +00:00
f3fb360ce0 Rename keycloakify 2021-03-05 20:43:22 +01:00
d3631dd10c Update changelog v0.0.33 2021-03-05 18:52:19 +00:00
891c91aa20 bump version (changelog ignore) 2021-03-05 19:50:40 +01:00
880018e926 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-05 19:50:14 +01:00
06ab2ab82e Fix syncronization with non react pages 2021-03-05 19:50:08 +01:00
aafcfb62f2 Update changelog v0.0.32 2021-03-05 14:48:38 +00:00
a69bee8726 bump version 2021-03-05 15:46:55 +01:00
240208793d Add log to tell when we are using react 2021-03-05 15:46:34 +01:00
e7a320f8f8 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-05 15:44:42 +01:00
f76438dd82 Fix missing parentesis 2021-03-05 15:44:35 +01:00
d6dbb42dea Update changelog v0.0.31 2021-03-05 14:34:34 +00:00
2e076aa058 bump version (changelog ignore) 2021-03-05 15:32:50 +01:00
b59447b840 Fix typo 2021-03-05 15:13:50 +01:00
702352bea2 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-05 15:08:56 +01:00
98f647fadf Fix register page 500 2021-03-05 14:50:46 +01:00
85d3a011d8 Update changelog v0.0.30 2021-03-05 00:40:09 +00:00
26b0b55a7d Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-05 01:38:37 +01:00
50d0e74c03 Edit language statistique 2021-03-05 01:38:15 +01:00
5a7fb7c303 Update changelog v0.0.30 2021-03-05 00:38:08 +00:00
e9f5a79d69 bump version (changelog ignore) 2021-03-05 01:36:17 +01:00
1378dbebee avoid escaping urls 2021-03-05 01:35:50 +01:00
7daa818996 Use default value instead of value 2021-03-05 00:44:27 +01:00
c8e219361b Fix double single quote problem in messages 2021-03-05 00:23:50 +01:00
b74323a48e Fix typo 2021-03-05 00:14:06 +01:00
0cfe240590 Fix non editable username 2021-03-05 00:03:21 +01:00
533105d63a Fix some bugs 2021-03-04 23:24:43 +01:00
62e4af2c78 Fix Object.deepAssign 2021-03-04 21:50:18 +01:00
934c07f365 Make the dongle to download smaller 2021-03-04 21:43:36 +01:00
624409434a Split kcContext among pages 2021-03-04 21:14:54 +01:00
546c74aa28 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-04 18:15:59 +01:00
2ee12abc43 Implement register 2021-03-04 18:15:48 +01:00
b9717e649f Update changelog v0.0.29 2021-03-04 13:44:23 +00:00
09c7b6ac03 Fix build 2021-03-04 14:42:38 +01:00
079da86cf1 bump version (changelog ignore) 2021-03-04 14:31:24 +01:00
57502a57af Fix i18n 2021-03-04 14:30:59 +01:00
6738f6f6cf Login appear to be working now 2021-03-04 13:56:51 +01:00
21763db561 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-03 03:18:02 +01:00
24e07c3e00 closer but not there yet 2021-03-03 03:17:56 +01:00
8820262b0f Update changelog v0.0.28 2021-03-03 01:33:07 +00:00
2d69a1f946 bump version (changelog ignore) 2021-03-03 02:31:25 +01:00
2a5a4c16ea fix build 2021-03-03 02:31:02 +01:00
52ba14cd8f Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-03 00:10:48 +01:00
6222850453 There is no reason not to let use translations outside of keycloak 2021-03-03 00:10:41 +01:00
ec2b085855 Update changelog v0.0.27 2021-03-02 23:05:59 +00:00
305d5b7328 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-03 00:04:28 +01:00
deb4ddaed2 Bump version (changelog ignore) 2021-03-03 00:04:26 +01:00
379f052e6e Implement entrypoint 2021-03-03 00:04:06 +01:00
2f592ab38c Update changelog v0.0.26 2021-03-02 22:50:08 +00:00
43c412405a Login page implemented 2021-03-02 23:48:31 +01:00
7f6ab1d391 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-02 22:48:42 +01:00
e5e39d036d Implement login 2021-03-02 22:48:36 +01:00
6a93c6e894 remove unesseary log 2021-03-02 12:19:12 +01:00
069262c83d Update changelog v0.0.25 2021-03-02 11:19:07 +00:00
69f017df36 Fix build and reduce size 2021-03-02 12:17:24 +01:00
63ccb0609e Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-02 01:05:22 +01:00
292881532a Implement the template 2021-03-02 01:05:15 +01:00
d35eac9661 Update changelog v0.0.24 2021-03-01 20:00:22 +00:00
f92488b80d update 2021-03-01 20:58:50 +01:00
46c482d098 update 2021-03-01 20:56:05 +01:00
1bd05d22c3 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-01 20:52:19 +01:00
9e51ef90c0 update 2021-03-01 20:52:14 +01:00
b6fadad6cc Update changelog v0.0.23 2021-03-01 19:52:03 +00:00
193f66c843 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-01 20:50:38 +01:00
c3459106d6 update 2021-03-01 20:50:13 +01:00
931ce161d7 Update changelog v0.0.23 2021-03-01 19:49:56 +00:00
83ebf93f2a Update changelog v0.0.23 2021-03-01 19:49:37 +00:00
84b9909931 update 2021-03-01 20:48:30 +01:00
8d91ea6307 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-01 20:47:59 +01:00
fa99d8dd9a update 2021-03-01 20:47:53 +01:00
67243ecfc6 Update changelog v0.0.23 2021-03-01 19:47:44 +00:00
6cfefd7fbd Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-03-01 20:45:44 +01:00
f599df2bb7 update 2021-03-01 20:45:37 +01:00
57a0d9f10f Bump version (changelog ignore) 2021-03-01 13:56:18 +01:00
34902b54e4 Handle formatting in translation function 2021-03-01 13:55:58 +01:00
5db564f481 Update changelog v0.0.22 2021-02-28 19:23:45 +00:00
0924a42341 Merge branch 'develop' of https://github.com/garronej/keycloak-react-theming into develop 2021-02-28 20:22:24 +01:00
be295ff328 Split page messages 2021-02-28 20:22:18 +01:00
68 changed files with 13134 additions and 9259 deletions

6
.gitattributes vendored
View File

@ -1,3 +1,3 @@
/src/lib/i18n/messages.generated.ts linguist-vendored=false
/src/bin/download-sample-keycloak-themes.ts linguist-vendored=false
/src/bin/build-keycloak-theme/index.ts linguist-vendored=false
src/lib/i18n/generated_messages/* linguist-documentation
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
src/bin/build-keycloak-theme/index.ts -linguist-detectable

View File

@ -2,87 +2,135 @@ name: ci
on:
push:
branches:
- develop
- main
pull_request:
branches:
- develop
- main
jobs:
test_node:
runs-on: ubuntu-latest
test:
runs-on: macos-10.15
strategy:
matrix:
node: [ '14', '13', '12' ]
name: Test with Node v${{ matrix.node }}
steps:
- name: Tell if project is using npm or yarn
id: _1
uses: garronej/github_actions_toolkit@v1.11
id: step1
uses: garronej/github_actions_toolkit@v2.2
with:
action_name: tell_if_project_uses_npm_or_yarn
owner: ${{github.repository_owner}}
repo: ${{github.event.repository.name}}
branch: ${{github.ref}}
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/checkout@v2.3.4
- uses: actions/setup-node@v2.1.3
with:
node-version: ${{ matrix.node }}
- if: steps._1.outputs.npm_or_yarn == 'yarn'
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn install --frozen-lockfile
yarn run build
yarn run test
- if: steps._1.outputs.npm_or_yarn == 'npm'
yarn build
yarn test
- if: steps.step1.outputs.npm_or_yarn == 'npm'
run: |
npm ci
npm run build
npm run test
trigger_publish:
name: Trigger publish.yaml workflow if package.json version updated ( and secrets.PAT is set ).
npm test
check_if_version_upgraded:
name: Check if version upgrade
if: github.event_name == 'push'
runs-on: ubuntu-latest
env:
PAT: ${{secrets.PAT}}
if: github.event_name == 'push' && github.event.head_commit.author.name != 'ts_ci'
needs: test_node
needs: test
outputs:
from_version: ${{ steps.step1.outputs.from_version }}
to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{steps.step1.outputs.is_upgraded_version }}
steps:
- name: Get version on latest
id: v_latest
uses: garronej/github_actions_toolkit@v1.11
- uses: garronej/github_actions_toolkit@v2.2
id: step1
with:
action_name: get_package_json_version
owner: ${{github.repository_owner}}
repo: ${{github.event.repository.name}}
branch: latest
compare_to_version: '0.0.0'
action_name: is_package_json_version_upgraded
- name: Get version on develop
id: v_develop
uses: garronej/github_actions_toolkit@v1.11
with:
action_name: get_package_json_version
owner: ${{github.repository_owner}}
repo: ${{github.event.repository.name}}
branch: ${{ github.sha }}
compare_to_version: ${{steps.v_latest.outputs.version || '0.0.0'}}
- name: 'Trigger the ''publish'' workflow'
if: ${{ !!env.PAT && steps.v_develop.outputs.compare_result == '1' }}
uses: garronej/github_actions_toolkit@v1.11
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
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.2
with:
action_name: dispatch_event
owner: ${{github.repository_owner}}
repo: ${{github.event.repository.name}}
event_type: publish
client_payload_json: |
${{
format(
'{{"from_version":"{0}","to_version":"{1}","repo":"{2}"}}',
steps.v_latest.outputs.version,
steps.v_develop.outputs.version,
github.event.repository.name
)
}}
action_name: update_changelog
branch: ${{ github.ref }}
commit_author_email: ts_ci@github.com
create_github_release:
runs-on: ubuntu-latest
needs:
- update_changelog
- check_if_version_upgraded
steps:
- uses: actions/checkout@v2
with:
ref: ${{ github.ref }}
- name: Build GitHub release body
id: step1
run: |
if [ "$FROM_VERSION" = "0.0.0" ]; then
echo "::set-output name=body::🚀"
else
echo "::set-output name=body::📋 [CHANGELOG](https://github.com/$GITHUB_REPOSITORY/blob/v$TO_VERSION/CHANGELOG.md)"
fi
env:
FROM_VERSION: ${{ needs.check_if_version_upgraded.outputs.from_version }}
TO_VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
- uses: garronej/action-gh-release@v0.2.0
with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
target_commitish: ${{ github.ref }}
body: ${{ steps.step1.outputs.body }}
draft: false
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_on_npm:
runs-on: macos-10.15
needs:
- update_changelog
- check_if_version_upgraded
steps:
- uses: actions/checkout@v2.3.4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v2.1.3
with:
node-version: '15'
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
if [ "$PACKAGE_MANAGER" = "yarn" ]; then
yarn install --frozen-lockfile
else
npm ci
fi
$PACKAGE_MANAGER run build
- run: npx -y -p denoify@0.6.5 denoify_enable_short_npm_import_path
env:
DRY_RUN: "0"
- name: Publishing on NPM
run: |
if [ "$(npm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
if [ "$NPM_TOKEN" = "" ]; then
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
false
fi
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
npm publish
rm .npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}

View File

@ -1,116 +0,0 @@
on:
repository_dispatch:
types: publish
jobs:
update_changelog_and_sync_package_lock_version:
name: Update CHANGELOG.md and make sure package.json and package-lock.json versions matches.
runs-on: ubuntu-latest
steps:
- name: Synchronize package.json and package-lock.json version if needed.
uses: garronej/github_actions_toolkit@v1.11
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
action_name: sync_package_and_package_lock_version
owner: ${{github.repository_owner}}
repo: ${{github.event.client_payload.repo}}
branch: develop
commit_author_email: ts_ci@github.com
- name: Update CHANGELOG.md
if: ${{ !!github.event.client_payload.from_version }}
uses: garronej/github_actions_toolkit@v1.11
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
action_name: update_changelog
owner: ${{github.repository_owner}}
repo: ${{github.event.client_payload.repo}}
branch_behind: latest
branch_ahead: develop
commit_author_email: ts_ci@github.com
exclude_commit_from_author_names_json: '["ts_ci"]'
publish:
runs-on: ubuntu-latest
needs: update_changelog_and_sync_package_lock_version
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 0
ref: develop
- name: Remove .github directory, useless on 'latest' branch
run: rm -r .github
- name: Remove branch 'latest'
continue-on-error: true
run: |
git branch -d latest || true
git push origin :latest
- name: Create the new 'latest' branch
run: |
git branch latest
git checkout latest
- uses: actions/setup-node@v1
- run: |
if [ -f "./yarn.lock" ]; then
yarn install --frozen-lockfile
else
npm ci
fi
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run enable_short_import_path
env:
DRY_RUN: "0"
- name: (DEBUG) Show how the files have been moved to enable short import
run: ls -lR
- name: Publishing on NPM
run: |
if [ "$(npm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
if [ "$NPM_TOKEN" = "" ]; then
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
false
fi
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
npm publish
rm .npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
VERSION: ${{ github.event.client_payload.to_version }}
- name: Commit changes
run: |
git config --local user.email "ts_ci@github.com"
git config --local user.name "ts_ci"
git add -A
git commit -am "Enabling shorter import paths [automatic]"
- run: git push origin latest
- name: Release body
id: id_rb
run: |
if [ "$FROM_VERSION" = "" ]; then
echo "::set-output name=body::🚀"
else
echo "::set-output name=body::📋 [CHANGELOG](https://github.com/$OWNER/$REPO/blob/$REF/CHANGELOG.md)"
fi
env:
FROM_VERSION: ${{ github.event.client_payload.from_version }}
OWNER: ${{github.repository_owner}}
REPO: ${{github.event.client_payload.repo}}
REF: v${{github.event.client_payload.to_version}}
- name: Create Release
uses: garronej/create-release@master
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
with:
tag_name: v${{ github.event.client_payload.to_version }}
release_name: Release v${{ github.event.client_payload.to_version }}
branch: latest
draft: false
prerelease: false
body: ${{ steps.id_rb.outputs.body }}

View File

@ -1,3 +1,325 @@
### **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

429
README.md
View File

@ -2,98 +2,383 @@
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
</p>
<p align="center">
<i>🔏 Keycloak theme generator for Reacts app 🔏</i>
<i>🔏 Create Keycloak themes using React 🔏</i>
<br>
<br>
<img src="https://github.com/garronej/keycloak-react-theming/workflows/ci/badge.svg?branch=develop">
<img src="https://img.shields.io/bundlephobia/minzip/keycloak-react-theming">
<img src="https://img.shields.io/npm/dw/keycloak-react-theming">
<img src="https://img.shields.io/npm/l/keycloak-react-theming">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=develop">
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
<img src="https://img.shields.io/npm/dw/keycloakify">
<img src="https://img.shields.io/npm/l/keycloakify">
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
</p>
**!!! This module is under active developement. It is not usable yet!!!**
<p align="center">
<i>Ultimately this build tool generates a Keycloak theme</i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
# Motivations
The problem:
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
It involves, however, a lot of raw JS/CSS/[FTL]() hacking, and bundling the theme is not exactly straightforward.
![keycloak_before](https://user-images.githubusercontent.com/6702424/108838381-dbbbcf80-75d3-11eb-8ae8-db41563ef9db.gif)
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.
When we redirected to Keycloak the user suffers from a harsh context switch.
On je login/register pages the language is set back to default and the theme is different that the one on the app.
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.
Keycloak does offer a way to customize theses pages but it requires a lot of raw HTML/CSS hacking
to reproduce the look and feel of a specific app. Not mentioning the maintenance cost of such an endeavour.
<p align="center">
<i>Without keycloakify, users suffers from a harsh context switch, no fronted form pre-validation</i><br>
<img src="https://user-images.githubusercontent.com/6702424/108838381-dbbbcf80-75d3-11eb-8ae8-db41563ef9db.gif">
</p>
Wouldn't it be great if we could just design the login and register pages as if they where part of our app while
still letting Keycloak handle the heavy lifting of actually authenticating the users?
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 🍸
Here is `yarn add keycloak-react-theming` 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>
TODO: Insert video after.
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
*NOTE: No autocomplete here just because it was an incognito window.*
If you already have a Keycloak custom theme, it can be easily ported to Keycloakify.
---
- [Motivations](#motivations)
- [Requirements](#requirements)
- [My framework doesnt seem to be supported, what can I do?](#my-framework-doesnt-seem-to-be-supported-what-can-i-do)
- [How to use](#how-to-use)
- [Setting up the build tool](#setting-up-the-build-tool)
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
- [Changing the look **and** feel](#changing-the-look-and-feel)
- [Hot reload](#hot-reload)
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
- [Support for Terms and conditions](#support-for-terms-and-conditions)
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
- [GitHub Actions](#github-actions)
- [Limitations](#limitations)
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported)
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-thesrc-dir)
- [Example of setup that **won't** work](#example-of-setup-that-wont-work)
- [Possible workarounds](#possible-workarounds)
- [Implement context persistence (optional)](#implement-context-persistence-optional)
- [Kickstart video](#kickstart-video)
- [Email domain whitelist](#email-domain-whitelist)
# Requirements
Tested with the following Keycloak versions:
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
(before you customize it) will always be the ones of Keycloak v11.
This tool assumes you are bundling your app with Webpack (tested with 4.44.2) .
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
and a `build/static/` directory generated by webpack.
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432)
## My framework doesnt seem to be supported, what can I do?
Currently Keycloakify is only compatible with `create-react-app` apps.
It doesnt 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
Add `keycloak-react-theming` to the dev dependencies of your project `npm install --save-dev keycloak-react-theming` or `yarn add --dev keycloak-react-theming`
then configure your `package.json` build's script to build the keycloak's theme by adding `&& build-keycloak-theme`.
Typically you will get:
`package.json`:
```json
"devDependencies": {
"keycloak-react-theming": "^0.0.10"
},
"scripts": {
"build": "react-scripts build && build-keycloak-theme"
},
```bash
yarn add keycloakify
```
Then build your app with `yarn run build` or `npm run build`, you will be provided with instructions
about how to load the theme into Keycloak.
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
```json
"scripts": {
"keycloak": "yarn build && build-keycloak-theme",
}
```
```bash
yarn keycloak # generates keycloak-theme.jar
```
On the console will be printed all the instructions about how to load the generated theme in Keycloak
### Changing just the look of the default Keycloak theme
The first approach is to only customize the style of the default Keycloak login by providing
your own class names.
If you have created a new React project specifically to create a Keycloak theme and nothing else then
your index should look something like:
`src/index.tsx`
```tsx
import { App } from "./<wherever>/App";
import {
KcApp,
defaultKcProps,
kcContext
} from "keycloakify";
import { css } from "tss-react";
const myClassName = css({ "color": "red" });
reactDom.render(
<KcApp
kcContext={kcContext}
{...{
...defaultKcProps,
"kcHeaderWrapperClass": myClassName
}}
/>
document.getElementById("root")
);
```
If you share a unique project for your app and the Keycloak theme, your index should look
more like this:
`src/index.tsx`
```tsx
import { App } from "./<wherever>/App";
import {
KcApp,
defaultKcProps,
kcContext
} from "keycloakify";
import { css } from "tss-react";
const myClassName = css({ "color": "red" });
reactDom.render(
// Unless the app is currently being served by Keycloak
// kcContext is undefined.
kcContext !== undefined ?
<KcApp
kcContext={kcContext}
{...{
...defaultKcProps,
"kcHeaderWrapperClass": myClassName
}}
/> :
<App />, // Your actual app
document.getElementById("root")
);
```
<p align="center">
<i>result:</i></br>
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
</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>
### Changing the look **and** feel
If you want to really re-implement the pages, the best approach is to
create your own version of the [`<KcApp />`](https://github.com/garronej/keycloakify/blob/develop/src/lib/components/KcApp.tsx).
Copy/past some of [the components](https://github.com/garronej/keycloakify/tree/develop/src/lib/components) provided by this module and start hacking around.
You can find an example of such customization [here](https://github.com/InseeFrLab/onyxia-ui/tree/master/src/app/components/KcApp).
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
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 release might break your app.
### Hot reload
Rebuild the theme each time you make a change to see the result is not practical.
If you want to test your login screens outside of Keycloak, in [storybook](https://storybook.js.org/)
for example you can use `kcContextMocks`.
```tsx
import {
KcApp,
defaultKcProps,
kcContextMocks
} from "keycloakify";
reactDom.render(
<KcApp
kcContext={kcContextMocks.kcLoginContext}
{...defaultKcProps}
/>
document.getElementById("root")
);
```
Then `yarn start`, you will see your login page.
Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-app/blob/main/src/index.tsx)
## Enable loading in a blink of an eye of login pages ⚡ (--external-assets)
By default the theme generated is standalone. Meaning that when your users
reach the login pages all scripts, images and stylesheet are downloaded from the Keycloak server.
If you are specifically building a theme to integrate with an app or a website that allows users
to first browse unauthenticated before logging in, you will get a significant
performance boost if you jump through those hoops:
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2)
- Build the theme using `npx build-keycloak-theme --external-assets` [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/.github/workflows/ci.yaml#L21)
- Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app.
- Make sure not to build your app and the keycloak theme separately
and remember to update the Keycloak theme every time you update your app.
- Be mindful that if your app is down your login pages are down as well.
Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-app#about-keycloakify)
# Support for Terms and conditions
[Many organizations have a requirement that when a new user logs in for the first time, they need to agree to the terms and conditions of the website.](https://www.keycloak.org/docs/4.8/server_admin/#terms-and-conditions).
First you need to enable the required action on the Keycloak server admin console:
![image](https://user-images.githubusercontent.com/6702424/114280501-dad2e600-9a39-11eb-9c39-a225572dd38a.png)
Then to load your own therms of services using [like this](https://github.com/garronej/keycloakify-demo-app/blob/8168c928a66605f2464f9bd28a4dc85fb0a231f9/src/index.tsx#L42-L66).
# Some pages still have the default theme. Why?
This project only support the most common user facing pages of Keycloak login.
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
If you need to customize pages that are not supported yet you can submit an issue about it and wait for me get it implemented.
If you can't wait, PR are welcome! [Here](https://github.com/InseeFrLab/keycloakify/commit/0163459ad6b1ad0afcc34fae5f3cc28dbcf8b4a7) is the commit that adds support
for the `login-otp.ftl` page. You can use it as a model for implementing other pages.
# GitHub Actions
![image](https://user-images.githubusercontent.com/6702424/114286938-47aea600-9a63-11eb-936e-17159e8826e8.png)
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
the building and publishing of the theme (the .jar file).
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
- For building the theme: `mvn` (Maven) must be installed (but you can build the theme in the CI)
- For testing the theme in a local Keycloak container (which is not mandatory for development):
`rm`, `mkdir`, `wget`, `unzip` are assumed to be available and `docker` up and running.
# Limitations
## `process.env.PUBLIC_URL` not supported.
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
(This isn't recommended anyway).
## `@font-face` importing fonts from the `src/` dir
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
this limitation doesn't apply, you can import fonts however you see fit.
### Example of setup that **won't** work
- We have a `fonts/` directory in `src/`
- We import the font like this [`src: url("/fonts/my-font.woff2") format("woff2");`](https://github.com/garronej/keycloakify-demo-app/blob/07d54a3012ef354ee12b1374c6f7ad1cb125d56b/src/fonts.scss#L4) in a `.scss` a file.
### Possible workarounds
- Use [`--external-assets`](#enable-loading-in-a-blink-of-a-eye-of-login-pages-).
- If it is possible, use Google Fonts or any other font provider.
- If you want to host your font recommended approach is to move your fonts into the `public`
directory and to place your `@font-face` statements in the `public/index.html`.
Example [here](https://github.com/InseeFrLab/onyxia-ui/blob/0e3a04610cfe872ca71dad59e05ced8f785dee4b/public/index.html#L6-L51).
- You can also [use non relative url](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/src/fonts.scss#L16) but don't forget [`Access-Control-Allow-Origin`](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/nginx.conf#L17-L19).
# Implement context persistence (optional)
If, before logging in, a user has selected a specific language
you don't want it to be reset to default when the user gets redirected to
the login or register pages.
Same goes for the dark mode, you don't want, if the user had it enabled
to show the login page with light themes.
The problem is that you are probably using `localStorage` to persist theses values across
reload but, as the Keycloak pages are not served on the same domain that the rest of your
app you won't be able to carry over states using `localStorage`.
The only reliable solution is to inject parameters into the URL before
redirecting to Keycloak. We integrate with
[`keycloak-js`](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc),
by providing you a way to tell `keycloak-js` that you would like to inject
some search parameters before redirecting.
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
You can implement your own mechanism to pass the states in the URL and
restore it on the other side but we recommend using `powerhooks/useGlobalState`
from the library [`powerhooks`](https://www.powerhooks.dev) that provide an elegant
way to handle states such as `isDarkModeEnabled` or `selectedLanguage`.
Let's modify [the example](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc) from the official `keycloak-js` documentation to
enables the states of `useGlobalStates` to be injected in the URL before redirecting.
Note that the states are automatically restored on the other side by `powerhooks`
```typescript
import keycloak_js from "keycloak-js";
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
import { createKeycloakAdapter } from "keycloakify";
//...
const keycloakInstance = keycloak_js({
"url": "http://keycloak-server/auth",
"realm": "myrealm",
"clientId": "myapp"
});
keycloakInstance.init({
"onLoad": 'check-sso',
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html",
"adapter": createKeycloakAdapter({
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
keycloakInstance
})
});
//...
```
If you really want to go the extra miles and avoid having the white
flash of the blank html before the js bundle have been evaluated
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
# Kickstart video
*NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded*
[![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8)
## Developing your login and register pages in your React app
# Email domain whitelist
TODO
# How to implement context persistance
If you want dark mode preference, language and others users preferences your can do so
very easily by using [`powerhooks/useGlobalState`](https://github.com/garronej/powerhooks)
WARNING: `powerhooks` is still a work in progress.
# REQUIREMENTS
This tools 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 `static/` directory generated by webpack.
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3=)
- For building the theme: `mvn` (Maven) must be installed
- For development, (testing the theme in a local container ): `rm`, `mkdir`, `wget`, `unzip` are assumed to be available
and `docker` up and running.
NOTE: This build tool has only be tested on MacOS.
# API Reference
## The build tool
Part of the lib that runs with node, at build time.
- `npx build-keycloak-theme`: Builds the theme, the CWD is assumed to be the root of your react project.
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
## The fronted lib ( imported into your react app )
Part of the lib that you import in your react project and runs on the browser.
**TODO**
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
and `kcRegisterContext["authorizedMailDomains"]` to validate on.

View File

@ -1,10 +1,10 @@
{
"name": "keycloak-react-theming",
"version": "0.0.21",
"name": "keycloakify",
"version": "1.0.3",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
"url": "git://github.com/garronej/keycloak-react-theming.git"
"url": "git://github.com/garronej/keycloakify.git"
},
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
@ -12,15 +12,13 @@
"clean": "rimraf dist/",
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"test": "node dist/test/build-keycloak-theme.js && node dist/test/download-sample-keycloak-themes.js",
"enable_short_import_path": "yarn build && denoify_enable_short_npm_import_path",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml dist/",
"generate-messages": "node dist/bin/generate-i18n-messages.js",
"watch": "tsc -w"
"test": "node dist/test",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
"generate-messages": "node dist/bin/generate-i18n-messages.js"
},
"bin": {
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
"download-sample-keycloak-themes": "dist/bin/download-sample-keycloak-themes.js"
"install-builtin-keycloak-themes": "dist/bin/install-builtin-keycloak-themes.js"
},
"author": "u/garronej",
"license": "MIT",
@ -40,21 +38,25 @@
"login",
"register"
],
"homepage": "https://github.com/garronej/keycloak-react-theming",
"homepage": "https://github.com/garronej/keycloakify",
"devDependencies": {
"@types/node": "^10.0.0",
"copyfiles": "^2.4.1",
"denoify": "^0.6.5",
"properties-parser": "^0.3.1",
"rimraf": "^3.0.2",
"scripting-tools": "^0.19.13",
"typescript": "^4.1.5",
"@types/react": "^17.0.0",
"react": "^17.0.1"
"copyfiles": "^2.4.1",
"properties-parser": "^0.3.1",
"react": "^17.0.1",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
},
"dependencies": {
"cheerio": "^1.0.0-rc.5",
"evt": "^1.9.12",
"powerhooks": "^0.0.14"
"evt": "2.0.0-beta.15",
"markdown": "^0.5.0",
"minimal-polyfills": "^2.1.6",
"path": "^0.12.7",
"powerhooks": "^0.0.36",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tss-react": "^0.0.12"
}
}

View File

@ -0,0 +1,2 @@
export const ftlValuesGlobalName = "kcContext";

View File

@ -0,0 +1,28 @@
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;
}
}
);

View File

@ -0,0 +1,26 @@
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
var unes = {
'&amp;': '&',
'&#38;': '&',
'&lt;': '<',
'&#60;': '<',
'&gt;': '>',
'&#62;': '>',
'&apos;': "'",
'&#39;': "'",
'&quot;': '"',
'&#34;': '"'
};
var cape = function (m) { return unes[m]; };
Object.defineProperty(
String,
"htmlUnescape",
{
"value": function (un) {
return String.prototype.replace.call(un, es, cape);
}
}
);

View File

@ -0,0 +1,263 @@
<script>const _=
{
"url": {
"loginAction": (function (){
<#attempt>
return "${url.loginAction?no_esc}";
<#recover>
</#attempt>
})(),
"resourcesPath": (function (){
<#attempt>
return "${url.resourcesPath?no_esc}";
<#recover>
</#attempt>
})(),
"resourcesCommonPath": (function (){
<#attempt>
return "${url.resourcesCommonPath?no_esc}";
<#recover>
</#attempt>
})(),
"loginRestartFlowUrl": (function (){
<#attempt>
return "${url.loginRestartFlowUrl?no_esc}";
<#recover>
</#attempt>
})(),
"loginUrl": (function (){
<#attempt>
return "${url.loginUrl?no_esc}";
<#recover>
</#attempt>
})()
},
"realm": {
"displayName": (function (){
<#attempt>
return "${realm.displayName!''}" || undefined;
<#recover>
</#attempt>
})(),
"displayNameHtml": (function (){
<#attempt>
return "${realm.displayNameHtml!''}" || undefined;
<#recover>
</#attempt>
})(),
"internationalizationEnabled": (function (){
<#attempt>
return ${realm.internationalizationEnabled?c};
<#recover>
</#attempt>
})(),
"registrationEmailAsUsername": (function (){
<#attempt>
return ${realm.registrationEmailAsUsername?c};
<#recover>
</#attempt>
})()
},
"locale": (function (){
<#attempt>
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
var out= [];
<#attempt>
<#list locale.supported as lng>
out.push({
"url": (function (){
<#attempt>
return "${lng.url?no_esc}";
<#recover>
</#attempt>
})(),
"label": (function (){
<#attempt>
return "${lng.label}";
<#recover>
</#attempt>
})(),
"languageTag": (function (){
<#attempt>
return "${lng.languageTag}";
<#recover>
</#attempt>
})()
});
</#list>
<#recover>
</#attempt>
return out;
})(),
"current": (function (){
<#attempt>
return "${locale.current}";
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})(),
"auth": (function (){
<#attempt>
<#if auth?has_content>
var out= {
"showUsername": (function (){
<#attempt>
return ${auth.showUsername()?c};
<#recover>
</#attempt>
})(),
"showResetCredentials": (function (){
<#attempt>
return ${auth.showResetCredentials()?c};
<#recover>
</#attempt>
})(),
"showTryAnotherWayLink": (function(){
<#attempt>
return ${auth.showTryAnotherWayLink()?c};
<#recover>
</#attempt>
})()
};
<#attempt>
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": (function (){
<#attempt>
return "${auth.attemptedUsername}";
<#recover>
</#attempt>
})()
}
);
</#if>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})(),
"scripts": (function(){
var out = [];
<#attempt>
<#if scripts??>
<#attempt>
<#list scripts as script>
out.push((function (){
<#attempt>
return "${script}";
<#recover>
</#attempt>
})());
</#list>
<#recover>
</#attempt>
</#if>
<#recover>
</#attempt>
return out;
})(),
"message": (function (){
<#attempt>
<#if message?has_content>
return { 
"type": (function (){
<#attempt>
return "${message.type}";
<#recover>
</#attempt>
})(),
"summary": (function (){
<#attempt>
return String.htmlUnescape("${message.summary}");
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})(),
"isAppInitiatedAction": (function (){
<#attempt>
<#if isAppInitiatedAction??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
}
</script>

View File

@ -0,0 +1,23 @@
<script>const _=
{
"client": (function (){
<#attempt>
<#if client??>
return {
"baseUrl": (function (){
<#attempt>
return "${(client.baseUrl!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})()
}
</script>

View File

@ -1,113 +0,0 @@
<script>const _=
{
"url": {
"loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}"
},
"realm": {
"displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || undefined,
"internationalizationEnabled": ${realm.internationalizationEnabled?c}
},
"locale": (function (){
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
<#if realm.internationalizationEnabled>
var out= [];
<#list locale.supported as lng>
out.push({
"url": "${lng.url}",
"label": "${lng.label}",
"languageTag": "${lng.languageTag}"
});
</#list>
return out;
</#if>
return undefined;
})(),
"current": "${locale.current}"
};
</#if>
return undefined;
})(),
"auth": (function (){
<#if auth?has_content>
var out= {
"showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c}
};
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": "${auth.attemptedUsername}"
}
);
</#if>
return out;
</#if>
return undefined;
})(),
"scripts": (function(){
var out = [];
<#if scripts??>
<#list scripts as script>
out.push("${script}");
</#list>
</#if>
return out;
})(),
"message": (function (){
<#if message?has_content>
return { 
"type": "${message.type}",
"summary": "${kcSanitize(message.summary)?no_esc}"
};
</#if>
return undefined;
})(),
"isAppInitiatedAction": (function (){
<#if isAppInitiatedAction??>
return true;
</#if>
return false;
})()
}
</script>

View File

@ -2,36 +2,76 @@
import cheerio from "cheerio";
import {
replaceImportFromStaticInJsCode,
replaceImportsFromStaticInJsCode,
replaceImportsInInlineCssCode,
generateCssCodeToDefineGlobals
} from "../replaceImportFromStatic";
import fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
export const pageIds = [
"login.ftl", "register.ftl", "info.ftl",
"error.ftl", "login-reset-password.ftl",
"login-verify-email.ftl", "terms.ftl",
"login-otp.ftl"
] as const;
export type PageId = typeof pageIds[number];
function loadAdjacentFile(fileBasename: string) {
return fs.readFileSync(pathJoin(__dirname, fileBasename))
.toString("utf8");
};
function loadFtlFile(ftlFileBasename: PageId | "common.ftl") {
try {
return loadAdjacentFile(ftlFileBasename)
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
} catch {
return "{}";
}
}
export function generateFtlFilesCodeFactory(
params: {
ftlValuesGlobalName: string;
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
urlPathname: string;
urlOrigin: undefined | string;
}
) {
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode } = params;
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
const $ = cheerio.load(indexHtmlCode);
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
ftlValuesGlobalName,
"jsCode": $(element).html()!
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"],
@ -41,78 +81,103 @@ export function generateFtlFilesCodeFactory(
const href = $(element).attr(attrName);
if (!href?.startsWith("/")) {
if (href === undefined) {
return;
}
$(element).attr(attrName, "${url.resourcesPath}" + href);
$(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": "xIdLqMeOed9sdLdIdOxdK0d" }':
fs.readFileSync(pathJoin(__dirname, "ftl2js.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
const ftlCommonPlaceholders = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("common.ftl"),
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
[
'<#if scripts??>',
' <#list scripts as script>',
' <script src="${script}" type="text/javascript"></script>',
' </#list>',
'</#if>',
'</#if>'
].join("\n")
};
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
$("head").prepend(
[
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
'',
'<style>',
generateCssCodeToDefineGlobals(
{ cssGlobalsToDefine }
).cssCodeToPrependInHead,
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
urlPathname
}).cssCodeToPrependInHead,
'</style>',
''
]),
...["Object.deepAssign.js", "String.htmlUnescape.js"].map(
fileBasename => [
"<script>",
loadAdjacentFile(fileBasename),
"</script>"
].join("\n")
),
'<script>',
' Object.assign(',
` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
` window.${ftlValuesGlobalName}= Object.assign(`,
` {},`,
` ${objectKeys(ftlCommonPlaceholders)[0]}`,
' );',
'</script>',
'',
objectKeys(ftlPlaceholders)[1],
''
pageSpecificCodePlaceholder,
'',
objectKeys(ftlCommonPlaceholders)[1]
].join("\n"),
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(
params: {
pageBasename: "login.ftl" | "register.ftl"
pageId: PageId;
}
): { ftlCode: string; } {
const { pageBasename } = params;
const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
$("head").prepend(
[
'',
'<script>',
` window.${ftlValuesGlobalName} = { "pageBasename": "${pageBasename}" };`,
'</script>',
''
].join("\n")
);
const ftlPlaceholders = {
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageId),
...ftlCommonPlaceholders
};
let ftlCode = $.html();
let ftlCode = $.html()
.replace(
pageSpecificCodePlaceholder,
[
'<script>',
` Object.deepAssign(`,
` window.${ftlValuesGlobalName},`,
` { "pageId": "${pageId}" }`,
' );',
` Object.deepAssign(`,
` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
' );',
'</script>'
].join("\n")
);
objectKeys(ftlPlaceholders)
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id]));

View File

@ -0,0 +1,82 @@
<script>const _=
{
"messageHeader": (function (){
<#attempt>
return "${messageHeader!''}" || undefined;
<#recover>
</#attempt>
})(),
"requiredActions": (function (){
<#attempt>
<#if requiredActions??>
var out =[];
<#attempt>
<#list requiredActions>
<#attempt>
<#items as reqActionItem>
out.push((function (){
<#attempt>
return "${reqActionItem}";
<#recover>
</#attempt>
})());
</#items>
<#recover>
</#attempt>
</#list>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})(),
"skipLink": (function (){
<#attempt>
<#if skipLink??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"pageRedirectUri": (function (){
<#attempt>
return "${(pageRedirectUri!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})(),
"actionUri": (function (){
<#attempt>
return "${(actionUri!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})(),
"client": {
"baseUrl": (function(){
<#attempt>
return "${(client.baseUrl!'')?no_esc}" || undefined;
<#recover>
</#attempt>
})()
}
}
</script>

View File

@ -0,0 +1,37 @@
<script>const _=
{
"otpLogin": {
"userOtpCredentials": (function(){
var out = [];
<#attempt>
<#list otpLogin.userOtpCredentials as otpCredential>
out.push({
"id": (function (){
<#attempt>
return "${otpCredential.id}";
<#recover>
</#attempt>
})(),
"userLabel": (function (){
<#attempt>
return "${otpCredential.userLabel}";
<#recover>
</#attempt>
})()
});
</#list>
<#recover>
</#attempt>
return out;
})()
}
}
</script>

View File

@ -0,0 +1,14 @@
<script>const _=
{
"realm": {
"loginWithEmailAllowed": (function (){
<#attempt>
return ${realm.loginWithEmailAllowed?c};
<#recover>
</#attempt>
})()
}
}
</script>

View File

@ -0,0 +1,160 @@
<script>const _=
{
"url": {
"loginResetCredentialsUrl": (function (){
<#attempt>
return "${url.loginResetCredentialsUrl?no_esc}";
<#recover>
</#attempt>
})(),
"registrationUrl": (function (){
<#attempt>
return "${url.registrationUrl?no_esc}";
<#recover>
</#attempt>
})()
},
"realm": {
"loginWithEmailAllowed": (function(){
<#attempt>
return ${realm.loginWithEmailAllowed?c};
<#recover>
</#attempt>
})(),
"rememberMe": (function (){
<#attempt>
return ${realm.rememberMe?c};
<#recover>
</#attempt>
})(),
"password": (function (){
<#attempt>
return ${realm.password?c};
<#recover>
</#attempt>
})(),
"resetPasswordAllowed": (function (){
<#attempt>
return ${realm.resetPasswordAllowed?c};
<#recover>
</#attempt>
})(),
"registrationAllowed": (function (){
<#attempt>
return ${realm.registrationAllowed?c};
<#recover>
</#attempt>
})()
},
"auth": (function (){
<#attempt>
<#if auth?has_content>
return {
"selectedCredential": (function (){
<#attempt>
return "${auth.selectedCredential!''}" || undefined;
<#recover>
</#attempt>
})()
};
</#if>
<#recover>
</#attempt>
})(),
"social": {
"displayInfo": (function (){
<#attempt>
return ${social.displayInfo?c};
<#recover>
</#attempt>
})(),
"providers": (()=>{
<#attempt>
<#if social.providers??>
var out= [];
<#attempt>
<#list social.providers as p>
out.push({
"loginUrl": (function (){
<#attempt>
return "${p.loginUrl?no_esc}";
<#recover>
</#attempt>
})(),
"alias": (function (){
<#attempt>
return "${p.alias}";
<#recover>
</#attempt>
})(),
"providerId": (function (){
<#attempt>
return "${p.providerId}";
<#recover>
</#attempt>
})(),
"displayName": (function (){
<#attempt>
return "${p.displayName}";
<#recover>
</#attempt>
})()
});
</#list>
<#recover>
</#attempt>
return out;
</#if>
<#recover>
</#attempt>
})()
},
"usernameEditDisabled": (function () {
<#attempt>
<#if usernameEditDisabled??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"login": {
"username": (function (){
<#attempt>
return "${login.username!''}" || undefined;
<#recover>
</#attempt>
})(),
"rememberMe": (function (){
<#attempt>
<#if login.rememberMe??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
},
"registrationDisabled": (function (){
<#attempt>
<#if registrationDisabled??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})()
}
</script>

View File

@ -0,0 +1,189 @@
<script>const _=
{
"url": {
"registrationAction": (function (){
<#attempt>
return "${url.registrationAction?no_esc}";
<#recover>
</#attempt>
})()
},
"messagesPerField": {
"printIfExists": function (key, x) {
switch(key){
case "userLabel": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "username": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "email": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "firstName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "lastName": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
case "password-confirm": return (function (){
<#attempt>
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
<#recover>
</#attempt>
})();
}
}
},
"register": {
"formData": {
"firstName": (function (){
<#attempt>
return "${register.formData.firstName!''}" || undefined;
<#recover>
</#attempt>
})(),
"displayName": (function (){
<#attempt>
return "${register.formData.displayName!''}" || undefined;
<#recover>
</#attempt>
})(),
"lastName": (function (){
<#attempt>
return "${register.formData.lastName!''}" || undefined;
<#recover>
</#attempt>
})(),
"email": (function(){
<#attempt>
return "${register.formData.email!''}" || undefined;
<#recover>
</#attempt>
})(),
"username": (function (){
<#attempt>
return "${register.formData.username!''}" || undefined;
<#recover>
</#attempt>
})()
}
},
"passwordRequired": (function (){
<#attempt>
<#if passwordRequired??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"recaptchaRequired": (function (){
<#attempt>
<#if passwordRequired??>
return true;
</#if>
<#recover>
</#attempt>
return false;
})(),
"recaptchaSiteKey": (function (){
<#attempt>
return "${recaptchaSiteKey!''}" || undefined;
<#recover>
</#attempt>
})(),
"authorizedMailDomains": (function (){
<#attempt>
return "${authorizedMailDomains!''}" || undefined;
<#recover>
</#attempt>
})(),
"authorizedMailDomains": (function(){
var out = undefined;
<#attempt>
<#if authorizedMailDomains??>
out = [];
<#attempt>
<#list authorizedMailDomains as authorizedMailDomain>
out.push((function (){
<#attempt>
return "${authorizedMailDomain}";
<#recover>
</#attempt>
})());
</#list>
<#recover>
</#attempt>
</#if>
<#recover>
</#attempt>
return out;
})(),
}
</script>

View File

@ -32,7 +32,7 @@ export function generateJavaStackFiles(
return (!homepage ?
fallbackGroupId :
url.parse(homepage).host?.split(".").reverse().join(".") ?? fallbackGroupId
url.parse(homepage).host?.replace(/:[0-9]+$/,"")?.split(".").reverse().join(".") ?? fallbackGroupId
) + ".keycloak";
})();

View File

@ -3,35 +3,56 @@ import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin } from "path";
import {
replaceImportFromStaticInCssCode,
replaceImportFromStaticInJsCode
replaceImportsInCssCode,
replaceImportsFromStaticInJsCode
} from "./replaceImportFromStatic";
import { generateFtlFilesCodeFactory } from "./generateFtl";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
import * as child_process from "child_process";
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/kcContextMocks/urlResourcesPath";
import { isInside } from "../tools/isInside";
export const ftlValuesGlobalName = "keycloakPagesContext";
export function generateKeycloakThemeResources(
params: {
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
urlPathname: string;
//If urlOrigin is not undefined then it means --externals-assets
urlOrigin: undefined | string;
}
) {
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath } = params;
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname, urlOrigin } = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({
"destDirPath": pathJoin(themeDirPath, "resources"),
"destDirPath":
urlOrigin === undefined ?
pathJoin(themeDirPath, "resources", "build") :
reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCodeString": ({ filePath, sourceCode }) => {
"transformSourceCode": ({ filePath, sourceCode }) => {
if (/\.css?$/i.test(filePath)) {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
urlOrigin === undefined &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, subDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode(
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode(
{ "cssCode": sourceCode.toString("utf8") }
);
@ -46,42 +67,102 @@ export function generateKeycloakThemeResources(
if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
ftlValuesGlobalName
urlOrigin
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return { "modifiedSourceCode": sourceCode };
return urlOrigin === undefined ?
{ "modifiedSourceCode": sourceCode } :
undefined;
}
});
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine,
ftlValuesGlobalName,
"indexHtmlCode": fs.readFileSync(
pathJoin(reactAppBuildDirPath, "index.html")
).toString("utf8")
).toString("utf8"),
urlPathname,
urlOrigin
});
(["login.ftl", "register.ftl"] as const).forEach(pageBasename => {
pageIds.forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageBasename });
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(
pathJoin(themeDirPath, pageBasename),
pathJoin(themeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
)
);
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
downloadAndUnzip({
"url": builtinThemesUrl,
"destDirPath": tmpDirPath
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(
reactAppPublicDirPath,
resourcesPath
)
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(
reactAppPublicDirPath,
resourcesCommonPath
)
});
const keycloakResourcesWithinPublicDirPath =
pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from([
"This is just a test folder that helps develop",
"the login and register page without having to yarn build"
].join(" "))
);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"),
Buffer.from("*", "utf8")
);
child_process.execSync(`rm -r ${tmpDirPath}`);
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from("parent=base\n", "utf8")
Buffer.from("parent=keycloak", "utf8")
);
}

View File

@ -6,10 +6,13 @@ import type { ParsedPackageJson } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import * as child_process from "child_process";
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
import { URL } from "url";
const reactProjectDirPath = process.cwd();
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
@ -22,7 +25,38 @@ if (require.main === module) {
generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
"themeName": parsedPackageJson.name
"themeName": parsedPackageJson.name,
...(() => {
const url = (() => {
const { homepage } = parsedPackageJson;
return homepage === undefined ?
undefined :
new URL(homepage);
})();
return {
"urlPathname":
url === undefined ?
"/" :
url.pathname.replace(/([^/])$/, "$1/"),
"urlOrigin": !doUseExternalAssets ? undefined : (() => {
if (url === undefined) {
console.error("ERROR: You must specify 'homepage' in your package.json");
process.exit(-1);
}
return url.origin;
})()
};
})()
});
const { jarFilePath } = generateJavaStackFiles({

View File

@ -1,25 +1,52 @@
import * as crypto from "crypto";
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
export function replaceImportFromStaticInJsCode(
export function replaceImportsFromStaticInJsCode(
params: {
ftlValuesGlobalName: string;
jsCode: string;
urlOrigin: undefined | string;
}
): { fixedJsCode: string; } {
const { jsCode, ftlValuesGlobalName } = params;
const { jsCode, urlOrigin } = params;
const fixedJsCode = jsCode!.replace(
/"static\//g,
`window.${ftlValuesGlobalName}.url.resourcesPath.replace(/^\\//,"") + "/" + "static/`
const fixedJsCode = jsCode.replace(
/([a-z]+\.[a-z]+)\+"static\//g,
(...[, group]) =>
urlOrigin === undefined ?
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
);
return { fixedJsCode };
}
export function replaceImportFromStaticInCssCode(
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;
}
@ -32,7 +59,7 @@ export function replaceImportFromStaticInCssCode(
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/(url\(\/[^)]+\))/g) ?? [])
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? [])
.forEach(match =>
cssGlobalsToDefine[
"url" + crypto
@ -60,12 +87,13 @@ export function replaceImportFromStaticInCssCode(
export function generateCssCodeToDefineGlobals(
params: {
cssGlobalsToDefine: Record<string, string>;
urlPathname: string;
}
): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine } = params;
const { cssGlobalsToDefine, urlPathname } = params;
return {
"cssCodeToPrependInHead": [
@ -73,12 +101,8 @@ export function generateCssCodeToDefineGlobals(
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName => [
`--${cssVariableName}:`,
[
"url(",
"${url.resourcesPath}" +
cssGlobalsToDefine[cssVariableName].match(/^url\(([^)]+)\)$/)![1],
")"
].join("")
cssGlobalsToDefine[cssVariableName]
.replace(new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"), "url(${url.resourcesPath}/build/")
].join(" "))
.map(line => ` ${line};`),
"}"

View File

@ -1,19 +0,0 @@
#!/usr/bin/env node
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
export const keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl =
"https://github.com/garronej/keycloak-react-theming/releases/download/v0.0.1/other_keycloak_thems.zip";
if (require.main === module) {
console.log("execute!");
downloadAndUnzip({
"url": keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl,
"destDirPath": keycloakThemeBuildingDirPath
});
}

View File

@ -1,23 +1,22 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { crawl } from "./tools/crawl";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl } from "./download-sample-keycloak-themes";
import { builtinThemesUrl } from "./install-builtin-keycloak-themes";
import { getProjectRoot } from "./tools/getProjectRoot";
import * as child_process from "child_process";
//@ts-ignore
const propertiesParser = require("properties-parser");
console.log(propertiesParser);
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
child_process.execSync(`rm -rf ${tmpDirPath}`);
downloadAndUnzip({
"destDirPath": tmpDirPath,
"url": keycloakBuiltinThemesAndThirdPartyExamplesThemsUrl
"url": builtinThemesUrl
});
type Dictionary = { [idiomId: string]: string };
@ -36,29 +35,42 @@ crawl(".").forEach(filePath => {
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language] =
propertiesParser.parse(
fs.readFileSync(filePath)
.toString("utf8")
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] =
Object.fromEntries(
Object.entries(
propertiesParser.parse(
fs.readFileSync(filePath)
.toString("utf8")
)
).map(([key, value]: any) => [key, value.replace(/''/g, "'")])
);
});
child_process.execSync(`rm -r ${tmpDirPath}`);
const targetFilePath = pathJoin("src", "lib", "i18n", "messages.generated.ts");
const targetDirPath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_kcMessages");
fs.writeFileSync(
pathJoin(getProjectRoot(), targetFilePath),
Buffer.from(
[
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
'//PLEASE DO NOT EDIT MANUALLY',
'',
'/* spell-checker: disable */',
`export const messages= ${JSON.stringify(record, null, 2)} as const;`,
'/* spell-checker: enable */'
].join("\n"), "utf8")
);
fs.mkdirSync(targetDirPath, { "recursive": true });
console.log(`${targetFilePath} wrote`);
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`);
});

View File

@ -0,0 +1,18 @@
#!/usr/bin/env node
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { join as pathJoin } from "path";
export const builtinThemesUrl =
"https://github.com/garronej/keycloakify/releases/download/v0.0.1/keycloak_11.0.3_builtin_themes_with_light_mods.zip";
if (require.main === module) {
downloadAndUnzip({
"url": builtinThemesUrl,
"destDirPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme")
});
}

View File

@ -1,8 +1,10 @@
import { basename as pathBasename } from "path";
import child_process from "child_process";
import { basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process";
import fs from "fs";
import { transformCodebase } from "../tools/transformCodebase";
/** assert url ends with .zip */
export function downloadAndUnzip(
params: {
url: string;
@ -12,11 +14,21 @@ export function downloadAndUnzip(
const { url, destDirPath } = params;
fs.mkdirSync(destDirPath, { "recursive": true });
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
[
`wget ${url}`,
...["unzip", "rm"].map(prg => `${prg} ${pathBasename(url)}`),
].forEach(cmd => child_process.execSync(cmd, { "cwd": destDirPath }));
execSync(`rm -rf ${tmpDirPath}`);
fs.mkdirSync(tmpDirPath, { "recursive": true });
execSync(`wget ${url}`, { "cwd": tmpDirPath })
execSync(`unzip ${pathBasename(url)}`, { "cwd": tmpDirPath });
execSync(`rm ${pathBasename(url)}`, { "cwd": tmpDirPath });
transformCodebase({
"srcDirPath": tmpDirPath,
"destDirPath": destDirPath,
});
execSync(`rm -r ${tmpDirPath}`);
}

14
src/bin/tools/isInside.ts Normal file
View File

@ -0,0 +1,14 @@
import { relative as pathRelative } from "path";
export function isInside(
params: {
dirPath: string;
filePath: string;
}
) {
const { dirPath, filePath } = params;
return !pathRelative(dirPath, filePath).startsWith("..");
}

View File

@ -3,34 +3,42 @@
import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
import { id } from "evt/tools/typeSafety/id";
/** Apply a transformation function to every file of directory */
export function transformCodebase(
params: {
srcDirPath: string;
destDirPath: string;
transformSourceCodeString: (params: {
type TransformSourceCode =
(params: {
sourceCode: Buffer;
filePath: string;
}) => {
modifiedSourceCode: Buffer;
newFileName?: string;
} | undefined;
/** Apply a transformation function to every file of directory */
export function transformCodebase(
params: {
srcDirPath: string;
destDirPath: string;
transformSourceCode?: TransformSourceCode;
}
) {
const { srcDirPath, destDirPath, transformSourceCodeString } = params;
const {
srcDirPath,
destDirPath,
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({ "modifiedSourceCode": sourceCode }))
} = params;
for (const file_relative_path of crawl(srcDirPath)) {
const filePath = path.join(srcDirPath, file_relative_path);
const transformSourceCodeStringResult = transformSourceCodeString({
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path)
});
if (transformSourceCodeStringResult === undefined) {
if (transformSourceCodeResult === undefined) {
continue;
}
@ -44,7 +52,7 @@ export function transformCodebase(
{ "recursive": true }
);
const { newFileName, modifiedSourceCode } = transformSourceCodeStringResult;
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(
path.join(

View File

@ -0,0 +1,39 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContext.Error; } & KcProps) => {
const { msg } = useKcMessage();
assert(kcContext.message !== undefined);
const { message, client } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{
client !== undefined && client.baseUrl !== undefined &&
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
}
</div>
}
/>
);
});

View File

@ -0,0 +1,72 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContext.Info; } & KcProps) => {
const { msg } = useKcMessage();
assert(kcContext.message !== undefined);
const {
messageHeader,
message,
requiredActions,
skipLink,
pageRedirectUri,
actionUri,
client
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
displayMessage={false}
headerNode={
messageHeader !== undefined ?
<>{messageHeader}</>
:
<>{message.summary}</>
}
formNode={
<div id="kc-info-message">
<p className="instruction">{message.summary}
{
requiredActions !== undefined &&
<b>
{
requiredActions
.map(requiredAction => msg(`requiredAction.${requiredAction}` as const))
.join(",")
}
</b>
}
</p>
{
!skipLink &&
pageRedirectUri !== undefined ?
<p><a href={pageRedirectUri}>{(msg("backToApplication"))}</a></p>
:
actionUri !== undefined ?
<p><a href={actionUri}>{msg("proceedWithAction")}</a></p>
:
client.baseUrl !== undefined &&
<p><a href={client.baseUrl}>{msg("backToApplication")}</a></p>
}
</div>
}
/>
);
});

View File

@ -0,0 +1,25 @@
import { memo } from "react";
import type { KcContext } from "../KcContext";
import type { KcProps } from "./KcProps";
import { Login } from "./Login";
import { Register } from "./Register";
import { Info } from "./Info";
import { Error } from "./Error";
import { LoginResetPassword } from "./LoginResetPassword";
import { LoginVerifyEmail } from "./LoginVerifyEmail";
import { Terms } from "./Terms";
import { LoginOtp } from "./LoginOtp";
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } & KcProps ) => {
switch (kcContext.pageId) {
case "login.ftl": return <Login {...{ kcContext, ...props }} />;
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
case "info.ftl": return <Info {...{ kcContext, ...props }} />;
case "error.ftl": return <Error {...{ kcContext, ...props }} />;
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />;
case "terms.ftl": return <Terms {...{ kcContext, ...props }}/>;
case "login-otp.ftl": return <LoginOtp {...{ kcContext, ...props }}/>;
}
});

View File

@ -0,0 +1,205 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { doExtends } from "evt/tools/typeSafety/doExtends";
/** Class names can be provided as an array or separated by whitespace */
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };
export type KcTemplateClassKey =
"stylesCommon" |
"styles" |
"scripts" |
"kcHtmlClass" |
"kcLoginClass" |
"kcHeaderClass" |
"kcHeaderWrapperClass" |
"kcFormCardClass" |
"kcFormCardAccountClass" |
"kcFormHeaderClass" |
"kcLocaleWrapperClass" |
"kcContentWrapperClass" |
"kcLabelWrapperClass" |
"kcContentWrapperClass" |
"kcLabelWrapperClass" |
"kcFormGroupClass" |
"kcResetFlowIcon" |
"kcResetFlowIcon" |
"kcFeedbackSuccessIcon" |
"kcFeedbackWarningIcon" |
"kcFeedbackErrorIcon" |
"kcFeedbackInfoIcon" |
"kcContentWrapperClass" |
"kcFormSocialAccountContentClass" |
"kcFormSocialAccountClass" |
"kcSignUpClass" |
"kcInfoAreaWrapperClass"
;
export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>;
export const defaultKcTemplateProps = {
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"scripts": [],
"kcHtmlClass": ["login-pf"],
"kcLoginClass": ["login-pf-page"],
"kcContentWrapperClass": ["row"],
"kcHeaderClass": ["login-pf-page-header"],
"kcHeaderWrapperClass": [],
"kcFormCardClass": ["card-pf"],
"kcFormCardAccountClass": ["login-pf-accounts"],
"kcFormSocialAccountClass": ["login-pf-social-section"],
"kcFormSocialAccountContentClass": ["col-xs-12", "col-sm-6"],
"kcFormHeaderClass": ["login-pf-header"],
"kcLocaleWrapperClass": [],
"kcFeedbackErrorIcon": ["pficon", "pficon-error-circle-o"],
"kcFeedbackWarningIcon": ["pficon", "pficon-warning-triangle-o"],
"kcFeedbackSuccessIcon": ["pficon", "pficon-ok"],
"kcFeedbackInfoIcon": ["pficon", "pficon-info"],
"kcResetFlowIcon": ["pficon", "pficon-arrow fa-2x"],
"kcFormGroupClass": ["form-group"],
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcSignUpClass": ["login-pf-signup"],
"kcInfoAreaWrapperClass": []
} as const;
doExtends<typeof defaultKcTemplateProps, KcTemplateProps>();
/** Tu use if you don't want any default */
export const allClearKcTemplateProps =
allPropertiesValuesToUndefined(defaultKcTemplateProps);
doExtends<typeof allClearKcTemplateProps, KcTemplateProps>();
export type KcProps = KcPropsGeneric<
KcTemplateClassKey |
"kcLogoLink" |
"kcLogoClass" |
"kcContainerClass" |
"kcContentClass" |
"kcFeedbackAreaClass" |
"kcLocaleClass" |
"kcAlertIconClasserror" |
"kcFormAreaClass" |
"kcFormSocialAccountListClass" |
"kcFormSocialAccountDoubleListClass" |
"kcFormSocialAccountListLinkClass" |
"kcWebAuthnKeyIcon" |
"kcFormClass" |
"kcFormGroupErrorClass" |
"kcLabelClass" |
"kcInputClass" |
"kcInputWrapperClass" |
"kcFormOptionsClass" |
"kcFormButtonsClass" |
"kcFormSettingClass" |
"kcTextareaClass" |
"kcInfoAreaClass" |
"kcButtonClass" |
"kcButtonPrimaryClass" |
"kcButtonDefaultClass" |
"kcButtonLargeClass" |
"kcButtonBlockClass" |
"kcInputLargeClass" |
"kcSrOnlyClass" |
"kcSelectAuthListClass" |
"kcSelectAuthListItemClass" |
"kcSelectAuthListItemInfoClass" |
"kcSelectAuthListItemLeftClass" |
"kcSelectAuthListItemBodyClass" |
"kcSelectAuthListItemDescriptionClass" |
"kcSelectAuthListItemHeadingClass" |
"kcSelectAuthListItemHelpTextClass" |
"kcAuthenticatorDefaultClass" |
"kcAuthenticatorPasswordClass" |
"kcAuthenticatorOTPClass" |
"kcAuthenticatorWebAuthnClass" |
"kcAuthenticatorWebAuthnPasswordlessClass" |
"kcSelectOTPListClass" |
"kcSelectOTPListItemClass" |
"kcAuthenticatorOtpCircleClass" |
"kcSelectOTPItemHeadingClass" |
"kcFormOptionsWrapperClass"
>;
export const defaultKcProps = {
...defaultKcTemplateProps,
"kcLogoLink": "http://www.keycloak.org",
"kcLogoClass": "login-pf-brand",
"kcContainerClass": "container-fluid",
"kcContentClass": ["col-sm-8", "col-sm-offset-2", "col-md-6", "col-md-offset-3", "col-lg-6", "col-lg-offset-3"],
"kcFeedbackAreaClass": ["col-md-12"],
"kcLocaleClass": ["col-xs-12", "col-sm-1"],
"kcAlertIconClasserror": ["pficon", "pficon-error-circle-o"],
"kcFormAreaClass": ["col-sm-10", "col-sm-offset-1", "col-md-8", "col-md-offset-2", "col-lg-8", "col-lg-offset-2"],
"kcFormSocialAccountListClass": ["login-pf-social", "list-unstyled", "login-pf-social-all"],
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"],
"kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormSettingClass": ["login-pf-settings"],
"kcTextareaClass": ["form-control"],
"kcInfoAreaClass": ["col-xs-12", "col-sm-4", "col-md-4", "col-lg-5", "details"],
// css classes for form buttons main class used for all buttons
"kcButtonClass": ["btn"],
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
"kcButtonPrimaryClass": ["btn-primary"],
"kcButtonDefaultClass": ["btn-default"],
// classes defining size of the button
"kcButtonLargeClass": ["btn-lg"],
"kcButtonBlockClass": ["btn-block"],
// css classes for input
"kcInputLargeClass": ["input-lg"],
// css classes for form accessability
"kcSrOnlyClass": ["sr-only"],
// css classes for select-authenticator form
"kcSelectAuthListClass": ["list-group", "list-view-pf"],
"kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"],
"kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
"kcSelectAuthListItemDescriptionClass": ["list-view-pf-description"],
"kcSelectAuthListItemHeadingClass": ["list-group-item-heading"],
"kcSelectAuthListItemHelpTextClass": ["list-group-item-text"],
// css classes for the authenticators
"kcAuthenticatorDefaultClass": ["fa", "list-view-pf-icon-lg"],
"kcAuthenticatorPasswordClass": ["fa", "fa-unlock list-view-pf-icon-lg"],
"kcAuthenticatorOTPClass": ["fa", "fa-mobile", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnPasswordlessClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
//css classes for the OTP Login Form
"kcSelectOTPListClass": ["card-pf", "card-pf-view", "card-pf-view-select", "card-pf-view-single-select"],
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
"kcFormOptionsWrapperClass": []
} as const;
doExtends<typeof defaultKcProps, KcProps>();
/** Tu use if you don't want any default */
export const allClearKcProps =
allPropertiesValuesToUndefined(defaultKcProps);
doExtends<typeof allClearKcProps, KcProps>();

View File

@ -0,0 +1,154 @@
import { useState, memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
import { useConstCallback } from "powerhooks";
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContext.Login; } & KcProps) => {
const { msg, msgStr } = useKcMessage();
const {
social, realm, url,
usernameEditDisabled, login,
auth, registrationDisabled
} = kcContext;
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback(() =>
(setIsLoginButtonDisabled(true), true)
);
return (
<Template
{...{ kcContext, ...props }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div
id="kc-form"
className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}
>
<div
id="kc-form-wrapper"
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}
>
{
realm.password &&
(
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{
!realm.loginWithEmailAllowed ?
msg("username")
:
(
!realm.registrationEmailAsUsername ?
msg("usernameOrEmail") :
msg("email")
)
}
</label>
<input
tabIndex={1}
id="username"
className={cx(props.kcInputClass)}
name="username"
defaultValue={login.username ?? ''}
type="text"
{...(usernameEditDisabled ? { "disabled": true } : { "autoFocus": true, "autoComplete": "off" })}
/>
</div>
<div className={cx(props.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>
{msg("password")}
</label>
<input tabIndex={2} id="password" className={cx(props.kcInputClass)} name="password" type="password" autoComplete="off" />
</div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-options">
{
(
realm.rememberMe &&
!usernameEditDisabled
) &&
<div className="checkbox">
<label>
<input tabIndex={3} id="rememberMe" name="rememberMe" type="checkbox" {...(login.rememberMe ? { "checked": true } : {})} />
{msg("rememberMe")}
</label>
</div>
}
</div>
<div className={cx(props.kcFormOptionsWrapperClass)}>
{
realm.resetPasswordAllowed &&
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{msg("doForgotPassword")}</a>
</span>
}
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
<input
type="hidden"
id="id-hidden-input"
name="credentialId"
{...(auth?.selectedCredential !== undefined ? { "value": auth.selectedCredential } : {})}
/>
<input
tabIndex={4}
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} name="login" id="kc-login" type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
)
}
</div>
{
(realm.password && social.providers !== undefined) &&
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}>
<ul className={cx(props.kcFormSocialAccountListClass, social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass)}>
{
social.providers.map(p =>
<li className={cx(props.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
<span>{p.displayName}</span>
</a>
</li>
)
}
</ul>
</div>
}
</div>
}
infoNode={
(
realm.password &&
realm.registrationAllowed &&
!registrationDisabled
) &&
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
}
/>
);
});

View File

@ -0,0 +1,145 @@
import { useEffect, memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
import { appendHead } from "../tools/appendHead";
import { join as pathJoin } from "path";
import { cx } from "tss-react";
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginOtp; } & KcProps) => {
const { otpLogin, url } = kcContext;
const { msg, msgStr } = useKcMessage();
useEffect(
() => {
let isCleanedUp = false;
appendHead({
"type": "javascript",
"src": pathJoin(
kcContext.url.resourcesCommonPath,
"node_modules/jquery/dist/jquery.min.js"
)
}).then(() => {
if (isCleanedUp) return;
evaluateInlineScript();
});
return () => { isCleanedUp = true };
},
[]
);
return (
<Template
{...{ kcContext, ...props }}
headerNode={msg("doLogIn")}
formNode={
<form
id="kc-otp-login-form"
className={cx(props.kcFormClass)}
action={url.loginAction}
method="post"
>
{
otpLogin.userOtpCredentials.length > 1 &&
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}>
{
otpLogin.userOtpCredentials.map(otpCredential =>
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}>
<input type="hidden" value="${otpCredential.id}" />
<div className={cx(props.kcSelectOTPListItemClass)}>
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>
{otpCredential.userLabel}
</h2>
</div>
</div>
)
}
</div>
</div>
}
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
id="otp"
name="otp"
autoComplete="off"
type="text"
className={cx(props.kcInputClass)}
autoFocus
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
</form >
}
/>
);
});
declare const $: any;
function evaluateInlineScript() {
$(document).ready(function () {
// Card Single Select
$('.card-pf-view-single-select').click(function (this: any) {
if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children().removeAttr('name'); }
else {
$('.card-pf-view-single-select').removeClass('active');
$('.card-pf-view-single-select').children().removeAttr('name');
$(this).addClass('active'); $(this).children().attr('name', 'selectedCredentialId');
}
});
var defaultCred = $('.card-pf-view-single-select')[0];
if (defaultCred) {
defaultCred.click();
}
});
}

View File

@ -0,0 +1,80 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginResetPassword; } & KcProps) => {
const { msg, msgStr } = useKcMessage();
const {
url,
realm,
auth
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
displayMessage={false}
headerNode={msg("emailForgotTitle")}
formNode={
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{
!realm.loginWithEmailAllowed ?
msg("username")
:
!realm.registrationEmailAsUsername ?
msg("usernameOrEmail") :
msg("email")
}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="username"
name="username"
className={cx(props.kcInputClass)}
autoFocus
defaultValue={
auth !== undefined && auth.showUsername ?
auth.attemptedUsername : undefined
}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(
props.kcButtonClass, props.kcButtonPrimaryClass,
props.kcButtonBlockClass, props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
/>
</div>
</div>
</form>
}
infoNode={msg("emailInstruction")}
/>
);
});

View File

@ -0,0 +1,38 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginVerifyEmail; } & KcProps) => {
const { msg } = useKcMessage();
const {
url
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
<>
<p className="instruction">
{msg("emailVerifyInstruction1")}
</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<a href={url.loginAction}>{msg("doClickHere")}</a>
{msg("emailVerifyInstruction3")}
</p>
</>
}
/>
);
});

View File

@ -0,0 +1,123 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContext.Register; } & KcProps) => {
const { msg, msgStr } = useKcMessage();
const {
url,
messagesPerField,
register,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
headerNode={msg("registerTitle")}
formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('firstName', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{msg("firstName")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="firstName" className={cx(props.kcInputClass)} name="firstName"
defaultValue={register.formData.firstName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>{msg("lastName")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="lastName" className={cx(props.kcInputClass)} name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('email', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}>{msg("email")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="email" className={cx(props.kcInputClass)} name="email"
defaultValue={register.formData.email ?? ""} autoComplete="email"
/>
</div>
</div>
{
!realm.registrationEmailAsUsername &&
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('username', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>{msg("username")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="username" className={cx(props.kcInputClass)} name="username"
defaultValue={register.formData.username ?? ""} autoComplete="username" />
</div>
</div >
}
{
passwordRequired &&
<>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>{msg("password")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password" className={cx(props.kcInputClass)} name="password" autoComplete="new-password" />
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>{msg("passwordConfirm")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
</div>
</div>
</>
}
{
recaptchaRequired &&
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div>
</div>
}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span><a href={url.loginUrl}>{msg("backToLogin")}</a></span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} type="submit"
value={msgStr("doRegister")} />
</div>
</div>
</form >
}
/>
);
});

View File

@ -0,0 +1,306 @@
import { useReducer, useEffect, memo } from "react";
import type { ReactNode } from "react";
import { useKcMessage } from "../i18n/useKcMessage";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
import type { KcContext } from "../KcContext";
import { assert } from "../tools/assert";
import { cx } from "tss-react";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks";
import { appendHead } from "../tools/appendHead";
import { join as pathJoin } from "path";
import { useConstCallback } from "powerhooks";
import type { KcTemplateProps } from "./KcProps";
export type TemplateProps = {
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
showUsernameNode?: ReactNode;
formNode: ReactNode;
infoNode?: ReactNode;
} & { kcContext: KcContext; } & KcTemplateProps;
export const Template = memo((props: TemplateProps) => {
const {
displayInfo = false,
displayMessage = true,
displayRequiredFields = false,
displayWide = false,
showAnotherWayIfPresent = true,
headerNode,
showUsernameNode = null,
formNode,
infoNode = null,
kcContext
} = props;
useEffect(() => { console.log("Rendering this page with react using keycloakify") }, []);
const { msg } = useKcMessage();
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
const onChangeLanguageClickFactory = useCallbackFactory(
([languageTag]: [KcLanguageTag]) =>
setKcLanguageTag(languageTag)
);
const onTryAnotherWayClick = useConstCallback(() =>
(document.forms["kc-select-try-another-way-form" as never].submit(), false)
);
const {
realm, locale, auth,
url, message, isAppInitiatedAction
} = kcContext;
useEffect(() => {
if (!realm.internationalizationEnabled) {
return;
}
assert(locale !== undefined);
if (kcLanguageTag === getBestMatchAmongKcLanguageTag(locale.current)) {
return;
}
window.location.href =
locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag)!.url;
}, [kcLanguageTag]);
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
useEffect(() => {
let isUnmounted = false;
const cleanups: (() => void)[] = [];
const toArr = (x: string | readonly string[] | undefined) =>
typeof x === "string" ? x.split(" ") : x ?? [];
Promise.all(
[
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
].map(href => appendHead({
"type": "css",
href
}))).then(() => {
if (isUnmounted) {
return;
}
setExtraCssLoaded();
});
toArr(props.scripts).forEach(
relativePath => appendHead({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
if (props.kcHtmlClass !== undefined) {
const htmlClassList =
document.getElementsByTagName("html")[0]
.classList;
const tokens = cx(props.kcHtmlClass).split(" ")
htmlClassList.add(...tokens);
cleanups.push(() => htmlClassList.remove(...tokens));
}
return () => {
isUnmounted = true;
cleanups.forEach(f => f());
};
}, [props.kcHtmlClass]);
if (!isExtraCssLoaded) {
return null;
}
return (
<div className={cx(props.kcLoginClass)}>
<div id="kc-header" className={cx(props.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(props.kcHeaderWrapperClass)}>
{msg("loginTitleHtml", realm.displayNameHtml)}
</div>
</div>
<div className={cx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
<header className={cx(props.kcFormHeaderClass)}>
{
(
realm.internationalizationEnabled &&
(assert(locale !== undefined), true) &&
locale.supported.length > 1
) &&
<div id="kc-locale">
<div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}>
<div className="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link">
{getKcLanguageTagLabel(kcLanguageTag)}
</a>
<ul>
{
locale.supported.map(
({ languageTag }) =>
<li key={languageTag} className="kc-dropdown-item">
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
{getKcLanguageTagLabel(languageTag)}
</a>
</li>
)
}
</ul>
</div>
</div>
</div>
}
{
!(
auth !== undefined &&
auth.showUsername &&
!auth.showResetCredentials
) ?
(
displayRequiredFields ?
(
<div className={cx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle">
<span className="required">*</span>
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
<h1 id="kc-page-title">{headerNode}</h1>
</div>
</div>
)
:
(
<h1 id="kc-page-title">{headerNode}</h1>
)
) : (
displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"><span className="required">*</span> {msg("requiredFields")}</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</div>
</div>
) : (
<>
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</>
)
)
}
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{
(
displayMessage &&
message !== undefined &&
(
message.type !== "warning" ||
!isAppInitiatedAction
)
) &&
<div className={cx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={cx(props.kcFeedbackSuccessIcon)}></span>}
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>}
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>}
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>}
<span className="kc-feedback-text">{message.summary}</span>
</div>
}
{formNode}
{
(
auth !== undefined &&
auth.showTryAnotherWayLink &&
showAnotherWayIfPresent
) &&
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && props.kcContentWrapperClass)} >
<div className={cx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])} >
<div className={cx(props.kcFormGroupClass)}>
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{msg("doTryAnotherWay")}</a>
</div>
</div >
</form>
}
{
displayInfo &&
<div id="kc-info" className={cx(props.kcSignUpClass)}>
<div id="kc-info-wrapper" className={cx(props.kcInfoAreaWrapperClass)}>
{infoNode}
</div>
</div>
}
</div>
</div>
</div>
</div>
);
});

View File

@ -0,0 +1,57 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContext } from "../KcContext";
import { useKcMessage } from "../i18n/useKcMessage";
import { cx } from "tss-react";
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContext.Terms; } & KcProps) => {
const { msg, msgStr } = useKcMessage();
const { url } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
displayMessage={false}
headerNode={msg("termsTitle")}
formNode={
<>
<div id="kc-terms-text">
{msg("termsText")}
</div>
<form className="form-actions" action={url.loginAction} method="POST">
<input
className={cx(
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass
)}
name="accept"
id="kc-accept"
type="submit"
value={msgStr("doAccept")}
/>
<input
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass
)}
name="cancel"
id="kc-decline"
type="submit"
value={msgStr("doDecline")}
/>
</form>
<div className="clearfix" />
</>
}
/>
);
});

View File

@ -0,0 +1,72 @@
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { kcMessages } from "./kcMessages/login";
export type KcLanguageTag = keyof typeof kcMessages;
export type LanguageLabel =
/* spell-checker: disable */
"Deutsch" | "Norsk" | "Русский" | "Svenska" | "Português (Brasil)" | "Lietuvių" |
"English" | "Italiano" | "Français" | "中文简体" | "Español" | "Čeština" | "日本語" |
"Slovenčina" | "Polski" | "Català" | "Nederlands" | "Türkçe";
/* spell-checker: enable */
export function getKcLanguageTagLabel(language: KcLanguageTag): LanguageLabel {
switch (language) {
/* spell-checker: disable */
case "es": return "Español";
case "it": return "Italiano";
case "fr": return "Français";
case "ca": return "Català";
case "en": return "English";
case "de": return "Deutsch";
case "no": return "Norsk";
case "pt-BR": return "Português (Brasil)";
case "ru": return "Русский";
case "sk": return "Slovenčina";
case "ja": return "日本語";
case "pl": return "Polski";
case "zh-CN": return "中文简体"
case "sv": return "Svenska";
case "lt": return "Lietuvių";
case "cs": return "Čeština";
case "nl": return "Nederlands";
case "tr": return "Türkçe";
/* spell-checker: enable */
}
return language;
}
const availableLanguages = objectKeys(kcMessages);
/**
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
* it corresponds to: "fr".
* If there is no reasonable match it's guessed from navigator.language.
* If still no matches "en" is returned.
*/
export function getBestMatchAmongKcLanguageTag(
languageLike: string
): KcLanguageTag {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
const kcLanguageTag = availableLanguages.find(language =>
language.toLowerCase().includes(iso2LanguageLike) ||
getKcLanguageTagLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase()
);
if (kcLanguageTag !== undefined) {
return kcLanguageTag;
}
if (languageLike !== navigator.language) {
return getBestMatchAmongKcLanguageTag(navigator.language);
}
return "en";
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,244 @@
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
//PLEASE DO NOT EDIT MANUALLY
/* spell-checker: disable */
export const kcMessages= {
"ca": {
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"invalidPasswordMinLengthMessage": "Contrasenya incorrecta: longitud mínima {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.",
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular."
},
"de": {
"invalidPasswordMinLengthMessage": "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.",
"invalidPasswordMinLowerCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.",
"invalidPasswordMinDigitsMessage": "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.",
"invalidPasswordMinUpperCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.",
"invalidPasswordMinSpecialCharsMessage": "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.",
"invalidPasswordNotUsernameMessage": "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.",
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht."
},
"en": {
"invalidPasswordMinLengthMessage": "Invalid password: minimum length {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Invalid password: must contain at least {0} lower case characters.",
"invalidPasswordMinDigitsMessage": "Invalid password: must contain at least {0} numerical digits.",
"invalidPasswordMinUpperCaseCharsMessage": "Invalid password: must contain at least {0} upper case characters.",
"invalidPasswordMinSpecialCharsMessage": "Invalid password: must contain at least {0} special characters.",
"invalidPasswordNotUsernameMessage": "Invalid password: must not be equal to the username.",
"invalidPasswordRegexPatternMessage": "Invalid password: fails to match regex pattern(s).",
"invalidPasswordHistoryMessage": "Invalid password: must not be equal to any of last {0} passwords.",
"invalidPasswordBlacklistedMessage": "Invalid password: password is blacklisted.",
"invalidPasswordGenericMessage": "Invalid password: new password does not match password policies.",
"ldapErrorInvalidCustomFilter": "Custom configured LDAP filter does not start with \"(\" or does not end with \")\".",
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout must be a number",
"ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number",
"ldapErrorMissingClientId": "Client ID needs to be provided in config when Realm Roles Mapping is not used.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Not possible to preserve group inheritance and use UID membership type together.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Can not set write only when LDAP provider mode is not WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly": "Can not set write-only and read-only together",
"ldapErrorCantEnableStartTlsAndConnectionPooling": "Can not enable both StartTLS and connection pooling.",
"ldapErrorCantEnableUnsyncedAndImportOff": "Can not disable Importing users when LDAP provider mode is UNSYNCED",
"ldapErrorMissingGroupsPathGroup": "Groups path group does not exist - please create the group on specified path first",
"clientRedirectURIsFragmentError": "Redirect URIs must not contain an URI fragment",
"clientRootURLFragmentError": "Root URL must not contain an URL fragment",
"clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme",
"clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme",
"clientRedirectURIsIllegalSchemeError": "A redirect URI uses an illegal scheme",
"clientBaseURLInvalid": "Base URL is not a valid URL",
"clientRootURLInvalid": "Root URL is not a valid URL",
"clientRedirectURIsInvalid": "A redirect URI is not a valid URI",
"pairwiseMalformedClientRedirectURI": "Client contained an invalid redirect URI.",
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs must contain a valid host component.",
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI."
},
"es": {
"invalidPasswordMinLengthMessage": "Contraseña incorrecta: longitud mínima {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.",
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"invalidPasswordMinUpperCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.",
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas."
},
"fr": {
"invalidPasswordMinLengthMessage": "Mot de passe invalide : longueur minimale requise de {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.",
"invalidPasswordMinDigitsMessage": "Mot de passe invalide : doit contenir au moins {0} chiffre(s).",
"invalidPasswordMinUpperCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.",
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.",
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe."
},
"it": {},
"ja": {
"invalidPasswordMinLengthMessage": "無効なパスワード: 最小{0}の長さが必要です。",
"invalidPasswordMinLowerCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。",
"invalidPasswordMinDigitsMessage": "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。",
"invalidPasswordMinUpperCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。",
"invalidPasswordMinSpecialCharsMessage": "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。",
"invalidPasswordNotUsernameMessage": "無効なパスワード: ユーザー名と同じパスワードは禁止されています。",
"invalidPasswordRegexPatternMessage": "無効なパスワード: 正規表現パターンと一致しません。",
"invalidPasswordHistoryMessage": "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。",
"invalidPasswordBlacklistedMessage": "無効なパスワード: パスワードがブラックリストに含まれています。",
"invalidPasswordGenericMessage": "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。",
"ldapErrorInvalidCustomFilter": "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。",
"ldapErrorConnectionTimeoutNotNumber": "接続タイムアウトは数字でなければなりません",
"ldapErrorReadTimeoutNotNumber": "読み取りタイムアウトは数字でなければなりません",
"ldapErrorMissingClientId": "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。",
"ldapErrorCantWriteOnlyAndReadOnly": "write-onlyとread-onlyを一緒に設定することはできません。",
"ldapErrorCantEnableStartTlsAndConnectionPooling": "StartTLSと接続プーリングの両方を有効にできません。",
"clientRedirectURIsFragmentError": "リダイレクトURIにURIフラグメントを含めることはできません。",
"clientRootURLFragmentError": "ルートURLにURLフラグメントを含めることはできません。",
"pairwiseMalformedClientRedirectURI": "クライアントに無効なリダイレクトURIが含まれていました。",
"pairwiseClientRedirectURIsMissingHost": "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。",
"pairwiseClientRedirectURIsMultipleHosts": "設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。",
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。",
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。"
},
"lt": {
"invalidPasswordMinLengthMessage": "Per trumpas slaptažodis: mažiausias ilgis {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.",
"invalidPasswordMinDigitsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.",
"invalidPasswordMinUpperCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.",
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
"ldapErrorInvalidCustomFilter": "Sukonfigūruotas LDAP filtras neprasideda \"(\" ir nesibaigia \")\" simboliais.",
"ldapErrorMissingClientId": "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly": "Negalima nustatyti tik rašyti ir tik skaityti kartu",
"clientRedirectURIsFragmentError": "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose",
"clientRootURLFragmentError": "Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese",
"pairwiseMalformedClientRedirectURI": "Klientas pateikė neteisingą nukreipimo nuorodą.",
"pairwiseClientRedirectURIsMissingHost": "Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.",
"pairwiseClientRedirectURIsMultipleHosts": "Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.",
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI."
},
"nl": {
"invalidPasswordMinLengthMessage": "Ongeldig wachtwoord: de minimale lengte is {0} karakters.",
"invalidPasswordMinLowerCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.",
"invalidPasswordMinDigitsMessage": "Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.",
"invalidPasswordMinUpperCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.",
"invalidPasswordMinSpecialCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.",
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
"ldapErrorInvalidCustomFilter": "LDAP filter met aangepaste configuratie start niet met \"(\" of eindigt niet met \")\".",
"ldapErrorConnectionTimeoutNotNumber": "Verbindingstimeout moet een getal zijn",
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn",
"ldapErrorMissingClientId": "Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Kan groepsovererving niet behouden bij UID-lidmaatschapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is",
"ldapErrorCantWriteOnlyAndReadOnly": "Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn",
"clientRedirectURIsFragmentError": "Redirect URIs mogen geen URI fragment bevatten",
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten",
"pairwiseMalformedClientRedirectURI": "Client heeft een ongeldige redirect URI.",
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs moeten een geldige host-component bevatten.",
"pairwiseClientRedirectURIsMultipleHosts": "Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI."
},
"no": {
"invalidPasswordMinLengthMessage": "Ugyldig passord: minimum lengde {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} små bokstaver.",
"invalidPasswordMinDigitsMessage": "Ugyldig passord: må inneholde minst {0} sifre.",
"invalidPasswordMinUpperCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} store bokstaver.",
"invalidPasswordMinSpecialCharsMessage": "Ugyldig passord: må inneholde minst {0} spesialtegn.",
"invalidPasswordNotUsernameMessage": "Ugyldig passord: kan ikke være likt brukernavn.",
"invalidPasswordRegexPatternMessage": "Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.",
"invalidPasswordHistoryMessage": "Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.",
"ldapErrorInvalidCustomFilter": "Tilpasset konfigurasjon av LDAP-filter starter ikke med \"(\" eller slutter ikke med \")\".",
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only"
},
"pl": {},
"pt-BR": {
"invalidPasswordMinLengthMessage": "Senha inválida: deve conter ao menos {0} caracteres.",
"invalidPasswordMinLowerCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres minúsculos.",
"invalidPasswordMinDigitsMessage": "Senha inválida: deve conter ao menos {0} digitos numéricos.",
"invalidPasswordMinUpperCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres maiúsculos.",
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres especiais.",
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário.",
"invalidPasswordRegexPatternMessage": "Senha inválida: falha ao passar por padrões.",
"invalidPasswordHistoryMessage": "Senha inválida: não deve ser igual às últimas {0} senhas.",
"ldapErrorInvalidCustomFilter": "Filtro LDAP não inicia com \"(\" ou não termina com \")\".",
"ldapErrorMissingClientId": "ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo",
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos",
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos"
},
"ru": {
"invalidPasswordMinLengthMessage": "Некорректный пароль: длина пароля должна быть не менее {0} символов(а).",
"invalidPasswordMinDigitsMessage": "Некорректный пароль: должен содержать не менее {0} цифр(ы).",
"invalidPasswordMinLowerCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.",
"invalidPasswordMinUpperCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.",
"invalidPasswordMinSpecialCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).",
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.",
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не прошел проверку по регулярному выражению.",
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).",
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.",
"ldapErrorInvalidCustomFilter": "Сконфигурированный пользователем фильтр LDAP не должен начинаться с \"(\" или заканчиваться на \")\".",
"ldapErrorMissingClientId": "Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Не удалось унаследовать группу и использовать членство UID типа вместе.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Невозможно установить режим \"только на запись\", когда LDAP провайдер не в режиме WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly": "Невозможно одновременно установить режимы \"только на чтение\" и \"только на запись\"",
"clientRedirectURIsFragmentError": "URI перенаправления не должен содержать фрагмент URI",
"clientRootURLFragmentError": "Корневой URL не должен содержать фрагмент URL ",
"pairwiseMalformedClientRedirectURI": "Клиент содержит некорректный URI перенаправления.",
"pairwiseClientRedirectURIsMissingHost": "URI перенаправления клиента должен содержать корректный компонент хоста.",
"pairwiseClientRedirectURIsMultipleHosts": "Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.",
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI."
},
"zh-CN": {
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.",
"invalidPasswordMinLowerCaseCharsMessage": "无效的密码:至少包含 {0} 小写字母",
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字",
"invalidPasswordMinUpperCaseCharsMessage": "无效的密码:最短长度 {0} 大写字母",
"invalidPasswordMinSpecialCharsMessage": "无效的密码:最短长度 {0} 特殊字符",
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同",
"invalidPasswordRegexPatternMessage": "无效的密码: 无法与正则表达式匹配",
"invalidPasswordHistoryMessage": "无效的密码:不能与最后使用的 {0} 个密码相同",
"ldapErrorInvalidCustomFilter": "定制的 LDAP过滤器不是以 \"(\" 开头或以 \")\"结尾.",
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout 必须是个数字",
"ldapErrorMissingClientId": "当域角色映射未启用时,客户端 ID 需要指定。",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "无法在使用UID成员类型的同时维护组继承属性。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "当LDAP提供方不是可写模式时无法设置只写",
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写",
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段",
"clientRootURLFragmentError": "根URL 不应包含 URL 片段",
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL",
"pairwiseClientRedirectURIsMissingHost": "客户端重定向URL需要有一个有效的主机",
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。"
}
};
/* spell-checker: enable */

View File

@ -0,0 +1,638 @@
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
//PLEASE DO NOT EDIT MANUALLY
/* spell-checker: disable */
export const kcMessages= {
"ca": {
"emailVerificationSubject": "Verificació d'email",
"emailVerificationBody": "Algú ha creat un compte de {2} amb aquesta adreça de correu electrònic. Si has estat tu, fes clic a l'enllaç següent per verificar la teva adreça de correu electrònic.\n\n{0}\n\nAquest enllaç expirarà en {1} minuts.\n\nSi tu no has creat aquest compte, simplement ignora aquest missatge.",
"emailVerificationBodyHtml": "<p>Algú ha creat un compte de {2} amb aquesta adreça de correu electrònic. Si has estat tu, fes clic a l'enllaç següent per verificar la teva adreça de correu electrònic.</p><p><a href=\"{0}\">{0}</a></p><p> Aquest enllaç expirarà en {1} minuts.</p><p> Si tu no has creat aquest compte, simplement ignora aquest missatge.</p>",
"passwordResetSubject": "Reinicia contrasenya",
"passwordResetBody": "Algú ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l'enllaç següent per a reiniciar-les.\n\n{0}\n\nAquest enllaç expirarà en {1} minuts.\n\nSi no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzarà cap canvi.",
"passwordResetBodyHtml": "<p>Algú ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l'enllaç següent per a reiniciar-les.</p><p><a href=\"{0}\">{0}</a></p><p>Aquest enllaç expirarà en {1} minuts.</p><p>Si no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzarà cap canvi.</p>",
"executeActionsSubject": "Actualitza el teu compte",
"executeActionsBody": "L'administrador ha sol·licitat que actualitzis el teu compte de {2}. Fes clic a l'enllaç inferior per iniciar aquest procés.\n\n{0}\n\nAquest enllaç expirarà en {1} minutes.\n\nSi no estàs al tant que l'administrador hagi sol·licitat això, simplement ignora aquest missatge i no es realitzarà cap canvi.",
"executeActionsBodyHtml": "<p>L'administrador ha sol·licitat que actualitzis el teu compte de {2}. Fes clic a l'enllaç inferior per iniciar aquest procés.</p><p><a href=\"{0}\">{0}</a></p><p>Aquest enllaç expirarà en {1} minutes.</p><p>Si no estàs al tant que l'administrador hagi sol·licitat això, simplement ignora aquest missatge i no es realitzarà cap canvi.</p>",
"eventLoginErrorSubject": "Fallada en l'inici de sessió",
"eventLoginErrorBody": "S'ha detectat un intent d'accés fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
"eventLoginErrorBodyHtml": "<p>S'ha detectat un intent d'accés fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
"eventRemoveTotpSubject": "Esborrat OTP",
"eventRemoveTotpBody": "OTP s'ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, per favor contacta amb l'administrador.",
"eventRemoveTotpBodyHtml": "<p>OTP s'ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador. </ P>",
"eventUpdatePasswordSubject": "Actualització de contrasenya",
"eventUpdatePasswordBody": "La teva contrasenya s'ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
"eventUpdatePasswordBodyHtml": "<p>La teva contrasenya s'ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
"eventUpdateTotpSubject": "Actualització de OTP",
"eventUpdateTotpBody": "OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
"eventUpdateTotpBodyHtml": "<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>"
},
"cs": {
"emailVerificationSubject": "Ověření e-mailu",
"emailVerificationBody": "Někdo vytvořil účet {2} s touto e-mailovou adresou. Pokud jste to vy, klikněte na níže uvedený odkaz a ověřte svou e-mailovou adresu \n\n{0}\n\nTento odkaz vyprší za {1} minuty.\n\nPokud jste tento účet nevytvořili, tuto zprávu ignorujte.",
"emailVerificationBodyHtml": "<p>Někdo vytvořil účet {2} s touto e-mailovou adresou. Pokud jste to vy, klikněte na níže uvedený odkaz a ověřte svou e-mailovou adresu. </p><p><a href=\"{0}\">Odkaz na ověření e-mailové adresy</a></p><p>Platnost odkazu vyprší za {1} minut.</p><p>Pokud jste tento účet nevytvořili, tuto zprávu ignorujte.</p>",
"emailTestSubject": "[KEYCLOAK] - testovací zpráva",
"emailTestBody": "Toto je testovací zpráva",
"emailTestBodyHtml": "<p>Toto je testovací zpráva </p>",
"identityProviderLinkSubject": "Odkaz {0}",
"identityProviderLinkBody": "Někdo chce propojit váš účet \"{1}\" s účtem \"{0}\" uživatele {2}. Pokud jste to vy, klikněte na níže uvedený odkaz a propojte účty. \n\n{3}\n\nPlatnost tohoto odkazu je {5}.\n\nPokud nechcete propojit účet, tuto zprávu ignorujte. Pokud propojíte účty, budete se moci přihlásit jako {1} pomocí {0}.",
"identityProviderLinkBodyHtml": "<p>Někdo právě požádal o změnu hesla u vašeho účtu {2}. Pokud jste to vy, pro jeho změnu klikněte na odkaz níže.</p><p><a href=\"{0}\">Odkaz na změnu hesla.</a></p><p>Platnost tohoto odkazu je {3}.</p><p>Pokud heslo změnit nechcete, tuto zprávu ignorujte a nic se nezmění.</p>",
"passwordResetSubject": "Zapomenuté heslo",
"passwordResetBody": "Někdo právě požádal o změnu hesla u vašeho účtu {2}. Pokud jste to vy, pro jeho změnu klikněte na odkaz níže.\n\n{0}\n\nPlatnost tohoto odkazu je {3}.\n\nPokud heslo změnit nechcete, tuto zprávu ignorujte a nic se nezmění.",
"passwordResetBodyHtml": "<p> Někdo právě požádal o změnu pověření vašeho účtu {2}. Pokud jste to vy, klikněte na odkaz níže, abyste je resetovali.</p><p><a href=\"{0}\">Odkaz na obnovení pověření </a></p><p> Platnost tohoto odkazu vyprší během {1} minut.</p><p> Pokud nechcete obnovit vaše pověření, ignorujte tuto zprávu a nic se nezmění.</p>",
"executeActionsSubject": "Aktualizujte svůj účet",
"executeActionsBody": "Váš administrátor vás požádal o provedení následujících akcí u účtu {2}: {3}. Začněte kliknutím na níže uvedený odkaz.\n\n{0}\n\nPlatnost tohoto odkazu je {4}.\n\nPokud si nejste jisti, zda je tento požadavek v pořádku, ignorujte tuto zprávu.",
"executeActionsBodyHtml": "<p>Váš administrátor vás požádal o provedení následujících akcí u účtu {2}: {3}. Začněte kliknutím na níže uvedený odkaz.</p><p><a href=\"{0}\">Odkaz na aktualizaci účtu.</a></p><p>Platnost tohoto odkazu je {4}.</p><p>Pokud si nejste jisti, zda je tento požadavek v pořádku, ignorujte tuto zprávu.</p>",
"eventLoginErrorSubject": "Chyba přihlášení",
"eventLoginErrorBody": "Někdo se neúspěšně pokusil přihlásit k účtu {0} z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
"eventLoginErrorBodyHtml": "<p>Někdo se neúspěšně pokusil přihlásit k účtu {0} z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
"eventRemoveTotpSubject": "Odebrat TOTP",
"eventRemoveTotpBody": "V účtu {0} bylo odebráno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
"eventRemoveTotpBodyHtml": "<p>V účtu {0} bylo odebráno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
"eventUpdatePasswordSubject": "Aktualizace hesla",
"eventUpdatePasswordBody": "V účtu {0} bylo změněno heslo z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
"eventUpdatePasswordBodyHtml": "<p>V účtu {0} bylo změněno heslo z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
"eventUpdateTotpSubject": "Aktualizace OTP",
"eventUpdateTotpBody": "V účtu {0} bylo změněno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
"eventUpdateTotpBodyHtml": "<p>V účtu {0} bylo změněno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
"requiredAction.CONFIGURE_TOTP": "Konfigurace OTP",
"requiredAction.terms_and_conditions": "Smluvní podmínky",
"requiredAction.UPDATE_PASSWORD": "Aktualizace hesla",
"requiredAction.UPDATE_PROFILE": "Aktualizace profilu",
"requiredAction.VERIFY_EMAIL": "Ověření e-mailu",
"linkExpirationFormatter.timePeriodUnit.seconds": "sekund",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
"linkExpirationFormatter.timePeriodUnit.seconds.2": "sekundy",
"linkExpirationFormatter.timePeriodUnit.seconds.3": "sekundy",
"linkExpirationFormatter.timePeriodUnit.seconds.4": "sekundy",
"linkExpirationFormatter.timePeriodUnit.minutes": "minut",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuta",
"linkExpirationFormatter.timePeriodUnit.minutes.2": "minuty",
"linkExpirationFormatter.timePeriodUnit.minutes.3": "minuty",
"linkExpirationFormatter.timePeriodUnit.minutes.4": "minuty",
"linkExpirationFormatter.timePeriodUnit.hours": "hodin",
"linkExpirationFormatter.timePeriodUnit.hours.1": "hodina",
"linkExpirationFormatter.timePeriodUnit.hours.2": "hodiny",
"linkExpirationFormatter.timePeriodUnit.hours.3": "hodiny",
"linkExpirationFormatter.timePeriodUnit.hours.4": "hodiny",
"linkExpirationFormatter.timePeriodUnit.days": "dní",
"linkExpirationFormatter.timePeriodUnit.days.1": "den",
"linkExpirationFormatter.timePeriodUnit.days.2": "dny",
"linkExpirationFormatter.timePeriodUnit.days.3": "dny",
"linkExpirationFormatter.timePeriodUnit.days.4": "dny"
},
"de": {
"emailVerificationSubject": "E-Mail verifizieren",
"emailVerificationBody": "Jemand hat ein {2} Konto mit dieser E-Mail-Adresse erstellt. Falls Sie das waren, dann klicken Sie auf den Link, um die E-Mail-Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann können sie diese Nachricht ignorieren.",
"emailVerificationBodyHtml": "<p>Jemand hat ein {2} Konto mit dieser E-Mail-Adresse erstellt. Falls das Sie waren, klicken Sie auf den Link, um die E-Mail-Adresse zu verifizieren.</p><p><a href=\"{0}\">Link zur Bestätigung der E-Mail-Adresse</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie dieses Konto nicht erstellt haben, dann können sie diese Nachricht ignorieren.</p>",
"identityProviderLinkSubject": "Link {0}",
"identityProviderLinkBody": "Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{3}\n\n Die Gültigkeit des Links wird in {4} Minuten verfallen.\n\nSollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} über {0} ermöglicht.",
"identityProviderLinkBodyHtml": "<p>Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href=\"{3}\">Link zur Bestätigung der Kontoverknüpfung</a></p><p>Die Gültigkeit des Links wird in {4} Minuten verfallen.</p><p>Sollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} über {0} ermöglicht.</p>",
"passwordResetSubject": "Passwort zurücksetzen",
"passwordResetBody": "Es wurde eine Änderung der Anmeldeinformationen für Ihren Account {2} angefordert. Wenn Sie diese Änderung beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{0}\n\nDie Gültigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie keine Änderung vollziehen wollen können Sie diese Nachricht ignorieren und an Ihrem Account wird nichts geändert.",
"passwordResetBodyHtml": "<p>Es wurde eine Änderung der Anmeldeinformationen für Ihren Account {2} angefordert. Wenn Sie diese Änderung beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href=\"{0}\">Link zum Zurücksetzen von Anmeldeinformationen</a></p><p>Die Gültigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie keine Änderung vollziehen wollen können Sie diese Nachricht ignorieren und an Ihrem Account wird nichts geändert.</p>",
"executeActionsSubject": "Aktualisieren Sie Ihr Konto",
"executeActionsBody": "Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.\n\n{0}\n\nDie Gültigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unverändert.",
"executeActionsBodyHtml": "<p>Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.</p><p><a href=\"{0}\">Link zum Account-Update</a></p><p>Die Gültigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unverändert.</p>",
"eventLoginErrorSubject": "Fehlgeschlagene Anmeldung",
"eventLoginErrorBody": "Jemand hat um {0} von {1} versucht, sich mit Ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
"eventLoginErrorBodyHtml": "<p>Jemand hat um {0} von {1} versucht, sich mit Ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
"eventRemoveTotpSubject": "OTP Entfernt",
"eventRemoveTotpBody": "OTP wurde von Ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
"eventRemoveTotpBodyHtml": "<p>OTP wurde von Ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
"eventUpdatePasswordSubject": "Passwort Aktualisiert",
"eventUpdatePasswordBody": "Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
"eventUpdatePasswordBodyHtml": "<p>Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
"eventUpdateTotpSubject": "OTP Aktualisiert",
"eventUpdateTotpBody": "OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>"
},
"en": {
"emailVerificationSubject": "Verify email",
"emailVerificationBody": "Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn't create this account, just ignore this message.",
"emailVerificationBodyHtml": "<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href=\"{0}\">Link to e-mail address verification</a></p><p>This link will expire within {3}.</p><p>If you didn't create this account, just ignore this message.</p>",
"emailTestSubject": "[KEYCLOAK] - SMTP test message",
"emailTestBody": "This is a test message",
"emailTestBodyHtml": "<p>This is a test message</p>",
"identityProviderLinkSubject": "Link {0}",
"identityProviderLinkBody": "Someone wants to link your \"{1}\" account with \"{0}\" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {5}.\n\nIf you don't want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.",
"identityProviderLinkBodyHtml": "<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href=\"{3}\">Link to confirm account linking</a></p><p>This link will expire within {5}.</p><p>If you don't want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>",
"passwordResetSubject": "Reset password",
"passwordResetBody": "Someone just requested to change your {2} account's credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {3}.\n\nIf you don't want to reset your credentials, just ignore this message and nothing will be changed.",
"passwordResetBodyHtml": "<p>Someone just requested to change your {2} account's credentials. If this was you, click on the link below to reset them.</p><p><a href=\"{0}\">Link to reset credentials</a></p><p>This link will expire within {3}.</p><p>If you don't want to reset your credentials, just ignore this message and nothing will be changed.</p>",
"executeActionsSubject": "Update Your Account",
"executeActionsBody": "Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.",
"executeActionsBodyHtml": "<p>Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.</p><p><a href=\"{0}\">Link to account update</a></p><p>This link will expire within {4}.</p><p>If you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.</p>",
"eventLoginErrorSubject": "Login error",
"eventLoginErrorBody": "A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.",
"eventLoginErrorBodyHtml": "<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
"eventRemoveTotpSubject": "Remove OTP",
"eventRemoveTotpBody": "OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.",
"eventRemoveTotpBodyHtml": "<p>OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
"eventUpdatePasswordSubject": "Update password",
"eventUpdatePasswordBody": "Your password was changed on {0} from {1}. If this was not you, please contact an administrator.",
"eventUpdatePasswordBodyHtml": "<p>Your password was changed on {0} from {1}. If this was not you, please contact an administrator.</p>",
"eventUpdateTotpSubject": "Update OTP",
"eventUpdateTotpBody": "OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.",
"eventUpdateTotpBodyHtml": "<p>OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
"requiredAction.CONFIGURE_TOTP": "Configure OTP",
"requiredAction.terms_and_conditions": "Terms and Conditions",
"requiredAction.UPDATE_PASSWORD": "Update Password",
"requiredAction.UPDATE_PROFILE": "Update Profile",
"requiredAction.VERIFY_EMAIL": "Verify Email",
"linkExpirationFormatter.timePeriodUnit.seconds": "seconds",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "second",
"linkExpirationFormatter.timePeriodUnit.minutes": "minutes",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minute",
"linkExpirationFormatter.timePeriodUnit.hours": "hours",
"linkExpirationFormatter.timePeriodUnit.hours.1": "hour",
"linkExpirationFormatter.timePeriodUnit.days": "days",
"linkExpirationFormatter.timePeriodUnit.days.1": "day",
"emailVerificationBodyCode": "Please verify your email address by entering in the following code.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>"
},
"es": {
"emailVerificationSubject": "Verificación de email",
"emailVerificationBody": "Alguien ha creado una cuenta de {2} con esta dirección de email. Si has sido tú, haz click en el enlace siguiente para verificar tu dirección de email.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi tú no has creado esta cuenta, simplemente ignora este mensaje.",
"emailVerificationBodyHtml": "<p>Alguien ha creado una cuenta de {2} con esta dirección de email. Si has sido tú, haz click en el enlace siguiente para verificar tu dirección de email.</p><p><a href=\"{0}\">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si tú no has creado esta cuenta, simplemente ignora este mensaje.</p>",
"passwordResetSubject": "Reiniciar contraseña",
"passwordResetBody": "Alguien ha solicitado cambiar las credenciales de tu cuenta de {2}. Si has sido tú, haz clic en el enlace siguiente para reiniciarlas.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi no quieres reiniciar tus credenciales, simplemente ignora este mensaje y no se realizará ningún cambio.",
"passwordResetBodyHtml": "<p>Alguien ha solicitado cambiar las credenciales de tu cuenta de {2}. Si has sido tú, haz clic en el enlace siguiente para reiniciarlas.</p><p><a href=\"{0}\">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si no quieres reiniciar tus credenciales, simplemente ignora este mensaje y no se realizará ningún cambio.</p>",
"executeActionsSubject": "Actualiza tu cuenta",
"executeActionsBody": "El administrador ha solicitado que actualices tu cuenta de {2}. Haz clic en el enlace inferior para iniciar este proceso.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi no estás al tanto de que el administrador haya solicitado esto, simplemente ignora este mensaje y no se realizará ningún cambio.",
"executeActionsBodyHtml": "<p>El administrador ha solicitado que actualices tu cuenta de {2}. Haz clic en el enlace inferior para iniciar este proceso.</p><p><a href=\"{0}\">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si no estás al tanto de que el administrador haya solicitado esto, simplemente ignora este mensaje y no se realizará ningún cambio.</p>",
"eventLoginErrorSubject": "Fallo en el inicio de sesión",
"eventLoginErrorBody": "Se ha detectado un intento de acceso fallido a tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
"eventLoginErrorBodyHtml": "<p>Se ha detectado un intento de acceso fallido a tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
"eventRemoveTotpSubject": "Borrado OTP",
"eventRemoveTotpBody": "OTP fue eliminado de tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
"eventRemoveTotpBodyHtml": "<p>OTP fue eliminado de tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
"eventUpdatePasswordSubject": "Actualización de contraseña",
"eventUpdatePasswordBody": "Tu contraseña se ha actualizado el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
"eventUpdatePasswordBodyHtml": "<p>Tu contraseña se ha actualizado el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
"eventUpdateTotpSubject": "Actualización de OTP",
"eventUpdateTotpBody": "OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
"eventUpdateTotpBodyHtml": "<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>"
},
"fr": {
"emailVerificationSubject": "Vérification du courriel",
"emailVerificationBody": "Quelqu'un vient de créer un compte \"{2}\" avec votre courriel. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous afin de vérifier votre adresse de courriel\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon, veuillez ignorer ce message.",
"emailVerificationBodyHtml": "<p>Quelqu'un vient de créer un compte \"{2}\" avec votre courriel. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous afin de vérifier votre adresse de courriel</p><p><a href=\"{0}\">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon, veuillez ignorer ce message.</p>",
"passwordResetSubject": "Réinitialiser le mot de passe",
"passwordResetBody": "Quelqu'un vient de demander une réinitialisation de mot de passe pour votre compte {2}. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous pour le mettre à jour.\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.",
"passwordResetBodyHtml": "<p>Quelqu'un vient de demander une réinitialisation de mot de passe pour votre compte {2}. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous pour le mettre à jour.</p><p><a href=\"{0}\">Lien pour réinitialiser votre mot de passe</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.</p>",
"executeActionsSubject": "Mettre à jour votre compte",
"executeActionsBody": "Votre administrateur vient de demander une mise à jour de votre compte {2}. Veuillez cliquer sur le lien ci-dessous afin de commencer le processus.\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSi vous n'êtes pas à l'origine de cette requête, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.",
"executeActionsBodyHtml": "<p>Votre administrateur vient de demander une mise à jour de votre compte {2}. Veuillez cliquer sur le lien ci-dessous afin de commencer le processus.</p><p><a href=\"{0}\">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Si vous n'êtes pas à l'origine de cette requête, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.</p>",
"eventLoginErrorSubject": "Erreur de connexion",
"eventLoginErrorBody": "Une tentative de connexion a été détectée sur votre compte {0} depuis {1}. Si vous n'êtes pas à l'origine de cette requête, veuillez contacter votre administrateur.",
"eventLoginErrorBodyHtml": "<p>Une tentative de connexion a été détectée sur votre compte {0} depuis {1}. Si vous n'êtes pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
"eventRemoveTotpSubject": "Suppression du OTP",
"eventRemoveTotpBody": "Le OTP a été supprimé de votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
"eventRemoveTotpBodyHtml": "<p>Le OTP a été supprimé de votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
"eventUpdatePasswordSubject": "Mise à jour du mot de passe",
"eventUpdatePasswordBody": "Votre mot de passe pour votre compte {0} a été modifié depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
"eventUpdatePasswordBodyHtml": "<p>Votre mot de passe pour votre compte {0} a été modifié depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
"eventUpdateTotpSubject": "Mise à jour du OTP",
"eventUpdateTotpBody": "Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
"eventUpdateTotpBodyHtml": "<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>"
},
"it": {
"emailVerificationSubject": "Verifica l'email",
"emailVerificationBody": "Qualcuno ha creato un account {2} con questo indirizzo email. Se sei stato tu, fai clic sul link seguente per verificare il tuo indirizzo email\n\n{0}\n\nQuesto link scadrà in {3}.\n\nSe non sei stato tu a creare questo account, ignora questo messaggio.",
"emailVerificationBodyHtml": "<p>Qualcuno ha creato un account {2} con questo indirizzo email. Se sei stato tu, fai clic sul link seguente per verificare il tuo indirizzo email</p><p><a href=\"{0}\">{0}</a></p><p>Questo link scadrà in {3}.</p><p>Se non sei stato tu a creare questo account, ignora questo messaggio.</p>",
"emailTestSubject": "[KEYCLOAK] - messaggio di test SMTP",
"emailTestBody": "Questo è un messaggio di test",
"emailTestBodyHtml": "<p>Questo è un messaggio di test</p>",
"identityProviderLinkSubject": "Link {0}",
"identityProviderLinkBody": "Qualcuno vuole associare il tuo account \"{1}\" con l'account \"{0}\" dell'utente {2}. Se sei stato tu, fai clic sul link seguente per associare gli account\n\n{3}\n\nQuesto link scadrà in {5}.\n\nSe non vuoi associare l'account, ignora questo messaggio. Se associ gli account, potrai accedere a {1} attraverso {0}.",
"identityProviderLinkBodyHtml": "<p>Qualcuno vuole associare il tuo account <b>{1}</b> con l'account <b>{0}</b> dell'utente {2}. Se sei stato tu, fai clic sul link seguente per associare gli account</p><p><a href=\"{3}\">{3}</a></p><p>Questo link scadrà in {5}.</p><p>Se non vuoi associare l'account, ignora questo messaggio. Se associ gli account, potrai accedere a {1} attraverso {0}.</p>",
"passwordResetSubject": "Reimposta la password",
"passwordResetBody": "Qualcuno ha appena richiesto di cambiare le credenziali di accesso al tuo account {2}. Se sei stato tu, fai clic sul link seguente per reimpostarle.\n\n{0}\n\nQuesto link e codice scadranno in {3}.\n\nSe non vuoi reimpostare le tue credenziali di accesso, ignora questo messaggio e non verrà effettuato nessun cambio.",
"passwordResetBodyHtml": "<p>Qualcuno ha appena richiesto di cambiare le credenziali di accesso al tuo account {2}. Se sei stato tu, fai clic sul link seguente per reimpostarle.</p><p><a href=\"{0}\">{0}</a></p><p>Questo link scadrà in {3}.</p><p>Se non vuoi reimpostare le tue credenziali di accesso, ignora questo messaggio e non verrà effettuato nessun cambio.</p>",
"executeActionsSubject": "Aggiorna il tuo account",
"executeActionsBody": "Il tuo amministratore ha appena richiesto un aggiornamento del tuo account {2} ed è necessario che tu esegua la/le seguente/i azione/i: {3}. Fai clic sul link seguente per iniziare questo processo.\n\n{0}\n\nQuesto link scadrà in {4}.\n\nSe non sei a conoscenza della richiesta del tuo amministratore, ignora questo messaggio e non verrà effettuato nessun cambio.",
"executeActionsBodyHtml": "<p>Il tuo amministratore ha appena richiesto un aggiornamento del tuo account {2} ed è necessario che tu esegua la/le seguente/i azione/i: {3}. Fai clic sul link seguente per iniziare questo processo.</p><p><a href=\"{0}\">Link to account update</a></p><p>Questo link scadrà in {4}.</p><p>Se non sei a conoscenza della richiesta del tuo amministratore, ignora questo messaggio e non verrà effettuato nessun cambio.</p>",
"eventLoginErrorSubject": "Errore di accesso",
"eventLoginErrorBody": "È stato rilevato un tentativo fallito di accesso al tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
"eventLoginErrorBodyHtml": "<p>È stato rilevato un tentativo fallito di accesso al tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
"eventRemoveTotpSubject": "Rimozione OTP (password temporanea valida una volta sola)",
"eventRemoveTotpBody": "La OTP (password temporanea valida una volta sola) è stata rimossa dal tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
"eventRemoveTotpBodyHtml": "<p>La OTP (password temporanea valida una volta sola) è stata rimossa dal tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
"eventUpdatePasswordSubject": "Aggiornamento password",
"eventUpdatePasswordBody": "La tua password è stata cambiata il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
"eventUpdatePasswordBodyHtml": "<p>La tua password è stata cambiata il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
"eventUpdateTotpSubject": "Aggiornamento OTP (password temporanea valida una volta sola)",
"eventUpdateTotpBody": "La OTP (password temporanea valida una volta sola) è stata aggiornata per il tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
"eventUpdateTotpBodyHtml": "<p>La OTP (password temporanea valida una volta sola) è stata aggiornata per il tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
"requiredAction.CONFIGURE_TOTP": "Configurazione OTP",
"requiredAction.terms_and_conditions": "Termini e condizioni",
"requiredAction.UPDATE_PASSWORD": "Aggiornamento password",
"requiredAction.UPDATE_PROFILE": "Aggiornamento profilo",
"requiredAction.VERIFY_EMAIL": "Verifica dell'indirizzo email",
"linkExpirationFormatter.timePeriodUnit.seconds": "secondi",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "secondo",
"linkExpirationFormatter.timePeriodUnit.minutes": "minuti",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuto",
"linkExpirationFormatter.timePeriodUnit.hours": "ore",
"linkExpirationFormatter.timePeriodUnit.hours.1": "ora",
"linkExpirationFormatter.timePeriodUnit.days": "giorni",
"linkExpirationFormatter.timePeriodUnit.days.1": "giorno",
"emailVerificationBodyCode": "Per favore verifica il tuo indirizzo email inserendo il codice seguente.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Per favore verifica il tuo indirizzo email inserendo il codice seguente.</p><p><b>{0}</b></p>"
},
"ja": {
"emailVerificationSubject": "Eメールの確認",
"emailVerificationBody": "このメールアドレスで{2}アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは{3}だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。",
"emailVerificationBodyHtml": "<p>このメールアドレスで{2}アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href=\"{0}\">メールアドレスの確認</a></p><p>このリンクは{3}だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>",
"emailTestSubject": "[KEYCLOAK] - SMTPテストメッセージ",
"emailTestBody": "これはテストメッセージです",
"emailTestBodyHtml": "<p>これはテストメッセージです</p>",
"identityProviderLinkSubject": "リンク {0}",
"identityProviderLinkBody": "あなたの\"{1}\"アカウントと{2}ユーザーの\"{0}\"アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。\n\n{3}\n\nこのリンクは{5}だけ有効です。\n\nもしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0}経由で{1}にログインすることができるようになります。",
"identityProviderLinkBodyHtml": "<p>あなたの<b>{1}</b>アカウントと{2}ユーザーの<b>{0}</b>アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href=\"{3}\">アカウントリンクの確認</a></p><p>このリンクは{5}だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0}経由で{1}にログインすることができるようになります。</p>",
"passwordResetSubject": "パスワードのリセット",
"passwordResetBody": "あなたの{2}アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。\n\n{0}\n\nこのリンクは{3}だけ有効です。\n\nもしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。",
"passwordResetBodyHtml": "<p>あなたの{2}アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。</p><p><a href=\"{0}\">パスワードのリセット</a></p><p>このリンクは{3}だけ有効です。</p><p>もしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>",
"executeActionsSubject": "アカウントの更新",
"executeActionsBody": "次のアクションを実行することにより、管理者よりあなたの{2}アカウントの更新が要求されています: {3}。以下のリンクをクリックしてこのプロセスを開始してください。\n\n{0}\n\nこのリンクは{4}だけ有効です。\n\n管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。",
"executeActionsBodyHtml": "<p>次のアクションを実行することにより、管理者よりあなたの{2}アカウントの更新が要求されています: {3}。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href=\"{0}\">アカウントの更新</a></p><p>このリンクは{4}だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>",
"eventLoginErrorSubject": "ログインエラー",
"eventLoginErrorBody": "{0}に{1}からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は、管理者に連絡してください。",
"eventLoginErrorBodyHtml": "<p>{0}に{1}からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は管理者に連絡してください。</p>",
"eventRemoveTotpSubject": "OTPの削除",
"eventRemoveTotpBody": "{0}に{1}からの操作でOTPが削除されました。心当たりがない場合は、管理者に連絡してください。",
"eventRemoveTotpBodyHtml": "<p>{0}に{1}からの操作でOTPが削除されました。心当たりがない場合は、管理者に連絡してください。</p>",
"eventUpdatePasswordSubject": "パスワードの更新",
"eventUpdatePasswordBody": "{0}に{1}からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。",
"eventUpdatePasswordBodyHtml": "<p>{0}に{1}からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。</p>",
"eventUpdateTotpSubject": "OTPの更新",
"eventUpdateTotpBody": "{0}に{1}からの操作でOTPが更新されました。心当たりがない場合は、管理者に連絡してください。",
"eventUpdateTotpBodyHtml": "<p>{0}に{1}からの操作でOTPが更新されました。心当たりがない場合は、管理者に連絡してください。</p>",
"requiredAction.CONFIGURE_TOTP": "OTPの設定",
"requiredAction.terms_and_conditions": "利用規約",
"requiredAction.UPDATE_PASSWORD": "パスワードの更新",
"requiredAction.UPDATE_PROFILE": "プロファイルの更新",
"requiredAction.VERIFY_EMAIL": "Eメールの確認",
"linkExpirationFormatter.timePeriodUnit.seconds": "秒",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "秒",
"linkExpirationFormatter.timePeriodUnit.minutes": "分",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "分",
"linkExpirationFormatter.timePeriodUnit.hours": "時間",
"linkExpirationFormatter.timePeriodUnit.hours.1": "時間",
"linkExpirationFormatter.timePeriodUnit.days": "日",
"linkExpirationFormatter.timePeriodUnit.days.1": "日",
"emailVerificationBodyCode": "次のコードを入力してメールアドレスを確認してください。\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>次のコードを入力してメールアドレスを確認してください。</p><p><b>{0}</b></p>"
},
"lt": {
"emailVerificationSubject": "El. pašto patvirtinimas",
"emailVerificationBody": "Paskyra {2} sukurta naudojant šį el. pašto adresą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą\n\n{0}\n\nŠi nuoroda galioja {1} min.\n\nJei paskyros nekūrėte, tuomet ignuoruokite šį laišką. ",
"emailVerificationBodyHtml": "<p>Paskyra {2} sukurta naudojant šį el. pašto adresą. Jei tao buvote Jūs, tuomet paspauskite žemiau esančią nuorodą</p><p><a href=LT\"{0}\">{0}</a></p><p>Ši nuoroda galioja {1} min.</p><p>nJei paskyros nekūrėte, tuomet ignuoruokite šį laišką.</p>",
"identityProviderLinkSubject": "Sąsaja {0}",
"identityProviderLinkBody": "Kažas pageidauja susieti Jūsų \"{1}\" paskyrą su \"{0}\" {2} naudotojo paskyrą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą norėdami susieti paskyras\n\n{3}\n\nŠi nuoroda galioja {4} min.\n\nJei paskyrų susieti nenorite, tuomet ignoruokite šį laišką. Jei paskyras susiesite, tuomet prie {1} galėsiste prisijungti per {0}.",
"identityProviderLinkBodyHtml": "<p>žas pageidauja susieti Jūsų <b>{1}</b> paskyrą su <b>{0}</b> {2} naudotojo paskyrą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą norėdami susieti paskyras</p><p><a href=LT\"{3}\">{3}</a></p><p>Ši nuoroda galioja {4} min.</p><p>Jei paskyrų susieti nenorite, tuomet ignoruokite šį laišką. Jei paskyras susiesite, tuomet prie {1} galėsiste prisijungti per {0}.</p>",
"passwordResetSubject": "Slaptažodžio atkūrimas",
"passwordResetBody": "Kažkas pageidauja pakeisti Jūsų paskyros {2} slaptažodį. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą slaptažodžio pakeitimui.\n\n{0}\n\nŠi nuoroda ir kodas galioja {1} min.\n\nJei nepageidajate keisti slaptažodžio, tuomet ignoruokite šį laišką ir niekas nebus pakeista.",
"passwordResetBodyHtml": "<p>Kažkas pageidauja pakeisti Jūsų paskyros {2} slaptažodį. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą slaptažodžio pakeitimui.</p><p><a href=LT\"{0}\">{0}</a></p><p>Ši nuoroda ir kodas galioja {1} min.</p><p>Jei nepageidajate keisti slaptažodžio, tuomet ignoruokite šį laišką ir niekas nebus pakeista.</p>",
"executeActionsSubject": "Atnaujinkite savo paskyrą",
"executeActionsBody": "Sistemos administratorius pageidauja, kad Jūs atnaujintumėte savo {2} paskyrą. Paspauskite žemiau esančią nuorodą paskyros duomenų atnaujinimui.\n\n{0}\n\nŠi nuoroda galioja {1} min.\n\nJei Jūs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite šį laišką ir niekas nebus pakeista.",
"executeActionsBodyHtml": "<p>Sistemos administratorius pageidauja, kad Jūs atnaujintumėte savo {2} paskyrą. Paspauskite žemiau esančią nuorodą paskyros duomenų atnaujinimui.</p><p><a href=LT\"{0}\">{0}</a></p><p>Ši nuoroda galioja {1} min.</p><p>Jei Jūs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite šį laišką ir niekas nebus pakeista.</p>",
"eventLoginErrorSubject": "Nesėkmingas bandymas prisijungti prie jūsų paskyros",
"eventLoginErrorBody": "Bandymas prisijungti prie jūsų paskyros {0} iš {1} nesėkmingas. Jei tai nebuvote jūs, tuomet susisiekite su administratoriumi",
"eventLoginErrorBodyHtml": "<p>Bandymas prisijungti prie jūsų paskyros {0} iš {1} nesėkmingas. Jei tai nebuvote jūs, tuomet susisiekite su administratoriumi</p>",
"eventRemoveTotpSubject": "OTP pašalinimas",
"eventRemoveTotpBody": "Kažkas pageidauja atsieti TOPT Jūsų {1} paskyroje su {0}. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi",
"eventRemoveTotpBodyHtml": "<p>Kažkas pageidauja atsieti TOPT Jūsų <b>{1}</b> paskyroje su <b>{0}</b>. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>",
"eventUpdatePasswordSubject": "Slaptažodžio atnaujinimas",
"eventUpdatePasswordBody": "{1} paskyroje {0} pakeisas jūsų slaptažodis. Jei Jūs nekeitėte, tuomet susisiekite su administratoriumi",
"eventUpdatePasswordBodyHtml": "<p>{1} paskyroje {0} pakeisas jūsų slaptažodis. Jei Jūs nekeitėte, tuomet susisiekite su administratoriumi</p>",
"eventUpdateTotpSubject": "OTP atnaujinimas",
"eventUpdateTotpBody": "OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi",
"eventUpdateTotpBodyHtml": "<p>OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>"
},
"nl": {
"emailVerificationSubject": "Bevestig e-mailadres",
"emailVerificationBody": "Iemand heeft een {2} account aangemaakt met dit e-mailadres. Als u dit was, klikt u op de onderstaande koppeling om uw e-mailadres te bevestigen \n\n{0}\n\nDeze koppeling zal binnen {3} vervallen.\n\nU kunt dit bericht negeren indien u dit account niet heeft aangemaakt.",
"emailVerificationBodyHtml": "<p>Iemand heeft een {2} account aangemaakt met dit e-mailadres. Als u dit was, klikt u op de onderstaande koppeling om uw e-mailadres te bevestigen</p><p><a href=\"{0}\">Koppeling naar e-mailadres bevestiging</a></p><p>Deze koppeling zal binnen {3} vervallen.</p<p>U kunt dit bericht negeren indien u dit account niet heeft aangemaakt.</p>",
"emailTestSubject": "[KEYCLOAK] - SMTP testbericht",
"emailTestBody": "Dit is een testbericht",
"emailTestBodyHtml": "<p>Dit is een testbericht</p>",
"identityProviderLinkSubject": "Koppel {0}",
"identityProviderLinkBody": "Iemand wil uw \"{1}\" account koppelen met \"{0}\" account van gebruiker {2}. Als u dit was, klik dan op de onderstaande link om de accounts te koppelen\n\n{3}\n\nDeze link zal over {5} vervallen.\n\nAls u de accounts niet wilt koppelen, negeer dan dit bericht. Als u accounts koppelt, dan kunt u bij {1} inloggen via {0}.",
"identityProviderLinkBodyHtml": "<p>Iemand wil uw \"{1}\" account koppelen met \"{0}\" account van gebruiker {2}. Als u dit was, klik dan op de onderstaande link om de accounts te koppelen</p><p><a href=\"{3}\">Link om accounts te koppelen</a></p><p>Deze link zal over {5} vervallen.</p><p>Als u de accounts niet wilt koppelen, negeer dan dit bericht. Als u accounts koppelt, dan kunt u bij {1} inloggen via {0}.</p>",
"passwordResetSubject": "Wijzig wachtwoord",
"passwordResetBody": "Iemand verzocht de aanmeldgegevens van uw {2} account te wijzigen. Als u dit was, klik dan op de onderstaande koppeling om ze te wijzigen.\n\n{0}\n\nDe link en de code zullen binnen {3} vervallen.\n\nAls u uw aanmeldgegevens niet wilt wijzigen, negeer dan dit bericht en er zal niets gewijzigd worden.",
"passwordResetBodyHtml": "<p>Iemand verzocht de aanmeldgegevens van uw {2} account te wijzigen. Als u dit was, klik dan op de onderstaande koppeling om ze te wijzigen.</p><p><a href=\"{0}\">Wijzig aanmeldgegevens</a></p><p>De link en de code zullen binnen {3} vervallen.</p><p>Als u uw aanmeldgegevens niet wilt wijzigen, negeer dan dit bericht en er zal niets gewijzigd worden.</p>",
"executeActionsSubject": "Wijzig uw account",
"executeActionsBody": "Uw beheerder heeft u verzocht uw {2} account te wijzigen. Klik op de onderstaande koppeling om dit proces te starten. \n\n{0}\n\nDeze link zal over {4} vervallen. \n\nAls u niet over dit verzoek op de hoogte was, negeer dan dit bericht om uw account ongewijzigd te laten.",
"executeActionsBodyHtml": "<p>Uw beheerder heeft u verzocht uw {2} account te wijzigen. Klik op de onderstaande koppeling om dit proces te starten.</p><p><a href=\"{0}\">Link naar account wijziging</a></p><p>Deze link zal over {4} vervallen.</p><p>Als u niet over dit verzoek op de hoogte was, negeer dan dit bericht om uw account ongewijzigd te laten.</p>",
"eventLoginErrorSubject": "Inlogfout",
"eventLoginErrorBody": "Er is een foutieve inlogpoging gedetecteerd op uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met de beheerder.",
"eventLoginErrorBodyHtml": "<p>Er is een foutieve inlogpoging gedetecteerd op uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met de beheerder.</p>",
"eventRemoveTotpSubject": "OTP verwijderd",
"eventRemoveTotpBody": "OTP is verwijderd van uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
"eventRemoveTotpBodyHtml": "<p>OTP is verwijderd van uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
"eventUpdatePasswordSubject": "Wachtwoord gewijzigd",
"eventUpdatePasswordBody": "Uw wachtwoord is gewijzigd om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
"eventUpdatePasswordBodyHtml": "<p>Uw wachtwoord is gewijzigd om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
"eventUpdateTotpSubject": "OTP gewijzigd",
"eventUpdateTotpBody": "OTP is gewijzigd voor uw account om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
"eventUpdateTotpBodyHtml": "<p>OTP is gewijzigd voor uw account om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
"linkExpirationFormatter.timePeriodUnit.seconds": "seconden",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "seconde",
"linkExpirationFormatter.timePeriodUnit.minutes": "minuten",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuut",
"linkExpirationFormatter.timePeriodUnit.hours": "uur",
"linkExpirationFormatter.timePeriodUnit.hours.1": "uur",
"linkExpirationFormatter.timePeriodUnit.days": "dagen",
"linkExpirationFormatter.timePeriodUnit.days.1": "dag"
},
"no": {
"emailVerificationSubject": "Bekreft e-postadresse",
"emailVerificationBody": "Noen har opprettet en {2} konto med denne e-postadressen. Hvis dette var deg, klikk på lenken nedenfor for å bekrefte e-postadressen din\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke opprettet denne kontoen, vennligst ignorer denne meldingen.",
"emailVerificationBodyHtml": "<p>Noen har opprettet en {2} konto med denne e-postadressen. Hvis dette var deg, klikk på lenken nedenfor for å bekrefte e-postadressen din</p><p><a href=\"{0}\">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke opprettet denne kontoen, vennligst ignorer denne meldingen.</p>",
"identityProviderLinkSubject": "Lenke {0}",
"identityProviderLinkBody": "Noen vil koble din <b>{1}</b> konto med <b>{0}</b> konto til bruker {2}. Hvis dette var deg, klikk på lenken nedenfor for å koble kontoene\n\n{3}\n\nDenne lenken vil utløpe om {4} minutter\n\nHvis du ikke vil koble kontoene, vennligst ignorer denne meldingen. Hvis du kobler kontoene sammen vil du kunne logge inn til {1} gjennom {0}.",
"identityProviderLinkBodyHtml": "<p>Noen vil koble din <b>{1}</b> konto med <b>{0}</b> konto til bruker {2}. Hvis dette var deg, klikk på lenken nedenfor for å koble kontoene.</p><p><a href=\"{3}\">{3}</a></p><p>Denne lenken vil utløpe om {4} minutter.</p><p>Hvis du ikke vil koble kontoene, vennligst ignorer denne meldingen. Hvis du kobler kontoene sammen vil du kunne logge inn til {1} gjennom {0}.</p>",
"passwordResetSubject": "Tilbakestill passord",
"passwordResetBody": "Noen har bedt om å endre innloggingsdetaljene til din konto {2}. Hvis dette var deg, klikk på lenken nedenfor for å tilbakestille dem.\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke vil tilbakestille din innloggingsdata, vennligst ignorer denne meldingen og ingenting vil bli endret.",
"passwordResetBodyHtml": "<p>Noen har bedt om å endre innloggingsdetaljene til din konto {2}. Hvis dette var deg, klikk på lenken nedenfor for å tilbakestille dem.</p><p><a href=\"{0}\">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke vil tilbakestille din innloggingsdata, vennligst ignorer denne meldingen og ingenting vil bli endret.</p>",
"executeActionsSubject": "Oppdater kontoen din",
"executeActionsBody": "Administrator har anmodet at du oppdaterer din {2} konto. Klikk på lenken nedenfor for å starte denne prosessen\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke var klar over at administrator har bedt om dette, vennligst ignorer denne meldingen og ingenting vil bli endret.",
"executeActionsBodyHtml": "<p>Administrator har anmodet at du oppdaterer din {2} konto. Klikk på linken nedenfor for å starte denne prosessen.</p><p><a href=\"{0}\">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke var klar over at administrator har bedt om dette, ignorer denne meldingen og ingenting vil bli endret. </p>",
"eventLoginErrorSubject": "Innlogging feilet",
"eventLoginErrorBody": "Et mislykket innloggingsforsøk ble oppdaget på din konto på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
"eventLoginErrorBodyHtml": "<p>Et mislykket innloggingsforsøk ble oppdaget på din konto på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.</p>",
"eventRemoveTotpSubject": "Fjern engangskode",
"eventRemoveTotpBody": "Engangskode ble fjernet fra kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
"eventRemoveTotpBodyHtml": "<p>Engangskode ble fjernet fra kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.</p>",
"eventUpdatePasswordSubject": "Oppdater passord",
"eventUpdatePasswordBody": "Ditt passord ble endret i {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
"eventUpdatePasswordBodyHtml": "<p>Ditt passord ble endret i {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>",
"eventUpdateTotpSubject": "Oppdater engangskode",
"eventUpdateTotpBody": "Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
"eventUpdateTotpBodyHtml": "<p>Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>"
},
"pl": {
"emailVerificationSubject": "Zweryfikuj email",
"emailVerificationBody": "Ktoś utworzył już konto {2} z tym adresem e-mail. Jeśli to Ty, kliknij poniższy link, aby zweryfikować swój adres e-mail \n\n{0}\n\nLink ten wygaśnie w ciągu {3}.\n\nJeśli nie utworzyłeś tego konta, po prostu zignoruj tę wiadomość.",
"emailVerificationBodyHtml": "<p>Ktoś utworzył już konto {2} z tym adresem e-mail. Jeśli to Ty, kliknij <a href=\"{0}\">ten link</a> aby zweryfikować swój adres e-mail</p><p>Link ten wygaśnie w ciągu {3}</p><p>Jeśli nie utworzyłeś tego konta, po prostu zignoruj tę wiadomość.</p>",
"emailTestSubject": "[KEYCLOAK] - wiadomość testowa SMTP",
"emailTestBody": "To jest wiadomość testowa",
"emailTestBodyHtml": "<p>To jest wiadomość testowa</p>",
"identityProviderLinkSubject": "Link {0}",
"identityProviderLinkBody": "Ktoś chce połączyć Twoje konto \"{1}\" z kontem \"{0}\" użytkownika {2}. Jeśli to Ty, kliknij poniższy link by połączyć konta\n\n{3}\n\nTen link wygaśnie w ciągu {5}.\n\nJeśli nie chcesz połączyć konta to zignoruj tę wiadomość. Jeśli połączysz konta, będziesz mógł się zalogować na {1} przez {0}.",
"identityProviderLinkBodyHtml": "<p>Ktoś chce połączyć Twoje konto <b>{1}</b> z kontem <b>{0}</b> użytkownika {2}. Jeśli to Ty, kliknij <a href=\"{3}\">ten link</a> by połączyć konta.</p><p>Ten link wygaśnie w ciągu {5}.</p><p>Jeśli nie chcesz połączyć konta to zignoruj tę wiadomość. Jeśli połączysz konta, będziesz mógł się zalogować na {1} przez {0}.</p>",
"passwordResetSubject": "Zresetuj hasło",
"passwordResetBody": "Ktoś właśnie poprosił o zmianę danych logowania Twojego konta {2}. Jeśli to Ty, kliknij poniższy link, aby je zresetować.\n\n{0}\n\nTen link i kod stracą ważność w ciągu {3}.\n\nJeśli nie chcesz zresetować swoich danych logowania, po prostu zignoruj tę wiadomość i nic się nie zmieni.",
"passwordResetBodyHtml": "<p>Ktoś właśnie poprosił o zmianę poświadczeń Twojego konta {2}. Jeśli to Ty, kliknij poniższy link, aby je zresetować.</p><p><a href=\"{0}\">Link do resetowania poświadczeń</a></p><p>Ten link wygaśnie w ciągu {3}.</p><p>Jeśli nie chcesz resetować swoich poświadczeń, po prostu zignoruj tę wiadomość i nic się nie zmieni.</p>",
"executeActionsSubject": "Zaktualizuj swoje konto",
"executeActionsBody": "Administrator właśnie zażądał aktualizacji konta {2} poprzez wykonanie następujących działań: {3}. Kliknij poniższy link, aby rozpocząć ten proces.\n\n{0}\n\nTen link wygaśnie w ciągu {4}.\n\nJeśli nie masz pewności, że administrator tego zażądał, po prostu zignoruj tę wiadomość i nic się nie zmieni.",
"executeActionsBodyHtml": "<p>Administrator właśnie zażądał aktualizacji konta {2} poprzez wykonanie następujących działań: {3}. Kliknij <a href=\"{0}\">ten link</a>, aby rozpocząć proces.</p><p>Link ten wygaśnie w ciągu {4}.</p><p>Jeśli nie masz pewności, że administrator tego zażądał, po prostu zignoruj tę wiadomość i nic się nie zmieni.</p>",
"eventLoginErrorSubject": "Błąd logowania",
"eventLoginErrorBody": "Nieudana próba logowania została wykryta na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
"eventLoginErrorBodyHtml": "<p>Nieudana próba logowania została wykryta na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
"eventRemoveTotpSubject": "Usuń hasło jednorazowe (OTP)",
"eventRemoveTotpBody": "Hasło jednorazowe (OTP) zostało usunięte z Twojego konta w {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
"eventRemoveTotpBodyHtml": "<p>Hasło jednorazowe (OTP) zostało usunięte z Twojego konta w {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
"eventUpdatePasswordSubject": "Aktualizuj hasło",
"eventUpdatePasswordBody": "Twoje hasło zostało zmienione {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
"eventUpdatePasswordBodyHtml": "<p>Twoje hasło zostało zmienione {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
"eventUpdateTotpSubject": "Aktualizuj hasło jednorazowe (OTP)",
"eventUpdateTotpBody": "Hasło jednorazowe (OTP) zostało zaktualizowane na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
"eventUpdateTotpBodyHtml": "<p>Hasło jednorazowe (OTP) zostało zaktualizowane na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
"requiredAction.CONFIGURE_TOTP": "Konfiguracja hasła jednorazowego (OTP)",
"requiredAction.terms_and_conditions": "Regulamin",
"requiredAction.UPDATE_PASSWORD": "Aktualizacja hasła",
"requiredAction.UPDATE_PROFILE": "Aktualizacja profilu",
"requiredAction.VERIFY_EMAIL": "Weryfikacja adresu e-mail",
"linkExpirationFormatter.timePeriodUnit.seconds": "sekund",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
"linkExpirationFormatter.timePeriodUnit.seconds.2": "sekundy",
"linkExpirationFormatter.timePeriodUnit.seconds.3": "sekundy",
"linkExpirationFormatter.timePeriodUnit.seconds.4": "sekundy",
"linkExpirationFormatter.timePeriodUnit.minutes": "minut",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuta",
"linkExpirationFormatter.timePeriodUnit.minutes.2": "minuty",
"linkExpirationFormatter.timePeriodUnit.minutes.3": "minuty",
"linkExpirationFormatter.timePeriodUnit.minutes.4": "minuty",
"linkExpirationFormatter.timePeriodUnit.hours": "godzin",
"linkExpirationFormatter.timePeriodUnit.hours.1": "godzina",
"linkExpirationFormatter.timePeriodUnit.hours.2": "godziny",
"linkExpirationFormatter.timePeriodUnit.hours.3": "godziny",
"linkExpirationFormatter.timePeriodUnit.hours.4": "godziny",
"linkExpirationFormatter.timePeriodUnit.days": "dni",
"linkExpirationFormatter.timePeriodUnit.days.1": "dzień",
"emailVerificationBodyCode": "Potwierdź swój adres e-mail wprowadzając następujący kod.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Potwierdź swój adres e-mail, wprowadzając następujący kod.</p><p><b>{0}</b></p>"
},
"pt-BR": {
"emailVerificationSubject": "Verificação de e-mail",
"emailVerificationBody": "Alguém criou uma conta {2} com este endereço de e-mail. Se foi você, clique no link abaixo para verificar o seu endereço de email\n\n{0}\n\nEste link irá expirar dentro de {3}.\n\nSe não foi você que criou esta conta, basta ignorar esta mensagem.",
"emailVerificationBodyHtml": "<p>Alguém criou uma conta {2} com este endereço de e-mail. Se foi você, clique no link abaixo para verificar o seu endereço de email</p><p><a href=\"{0}\">{0}</a></p><p>Este link irá expirar dentro de {3}.</p><p>Se não foi você que criou esta conta, basta ignorar esta mensagem.</p>",
"emailTestSubject": "[KEYCLOAK] - SMTP mensagem de teste",
"emailTestBody": "Esta é uma mensagem de teste",
"emailTestBodyHtml": "<p>Esta é uma mensagem de teste</p>",
"identityProviderLinkSubject": "Vincular {0}",
"identityProviderLinkBody": "Alguém quer vincular sua conta \"{1}\" com a conta \"{0}\" do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.\n\n{3}\n\nEste link irá expirar em {5}.\n\nSe você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} atrávés de {0}.",
"identityProviderLinkBodyHtml": "<p>Alguém quer vincular sua conta <b>{1}</b> com a conta <b>{0}</b> do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.</p><p><a href=\"{3}\">{3}</a></p><p>Este link irá expirar em {5}.</p><p>Se você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} atrávés de {0}.</p>",
"passwordResetSubject": "Redefinição de senha",
"passwordResetBody": "Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.\n\n{0}\n\nEste link e código expiram em {3}.\n\nSe você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado.",
"passwordResetBodyHtml": "<p>Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.</p><p><a href=\"{0}\">Link para redefinir a senha</a></p><p>Este link irá expirar em {3}.</p><p>Se você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado.</p>",
"executeActionsSubject": "Atualização de conta",
"executeActionsBody": "O administrador solicitou que você atualize sua conta {2} executando a(s) seguinte(s) ação(ões): {3}. Clique no link abaixo para iniciar o processo.\n\n{0}\n\nEste link irá expirar em {4}.\n\nSe você não tem conhecimento de que o administrador solicitou isso, basta ignorar esta mensagem e nada será alterado.",
"executeActionsBodyHtml": "<p>O administrador solicitou que você atualize sua conta {2} executando a(s) seguinte(s) ação(ões): {3}. Clique no link abaixo para iniciar o processo.</p><p><a href=\"{0}\">Link to account update</a></p><p>Este link irá expirar em {4}.</p><p>Se você não tem conhecimento de que o administrador solicitou isso, basta ignorar esta mensagem e nada será alterado.</p>",
"eventLoginErrorSubject": "Erro de login",
"eventLoginErrorBody": "Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
"eventLoginErrorBodyHtml": "<p>Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
"eventRemoveTotpSubject": "Remover OTP",
"eventRemoveTotpBody": "OTP foi removido da sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
"eventRemoveTotpBodyHtml": "<p>OTP foi removido da sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
"eventUpdatePasswordSubject": "Atualização de senha",
"eventUpdatePasswordBody": "Sua senha foi alterada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
"eventUpdatePasswordBodyHtml": "<p>Sua senha foi alterada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
"eventUpdateTotpSubject": "Atualização OTP",
"eventUpdateTotpBody": "OTP foi atualizado para a sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
"eventUpdateTotpBodyHtml": "<p>OTP foi atualizado para a sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
"requiredAction.CONFIGURE_TOTP": "Configurar OTP",
"requiredAction.terms_and_conditions": "Termos e Condições",
"requiredAction.UPDATE_PASSWORD": "Atualizar Senha",
"requiredAction.UPDATE_PROFILE": "Atualizar Perfil",
"requiredAction.VERIFY_EMAIL": "Verificar E-mail",
"linkExpirationFormatter.timePeriodUnit.seconds": "segundos",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "segundo",
"linkExpirationFormatter.timePeriodUnit.minutes": "minutos",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuto",
"linkExpirationFormatter.timePeriodUnit.hours": "horas",
"linkExpirationFormatter.timePeriodUnit.hours.1": "hora",
"linkExpirationFormatter.timePeriodUnit.days": "dias",
"linkExpirationFormatter.timePeriodUnit.days.1": "dia",
"emailVerificationBodyCode": "Verifique seu endereço de e-mail digitando o seguinte código.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Verifique seu endereço de e-mail digitando o seguinte código.</p><p><b>{0}</b></p>"
},
"ru": {
"emailVerificationSubject": "Подтверждение E-mail",
"emailVerificationBody": "Кто-то создал учетную запись {2} с этим E-mail. Если это были Вы, нажмите на следующую ссылку для подтверждения вашего email\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли Вы не создавали учетную запись, просто проигнорируйте это письмо.",
"emailVerificationBodyHtml": "<p>Кто-то создал учетную запись {2} с этим E-mail. Если это были Вы, нажмите по ссылке для подтверждения вашего E-mail</p><p><a href=\"{0}\">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если Вы не создавали учетную запись, просто проигнорируйте это письмо.</p>",
"identityProviderLinkSubject": "Ссылка {0}",
"identityProviderLinkBody": "Кто-то хочет связать вашу учетную запись \"{1}\" с \"{0}\" учетной записью пользователя {2} . Если это были Вы, нажмите по следующей ссылке, чтобы связать учетные записи\n\n{3}\n\nЭта ссылка устареет через {4} минут.\n\nЕсли это не хотите объединять учетные записи, просто проигнориуйте это письмо. После объединения учетных записей Вы можете войти в {1} через {0}.",
"identityProviderLinkBodyHtml": "<p>Кто-то хочет связать вашу учетную запись <b>{1}</b> с <b>{0}</b> учетной записью пользователя {2} . Если это были Вы, нажмите по следующей ссылке, чтобы связать учетные записи</p><p><a href=\"{3}\">{3}</a></p><p>Эта ссылка устареет через {4} минут.</p><p>Если это не хотите объединять учетные записи, просто проигнориуйте это письмо. После объединения учетных записей Вы можете войти в {1} через {0}.</p>",
"passwordResetSubject": "Сброс пароля",
"passwordResetBody": "Кто-то только что запросил изменение пароля от Вашей учетной записи {2}. Если это были Вы, нажмите на следующую ссылку, чтобы сбросить его.\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли Вы не хотите сбрасывать пароль, просто проигнорируйте это письмо.",
"passwordResetBodyHtml": "<p>Кто-то только что запросил изменение пароля от Вашей учетной записи {2}. Если это были Вы, нажмите на следующую ссылку, чтобы сбросить его.</p><p><a href=\"{0}\">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если Вы не хотите сбрасывать пароль, просто проигнорируйте это письмо и ничего не изменится.</p>",
"executeActionsSubject": "Обновление Вашей учетной записи",
"executeActionsBody": "Администратор просит Вас обновить данные Вашей учетной записи {2}. Нажмите по следующей ссылке чтобы начать этот процесс.\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли у вас есть подозрения, что администратор не мог сделать такой запрос, просто проигнорируйте это письмо.",
"executeActionsBodyHtml": "<p>Администратор просит Вас обновить данные Вашей учетной записи {2}. Нажмите по следующей ссылке чтобы начать этот процесс.</p><p><a href=\"{0}\">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если у вас есть подозрения, что администратор не мог сделать такой запрос, просто проигнорируйте это письмо.</p>",
"eventLoginErrorSubject": "Ошибка входа",
"eventLoginErrorBody": "Была зафиксирована неудачная попытка входа в Вашу учетную запись {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
"eventLoginErrorBodyHtml": "<p>Была зафиксирована неудачная попытка входа в Вашу учетную запись {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
"eventRemoveTotpSubject": "Удалить OTP",
"eventRemoveTotpBody": "OTP был удален из вашей учетной записи {0} c {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
"eventRemoveTotpBodyHtml": "<p>OTP был удален из вашей учетной записи {0} c {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
"eventUpdatePasswordSubject": "Обновление пароля",
"eventUpdatePasswordBody": "Ваш пароль был изменен в {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
"eventUpdatePasswordBodyHtml": "<p>Ваш пароль был изменен в {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
"eventUpdateTotpSubject": "Обновление OTP",
"eventUpdateTotpBody": "OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
"eventUpdateTotpBodyHtml": "<p>OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>"
},
"sk": {
"emailVerificationSubject": "Overenie e-mailu",
"emailVerificationBody": "Niekto vytvoril účet {2} s touto e-mailovou adresou. Ak ste to vy, kliknite na nižšie uvedený odkaz a overte svoju e-mailovú adresu \n\n{0}\n\nTento odkaz uplynie do {1} minút.\n\nAk ste tento účet nevytvorili, ignorujte túto správu.",
"emailVerificationBodyHtml": "<p>Niekto vytvoril účet {2} s touto e-mailovou adresou. Ak ste to vy, kliknite na nižšie uvedený odkaz na overenie svojej e-mailovej adresy.</p><p><a href=\"{0}\"> Odkaz na overenie e-mailovej adresy </a></p><p>Platnosť odkazu vyprší za {1} minút.</p><p> Ak ste tento účet nevytvorili, ignorujte túto správu.</p>",
"emailTestSubject": "[KEYCLOAK] - Testovacia správa SMTP",
"emailTestBody": "Toto je skúšobná správa",
"emailTestBodyHtml": "<p>Toto je skúšobná správa</p>",
"identityProviderLinkSubject": "Odkaz {0}",
"identityProviderLinkBody": "Niekto chce prepojiť váš účet \"{1}\" s účtom {0}\"používateľa {2}. Ak ste to vy, kliknutím na odkaz nižšie prepojte účty. \n\n{3}\n\nTento odkaz uplynie do {4} minút.\n\nAk nechcete prepojiť účet, jednoducho ignorujte túto správu , Ak prepájate účty, budete sa môcť prihlásiť do {1} až {0}.",
"identityProviderLinkBodyHtml": "<p>Niekto chce prepojiť váš účet <b>{1}</b> s účtom <b>{0}</b> používateľa {2}. Ak ste to vy, kliknutím na odkaz nižšie prepojte účty</p><p><a href=\"{3}\">Odkaz na potvrdenie prepojenia účtu </a></p><p> Platnosť tohto odkazu vyprší v rámci {4} minút.</p><p>Ak nechcete prepojiť účet, ignorujte túto správu. Ak prepojujete účty, budete sa môcť prihlásiť do {1} až {0}.</p>",
"passwordResetSubject": "Obnovenie hesla",
"passwordResetBody": "Niekto požiadal, aby ste zmenili svoje poverenia účtu {2}. Ak ste to vy, kliknite na odkaz uvedený nižšie, aby ste ich vynulovali.\n\n{0}\n\nTento odkaz a kód uplynie do {1} minút.\n\nAk nechcete obnoviť svoje poverenia , ignorujte túto správu a nič sa nezmení.",
"passwordResetBodyHtml": "<p>Niekto požiadal, aby ste zmenili svoje poverenia účtu {2}. Ak ste to vy, kliknutím na odkaz nižšie ich resetujte.</p><p><a href=\"{0}\">Odkaz na obnovenie poverení </a></p><p>Platnosť tohto odkazu vyprší v priebehu {1} minút.</p><p>Ak nechcete obnoviť svoje poverenia, ignorujte túto správu a nič sa nezmení.</p>",
"executeActionsSubject": "Aktualizujte svoj účet",
"executeActionsBody": "Váš administrátor práve požiadal o aktualizáciu vášho účtu {2} vykonaním nasledujúcich akcií: {3}. Kliknutím na odkaz uvedený nižšie spustíte tento proces.\n\n{0}\n\nTento odkaz vyprší za {1} minúty.\n\nAk si nie ste vedomý, že váš adminstrátor o toto požiadal, ignorujte túto správu a nič bude zmenené.",
"executeActionsBodyHtml": "<p>Váš správca práve požiadal o aktualizáciu vášho účtu {2} vykonaním nasledujúcich akcií: {3}. Kliknutím na odkaz uvedený nižšie spustíte tento proces.</p><p><a href=\"{0}\"> Odkaz na aktualizáciu účtu </a></p><p> Platnosť tohto odkazu uplynie do {1} minúty.</p><p> Ak si nie ste vedomí, že váš adminstrátor o toto požiadal, ignorujte túto správu a nič sa nezmení.</p>",
"eventLoginErrorSubject": "Chyba prihlásenia",
"eventLoginErrorBody": "Bol zistený neúspešný pokus o prihlásenie do vášho účtu v {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
"eventLoginErrorBodyHtml": "<p>Bol zistený neúspešný pokus o prihlásenie vášho účtu na {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
"eventRemoveTotpSubject": "Odstrániť TOTP",
"eventRemoveTotpBody": "OTP bol odstránený z vášho účtu dňa {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
"eventRemoveTotpBodyHtml": "<p>OTP bol odstránený z vášho účtu dňa {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
"eventUpdatePasswordSubject": "Aktualizovať heslo",
"eventUpdatePasswordBody": "Vaše heslo bolo zmenené na {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
"eventUpdatePasswordBodyHtml": "<p>Vaše heslo bolo zmenené na {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
"eventUpdateTotpSubject": "Aktualizácia TOTP",
"eventUpdateTotpBody": "TOTP bol aktualizovaný pre váš účet na {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
"eventUpdateTotpBodyHtml": "<p>TOTP bol aktualizovaný pre váš účet dňa {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
"requiredAction.CONFIGURE_TOTP": "Konfigurácia OTP",
"requiredAction.terms_and_conditions": "Zmluvné podmienky",
"requiredAction.UPDATE_PASSWORD": "Aktualizovať heslo",
"requiredAction.UPDATE_PROFILE": "Aktualizovať profil",
"requiredAction.VERIFY_EMAIL": "Overiť e-mail",
"linkExpirationFormatter.timePeriodUnit.seconds": "sekundy",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
"linkExpirationFormatter.timePeriodUnit.minutes": "minuty",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minúta",
"linkExpirationFormatter.timePeriodUnit.hours": "hodiny",
"linkExpirationFormatter.timePeriodUnit.hours.1": "hodina",
"linkExpirationFormatter.timePeriodUnit.days": "dni",
"linkExpirationFormatter.timePeriodUnit.days.1": "deň "
},
"sv": {
"emailVerificationSubject": "Verifiera e-post",
"emailVerificationBody": "Någon har skapat ett {2} konto med den här e-postadressen. Om det var du, klicka då på länken nedan för att verifiera din e-postadress\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm det inte var du som skapade det här kontot, ignorera i så fall det här meddelandet.",
"emailVerificationBodyHtml": "<p>Någon har skapat ett {2} konto med den här e-postadressen. Om det var du, klicka då på länken nedan för att verifiera din e-postadress</p><p><a href=\"{0}\">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om det inte var du som skapade det här kontot, ignorera i så fall det här meddelandet.</p>",
"identityProviderLinkSubject": "Länk {0}",
"identityProviderLinkBody": "Någon vill länka ditt \"{1}\" konto med \"{0}\" kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona\n\n{3}\n\nDen här länken kommer att upphöra inom {4} minuter.\n\nOm du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.",
"identityProviderLinkBodyHtml": "<p>Någon vill länka ditt <b>{1}</b> konto med <b>{0}</b> kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona</p><p><a href=\"{3}\">{3}</a></p><p>Den här länken kommer att upphöra inom {4} minuter.</p><p>Om du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.</p>",
"passwordResetSubject": "Återställ lösenord",
"passwordResetBody": "Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.",
"passwordResetBodyHtml": "<p>Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href=\"{0}\">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>",
"executeActionsSubject": "Uppdatera ditt konto",
"executeActionsBody": "Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.",
"executeActionsBodyHtml": "<p>Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.</p><p><a href=\"{0}\">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>",
"eventLoginErrorSubject": "Inloggningsfel",
"eventLoginErrorBody": "Ett misslyckat inloggningsförsök har upptäckts på ditt konto på {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
"eventLoginErrorBodyHtml": "<p>Ett misslyckat inloggningsförsök har upptäckts på ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
"eventRemoveTotpSubject": "Ta bort OTP",
"eventRemoveTotpBody": "OTP togs bort från ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
"eventRemoveTotpBodyHtml": "<p>OTP togs bort från ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
"eventUpdatePasswordSubject": "Uppdatera lösenord",
"eventUpdatePasswordBody": "Ditt lösenord ändrades den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
"eventUpdatePasswordBodyHtml": "<p>Ditt lösenord ändrades den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
"eventUpdateTotpSubject": "Uppdatera OTP",
"eventUpdateTotpBody": "OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
"eventUpdateTotpBodyHtml": "<p>OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>"
},
"tr": {
"emailVerificationSubject": "E-postayı doğrula",
"emailVerificationBody": "Birisi bu e-posta adresiyle bir {2} hesap oluşturdu. Bu sizseniz, e-posta adresinizi doğrulamak için aşağıdaki bağlantıya tıklayın\n\n{0}\n\nBu bağlantı {3} içinde sona erecek.\n\nBu hesabı oluşturmadıysanız, sadece bu iletiyi yoksayınız.",
"emailVerificationBodyHtml": "<p>Birisi bu e-posta adresiyle bir {2} hesap oluşturdu. Bu sizseniz, e-posta adresinizi doğrulamak için aşağıdaki bağlantıyı tıklayın.</p><p><a href=\"{0}\">E-posta adresi doğrulama adresi</a></p><p>Bu bağlantının süresi {3} içerisinde sona erecek.</p><p>Bu hesabı siz oluşturmadıysanız, bu mesajı göz ardı edin.</p>",
"emailTestSubject": "[KEYCLOAK] - SMTP test mesajı",
"emailTestBody": "Bu bir test mesajı",
"emailTestBodyHtml": "<p>Bu bir test mesajı</p>",
"identityProviderLinkSubject": "Link {0}",
"identityProviderLinkBody": "Birisi \"{1}\" hesabınızı \"{0}\" kullanıcı hesabı {2} ile bağlamak istiyor. Bu sizseniz, hesapları bağlamak için aşağıdaki bağlantıyı tıklayın:\n\n{3}\n\nBu bağlantı {5} içinde sona erecek.\n\nHesabınızı bağlamak istemiyorsanız bu mesajı göz ardı edin. Hesapları bağlarsanız, {1} ile {0} arasında oturum açabilirsiniz.",
"identityProviderLinkBodyHtml": "<p>Birisi <b> {1} </ b> hesabınızı {2} kullanıcısı <b> {0} </ b> hesabına bağlamak istiyor. Bu sizseniz, bağlantı vermek için aşağıdaki bağlantıyı tıklayın</p><p><a href=\"{3}\">Hesap bağlantısını onaylamak için bağlantı</a></p><p>Bu bağlantının süresi {5} içerisinde sona erecek.</p><p>Hesabı bağlamak istemiyorsanız, bu mesajı göz ardı edin. Hesapları bağlarsanız, {1} ile {0} arasında oturum açabilirsiniz.</p>",
"passwordResetSubject": "Şifreyi sıfırla",
"passwordResetBody": "Birisi, {2} hesabınızın kimlik bilgilerini değiştirmeyi istedi.Bu sizseniz, sıfırlamak için aşağıdaki bağlantıyı tıklayın.\n\n{0}\n\nBu bağlantı ve kod {3} içinde sona erecek.\n\nFakat bilgilerinizi sıfırlamak istemiyorsanız, Sadece bu mesajı görmezden gelin ve hiçbir şey değişmeyecek.",
"passwordResetBodyHtml": "<p>Birisi, {2} hesabınızın kimlik bilgilerini değiştirmeyi istedi. Sizseniz, sıfırlamak için aşağıdaki linke tıklayınız.</p><p><a href=\"{0}\">Kimlik bilgilerini sıfırlamak için bağlantı</a></p><p>Bu bağlantının süresi {3} içerisinde sona erecek.</p><p>Kimlik bilgilerinizi sıfırlamak istemiyorsanız, bu mesajı göz ardı edin.</p>",
"executeActionsSubject": "Hesabınızı Güncelleyin",
"executeActionsBody": "Yöneticiniz aşağıdaki işlemleri gerçekleştirerek {2} hesabınızı güncelledi: {3}. Bu işlemi başlatmak için aşağıdaki linke tıklayın.\n\n{0}\n\nBu bağlantının süresi {4} içerisinde sona erecek.\n\nYöneticinizin bunu istediğinden habersizseniz, bu mesajı göz ardı edin ve hiçbir şey değişmez.",
"executeActionsBodyHtml": "<p>Yöneticiniz aşağıdaki işlemleri gerçekleştirerek {2} hesabınızı güncelledi: {3}. Bu işlemi başlatmak için aşağıdaki linke tıklayın.</p><p><a href=\"{0}\">Hesap güncelleme bağlantısı</a></p><p>Bu bağlantının süresi {4} içerisinde sona erecek.</p><p>Yöneticinizin bunu istediğinden habersizseniz, bu mesajı göz ardı edin ve hiçbir şey değişmez.</p>",
"eventLoginErrorSubject": "Giriş hatası",
"eventLoginErrorBody": "{1} 'den {0} tarihinde başarısız bir giriş denemesi yapıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
"eventLoginErrorBodyHtml": "<p>{1} 'den {0} tarihinde başarısız bir giriş denemesi yapıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
"eventRemoveTotpSubject": "OTP'yi kaldır",
"eventRemoveTotpBody": "OTP, {0} tarihinden {1} tarihinde hesabınızdan kaldırıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
"eventRemoveTotpBodyHtml": "<p>OTP, {0} tarihinden {1} tarihinde hesabınızdan kaldırıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
"eventUpdatePasswordSubject": "Şifreyi güncelle",
"eventUpdatePasswordBody": "Şifreniz {0} tarihinde {0} tarihinde değiştirildi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
"eventUpdatePasswordBodyHtml": "<p>Şifreniz {0} tarihinde {0} tarihinde değiştirildi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
"eventUpdateTotpSubject": "OTP'yi Güncelle",
"eventUpdateTotpBody": "OTP, {0} tarihinden {1} tarihinde hesabınız için güncellendi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
"eventUpdateTotpBodyHtml": "<p>OTP, {0} tarihinden {1} tarihinde hesabınız için güncellendi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
"requiredAction.CONFIGURE_TOTP": "OTP'yi yapılandır",
"requiredAction.terms_and_conditions": "Şartlar ve Koşullar",
"requiredAction.UPDATE_PASSWORD": "Şifre Güncelleme",
"requiredAction.UPDATE_PROFILE": "Profilleri güncelle",
"requiredAction.VERIFY_EMAIL": "E-mail doğrula",
"linkExpirationFormatter.timePeriodUnit.seconds": "saniye",
"linkExpirationFormatter.timePeriodUnit.seconds.1": "saniye",
"linkExpirationFormatter.timePeriodUnit.minutes": "dakika",
"linkExpirationFormatter.timePeriodUnit.minutes.1": "dakika",
"linkExpirationFormatter.timePeriodUnit.hours": "saat",
"linkExpirationFormatter.timePeriodUnit.hours.1": "saat",
"linkExpirationFormatter.timePeriodUnit.days": "gün",
"linkExpirationFormatter.timePeriodUnit.days.1": "gün",
"emailVerificationBodyCode": "Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.</p><p><b>{0}</b></p>"
},
"zh-CN": {
"emailVerificationSubject": "验证电子邮件",
"emailVerificationBody": "用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您没有注册用户请忽略这条消息。",
"emailVerificationBodyHtml": "<p>用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证</p><p><a href=\"{0}\">{0}</a></p><p>这个链接会在 {1} 分钟后过期.</p><p>如果您没有注册用户,请忽略这条消息。</p>",
"identityProviderLinkSubject": "链接 {0}",
"identityProviderLinkBody": "有用户想要将账户 \"{1}\" 与用户{2}的账户\"{0}\" 做链接 . 如果是本人操作,请点击以下链接完成链接请求\n\n{3}\n\n这个链接会在 {4} 分钟后过期.\n\n如非本人操作请忽略这条消息。如果您链接账户您将可以通过{0}登录账户 {1}.",
"identityProviderLinkBodyHtml": "<p>有用户想要将账户 <b>{1}</b> 与用户{2} 的账户<b>{0}</b> 做链接 . 如果是本人操作,请点击以下链接完成链接请求</p><p><a href=\"{3}\">{3}</a></p><p>这个链接会在 {4} 分钟后过期。</p><p>如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.</p>",
"passwordResetSubject": "重置密码",
"passwordResetBody": "有用户要求修改账户 {2} 的密码.如是本人操作,请点击下面链接进行重置.\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您不想重置您的密码请忽略这条消息密码不会改变。",
"passwordResetBodyHtml": "<p>有用户要求修改账户 {2} 的密码如是本人操作,请点击下面链接进行重置.</p><p><a href=\"{0}\">{0}</a></p><p>这个链接会在 {1} 分钟后过期</p><p>如果您不想重置您的密码,请忽略这条消息,密码不会改变。</p>",
"executeActionsSubject": "更新您的账户",
"executeActionsBody": "您的管理员要求您更新账户 {2}. 点击以下链接开始更新\n\n{0}\n\n这个链接会在 {1} 分钟后失效.\n\n如果您不知道管理员要求更新账户信息请忽略这条消息。账户信息不会修改。",
"executeActionsBodyHtml": "<p>您的管理员要求您更新账户{2}. 点击以下链接开始更新.</p><p><a href=\"{0}\">{0}</a></p><p>这个链接会在 {1} 分钟后失效.</p><p>如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。</p>",
"eventLoginErrorSubject": "登录错误",
"eventLoginErrorBody": "在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.",
"eventLoginErrorBodyHtml": "<p>在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.</p>",
"eventRemoveTotpSubject": "删除 OTP",
"eventRemoveTotpBody": "OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员",
"eventRemoveTotpBodyHtml": "<p>OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员。</p>",
"eventUpdatePasswordSubject": "更新密码",
"eventUpdatePasswordBody": "您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员",
"eventUpdatePasswordBodyHtml": "<p>您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员</p>",
"eventUpdateTotpSubject": "更新 OTP",
"eventUpdateTotpBody": "您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。",
"eventUpdateTotpBodyHtml": "<p>您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>"
}
};
/* spell-checker: enable */

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +0,0 @@
import type { AvailableLanguages } from "./useKeycloakLanguage";
export function getLanguageLabel(language: AvailableLanguages): LanguageLabel {
switch (language) {
/* spell-checker: disable */
case "es": return "Español";
case "it": return "Italiano";
case "fr": return "Français";
case "ca": return "Català";
case "en": return "English";
case "de": return "Deutsch";
case "no": return "Norsk";
case "pt_BR": return "Português (Brasil)";
case "ru": return "Русский";
case "sk":
case "sv": return "Slovenčina";
case "ja": return "日本語";
case "pl": return "Polish";
case "zh_CN": return "中文简体"
case "sv": return "Svenska";
case "lt": return "Lietuvių";
case "cs": return "Čeština";
case "nl": return "Nederlands";
case "tr": return "tr"
/* spell-checker: enable */
}
return language;
}
export type LanguageLabel =
/* spell-checker: disable */
"Deutsch" | "Norsk" | "Русский" | "Svenska" | "Português (Brasil)" | "Lietuvių" |
"English" | "Italiano" | "Français" | "中文简体" | "Español" | "Čeština" | "日本語" |
"Slovenčina" | "Polish" | "Català" | "Nederlands" | "tr";
/* spell-checker: enable */

View File

@ -0,0 +1,33 @@
import { kcMessages } from "../generated_kcMessages/login";
import { Evt } from "evt";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
objectKeys(kcMessages).forEach(kcLanguage =>
Object.defineProperty(
kcMessages[kcLanguage],
key,
(() => {
let value = key === "termsText" ? "⏳" : kcMessages[kcLanguage][key];
return {
"enumerable": true,
"get": () => value,
"set": (newValue: string) => {
value = newValue;
Evt.asPostable(evtTermsUpdated).post();
}
};
})()
)
)
);
export { kcMessages };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
import { createUseGlobalState } from "powerhooks";
import { kcContext } from "../KcContext";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
const wrap = createUseGlobalState(
"kcLanguageTag",
() => getBestMatchAmongKcLanguageTag(
kcContext?.locale?.current ??
navigator.language
),
{ "persistance": "cookie" }
);
export const { useKcLanguageTag } = wrap;
export function getEvtKcLanguage() {
return wrap.evtKcLanguageTag;
}

View File

@ -0,0 +1,51 @@
import { useCallback, useReducer } from "react";
import { useKcLanguageTag } from "./useKcLanguageTag";
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
import type { ReactNode } from "react";
import { useEvt } from "evt/hooks";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import ReactMarkdown from "react-markdown";
export type MessageKey = keyof typeof kcMessages["en"];
export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag();
const [trigger, forceUpdate] = useReducer((counter: number) => counter + 1, 0);
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
const msgStr = useCallback(
(key: MessageKey, ...args: (string | undefined)[]): string => {
let str: string = kcMessages[kcLanguageTag as any as "en"][key] ?? kcMessages["en"][key];
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
});
return str;
},
[kcLanguageTag, trigger]
);
const msg = useCallback<(...args: Parameters<typeof msgStr>) => ReactNode>(
(key, ...args) =>
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
{msgStr(key, ...args)}
</ReactMarkdown>,
[msgStr]
);
return { msg, msgStr };
}

View File

@ -1,40 +0,0 @@
import { createUseGlobalState } from "powerhooks";
import { messages } from "./messages.generated";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
import { getLanguageLabel } from "./getLanguageLabel";
const availableLanguages = objectKeys(messages["login"]);
export type AvailableLanguages = typeof availableLanguages[number];
export const { useKeycloakLanguage } = createUseGlobalState(
"keycloakLanguage",
getKeycloakAvailableLanguageBestGuess,
{ "persistance": "cookies" }
);
/**
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
* it corresponds to.
* If there is no reasonable match it's guessed from navigator.language.
* If still no matches en is returned.
*/
export function getKeycloakAvailableLanguageBestGuess(
languageLike: string = navigator.language
): AvailableLanguages {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
const language = availableLanguages.find(language =>
language.toLowerCase().includes(iso2LanguageLike) ||
getLanguageLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase()
);
if (language === undefined && languageLike !== navigator.language) {
return getKeycloakAvailableLanguageBestGuess(navigator.language);
}
return "en";
}

View File

@ -1,28 +0,0 @@
import { useKeycloakLanguage } from "./useKeycloakLanguage";
import { messages } from "./messages.generated";
import { useConstCallback } from "powerhooks";
export type MessageKey = keyof typeof messages["login"]["en"]
export function useKeycloakThemeTranslation() {
const { keycloakLanguage } = useKeycloakLanguage();
const t = useConstCallback(
(key: MessageKey): string => {
const out: string | undefined = messages["login"][keycloakLanguage as any as "en"][key];
if (out !== undefined) {
return out;
}
return messages["login"]["en"][key];
}
);
return { t };
}

View File

@ -1,4 +1,21 @@
export * from "./keycloakFtlValues";
export * from "./i18n/useKeycloakLanguage";
export * from "./i18n/useKeycloakTranslation";
export * from "./i18n/getLanguageLabel";
export * from "./KcContext";
export * from "./i18n/KcLanguageTag";
export * from "./i18n/useKcLanguageTag";
export * from "./i18n/useKcMessage";
export * from "./i18n/kcMessages/login";
export * from "./components/KcProps";
export * from "./components/Login";
export * from "./components/Template";
export * from "./components/KcApp";
export * from "./components/Info";
export * from "./components/Error";
export * from "./components/LoginResetPassword";
export * from "./components/LoginVerifyEmail";
export * from "./keycloakJsAdapter";
export * from "./tools/assert";
export * as kcContextMocks from "./kcContextMocks";

182
src/lib/kcContext.ts Normal file
View File

@ -0,0 +1,182 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { PageId } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id";
import type { KcLanguageTag } from "./i18n/KcLanguageTag";
import { doExtends } from "evt/tools/typeSafety/doExtends";
import type { MessageKey } from "./i18n/useKcMessage";
import type { LanguageLabel } from "./i18n/KcLanguageTag";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
StrEnum extends `${Prefix}${infer U}` ? U : never;
/** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages.
* (ex: url.loginAction is undefined on error.ftl)
*/
export type KcContext =
KcContext.Login | KcContext.Register | KcContext.Info |
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail |
KcContext.Terms | KcContext.LoginOtp;
export declare namespace KcContext {
export type Common = {
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
loginUrl: string;
};
realm: {
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
registrationEmailAsUsername: boolean;
};
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
url: string;
languageTag: KcLanguageTag;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
//label: LanguageLabel;
}[];
current: LanguageLabel;
},
auth?: {
showUsername: boolean;
showResetCredentials: boolean;
showTryAnotherWayLink: boolean;
attemptedUsername?: string;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
isAppInitiatedAction: boolean;
};
export type Login = Common & {
pageId: "login.ftl";
url: {
loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: {
loginWithEmailAllowed: boolean;
rememberMe: boolean;
password: boolean;
resetPasswordAllowed: boolean;
registrationAllowed: boolean;
};
auth: {
selectedCredential?: string;
};
registrationDisabled: boolean;
login: {
username?: string;
rememberMe: boolean;
};
usernameEditDisabled: boolean;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[]
};
};
export type Register = Common & {
pageId: "register.ftl";
url: {
registrationAction: string;
};
messagesPerField: {
printIfExists<T>(
key:
"userLabel" |
"username" |
"email" |
"firstName" |
"lastName" |
"password" |
"password-confirm",
x: T
): T | undefined;
};
register: {
formData: {
firstName?: string;
displayName?: string;
lastName?: string;
email?: string;
username?: string;
}
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
/**
* Defined when you use the keycloak-mail-whitelisting keycloak plugin
* (https://github.com/micedre/keycloak-mail-whitelisting)
*/
authorizedMailDomains?: string[];
};
export type Info = Common & {
pageId: "info.ftl";
messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[];
skipLink: boolean;
pageRedirectUri?: string;
actionUri?: string;
client: {
baseUrl?: string;
}
};
export type Error = Common & {
pageId: "error.ftl";
client?: {
baseUrl?: string;
}
};
export type LoginResetPassword = Common & {
pageId: "login-reset-password.ftl";
realm: {
loginWithEmailAllowed: boolean;
}
};
export type LoginVerifyEmail = Common & {
pageId: "login-verify-email.ftl";
};
export type Terms = Common & {
pageId: "terms.ftl";
};
export type LoginOtp = Common & {
pageId: "login-otp.ftl";
otpLogin: {
userOtpCredentials: { id: string; userLabel: string; }[];
}
};
}
doExtends<KcContext["pageId"], PageId>();
doExtends<PageId, KcContext["pageId"]>();
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);

View File

@ -0,0 +1,230 @@
import type { KcContext } from "../KcContext";
import { getEvtKcLanguage } from "../i18n/useKcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
const kcCommonContext: KcContext.Common = {
"url": {
"loginAction": "#",
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
},
"realm": {
"displayName": "myrealm",
"displayNameHtml": "myrealm",
"internationalizationEnabled": true,
"registrationEmailAsUsername": true,
},
"locale": {
"supported": [
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"languageTag": "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"languageTag": "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"languageTag": "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"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",
"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",
"languageTag": "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"languageTag": "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"languageTag": "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"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",
"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",
"languageTag": "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"languageTag": "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"languageTag": "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"languageTag": "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"languageTag": "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"languageTag": "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"languageTag": "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"languageTag": "tr"
}
],
"current": null as any
},
"auth": {
"showUsername": false,
"showResetCredentials": false,
"showTryAnotherWayLink": false
},
"scripts": [],
"message": {
"type": "success",
"summary": "This is a test message"
},
"isAppInitiatedAction": false,
};
Object.defineProperty(
kcCommonContext.locale!,
"current",
{
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
"enumerable": true
}
);
export const kcLoginContext: KcContext.Login = {
...kcCommonContext,
"pageId": "login.ftl",
"url": {
...kcCommonContext.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
},
"realm": {
...kcCommonContext.realm,
"loginWithEmailAllowed": true,
"rememberMe": true,
"password": true,
"resetPasswordAllowed": true,
"registrationAllowed": true
},
"auth": kcCommonContext.auth!,
"social": {
"displayInfo": true
},
"usernameEditDisabled": false,
"login": {
"rememberMe": false
},
"registrationDisabled": false,
};
export const kcRegisterContext: KcContext.Register = {
...kcCommonContext,
"url": {
...kcLoginContext.url,
"registrationAction": "http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
},
"messagesPerField": {
"printIfExists": (...[, x]) => x
},
"scripts": [],
"isAppInitiatedAction": false,
"pageId": "register.ftl",
"register": {
"formData": {}
},
"passwordRequired": true,
"recaptchaRequired": false,
"authorizedMailDomains": [
"example.com",
"another-example.com",
"*.yet-another-example.com",
"*.example.com",
"hello-world.com"
]
};
export const kcInfoContext: KcContext.Info = {
...kcCommonContext,
"pageId": "info.ftl",
"messageHeader": "<Message header>",
"requiredActions": undefined,
"skipLink": false,
"actionUri": "#",
"client": {
"baseUrl": "#"
}
};
export const kcErrorContext: KcContext.Error = {
...kcCommonContext,
"pageId": "error.ftl",
"client": {
"baseUrl": "#"
}
};
export const kcLoginResetPasswordContext: KcContext.LoginResetPassword = {
...kcCommonContext,
"pageId": "login-reset-password.ftl",
"realm": {
...kcCommonContext.realm,
"loginWithEmailAllowed": false
}
};
export const kcLoginVerifyEmailContext: KcContext.LoginVerifyEmail = {
...kcCommonContext,
"pageId": "login-verify-email.ftl"
};
export const kcTermsContext: KcContext.Terms = {
...kcCommonContext,
"pageId": "terms.ftl"
};
export const kcLoginOtpContext: KcContext.LoginOtp = {
...kcCommonContext,
"pageId": "login-otp.ftl",
"otpLogin": {
"userOtpCredentials": [
{
"id": "id1",
"userLabel": "label1"
},
{
"id": "id2",
"userLabel": "label2"
}
]
}
};

View File

@ -0,0 +1,6 @@
import { join as pathJoin } from "path";
export const subDirOfPublicDirBasename = "keycloak_static";
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "/resources");
export const resourcesCommonPath = pathJoin(subDirOfPublicDirBasename, "/resources_common");

View File

@ -1,51 +0,0 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id";
export type KeycloakFtlValues = {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
},
realm: {
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
},
//NOTE: Undefined if !realm.internationalizationEnabled
//We hide this since we provide a client side internationalization engine
/*
locale?: {
supported: {
url: string;
languageTag: AvailableLanguages;
//NOTE: Is determined by languageTag. Ex: languageTag === "en" => label === "English"
label: LanguageLabel;
},
current: LanguageLabel;
},
*/
auth?: {
showUsername: boolean;
showResetCredentials: boolean;
showTryAnotherWayLink: boolean;
attemptedUsername?: boolean;
},
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
},
isAppInitiatedAction: boolean;
};
export const { keycloakPagesContext } =
{ [ftlValuesGlobalName]: id<KeycloakFtlValues | undefined>((window as any)[ftlValuesGlobalName]) };
;

View File

@ -0,0 +1,118 @@
export declare namespace keycloak_js {
export type KeycloakPromiseCallback<T> = (result: T) => void;
export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> {
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
}
export interface KeycloakAdapter {
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>;
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
accountManagement(): KeycloakPromise<void, void>;
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
}
export interface KeycloakLogoutOptions {
redirectUri?: string;
}
export interface KeycloakLoginOptions {
scope?: string;
redirectUri?: string;
prompt?: 'none' | 'login';
action?: string;
maxAge?: number;
loginHint?: string;
idpHint?: string;
locale?: string;
cordovaOptions?: { [optionName: string]: string };
}
export type KeycloakInstance = Record<
"createLoginUrl" |
"createLogoutUrl" |
"createRegisterUrl",
(options: KeycloakLoginOptions | undefined) => string
> & {
createAccountUrl(): string;
redirectUri?: string;
}
}
/**
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
* The goal here is just to be able to inject search param in url before keycloak redirect.
* Our use case for it is to pass over the login screen the states of useGlobalState
* namely isDarkModeEnabled, lgn...
*/
export function createKeycloakAdapter(
params: {
keycloakInstance: keycloak_js.KeycloakInstance;
transformUrlBeforeRedirect(url: string): string;
}
): keycloak_js.KeycloakAdapter {
const { keycloakInstance, transformUrlBeforeRedirect } = params;
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties(
new Promise(() => { }),
{
"success": { "value": () => { } },
"error": { "value": () => { } }
}
);
return {
"login": options => {
window.location.href=
transformUrlBeforeRedirect(
keycloakInstance.createLoginUrl(
options
)
);
return neverResolvingPromise;
},
"logout": options => {
window.location.replace(
transformUrlBeforeRedirect(
keycloakInstance.createLogoutUrl(
options
)
)
);
return neverResolvingPromise;
},
"register": options => {
window.location.href =
transformUrlBeforeRedirect(
keycloakInstance.createRegisterUrl(
options
)
);
return neverResolvingPromise;
},
"accountManagement": () => {
var accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
if (typeof accountUrl !== 'undefined') {
window.location.href = accountUrl;
} else {
throw new Error("Not supported by the OIDC server");
}
return neverResolvingPromise;
},
"redirectUri": options => {
if (options && options.redirectUri) {
return options.redirectUri;
} else if (keycloakInstance.redirectUri) {
return keycloakInstance.redirectUri;
} else {
return window.location.href;
}
}
};
}

View File

@ -0,0 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
export function allPropertiesValuesToUndefined<T extends Record<string, unknown>>(obj: T): Record<keyof T, undefined> {
return Object.fromEntries(
Object.entries(obj)
.map(([key]) => [key, undefined])
) as any;
}

View File

@ -0,0 +1,49 @@
import { Deferred } from "evt/tools/Deferred";
export function appendHead(
params: {
type: "css";
href: string;
} | {
type: "javascript";
src: string;
}
) {
const htmlElement = document.createElement(
(() => {
switch (params.type) {
case "css": return "link";
case "javascript": return "script";
}
})()
);
const dLoaded = new Deferred<void>();
htmlElement.addEventListener("load", () => dLoaded.resolve());
Object.assign(
htmlElement,
(() => {
switch (params.type) {
case "css": return {
"href": params.href,
"type": "text/css",
"rel": "stylesheet",
"media": "screen,print"
};
case "javascript": return {
"src": params.src,
"type": "text/javascript",
};
}
})()
);
document.getElementsByTagName("head")[0].appendChild(htmlElement);
return dLoaded.pr;
}

2
src/lib/tools/assert.ts Normal file
View File

@ -0,0 +1,2 @@
export { assert } from "evt/tools/typeSafety/assert";

View File

@ -1,24 +0,0 @@
import {
setupSampleReactProject,
sampleReactProjectDirPath
} from "./setupSampleReactProject";
import * as st from "scripting-tools";
import { join as pathJoin } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
setupSampleReactProject();
console.log(`Running main in ${sampleReactProjectDirPath}`);
console.log(
st.execSync(
`node ${pathJoin(getProjectRoot(), "dist", "bin", "build-keycloak-theme")}`,
{ "cwd": sampleReactProjectDirPath }
)
);

View File

@ -1,14 +0,0 @@
import { sampleReactProjectDirPath } from "./setupSampleReactProject";
import * as st from "scripting-tools";
import { join as pathJoin } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
console.log(`Running main in ${sampleReactProjectDirPath}`);
st.execSync(
`node ${pathJoin(getProjectRoot(), "dist","bin","download-sample-keycloak-themes")}`,
{ "cwd": sampleReactProjectDirPath }
);

View File

@ -1,7 +1,7 @@
import { join as pathJoin } from "path";
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
import {
import {
setupSampleReactProject,
sampleReactProjectDirPath
} from "./setupSampleReactProject";
@ -9,8 +9,10 @@ import {
setupSampleReactProject();
generateKeycloakThemeResources({
"themeName": "onyxia-ui",
"themeName": "keycloakify-demo-app",
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme")
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
"urlPathname": "/keycloakify-demo-app/",
"urlOrigin": undefined
});

24
src/test/index.ts Normal file
View File

@ -0,0 +1,24 @@
import {
setupSampleReactProject,
sampleReactProjectDirPath
} from "./setupSampleReactProject";
import * as st from "scripting-tools";
import { join as pathJoin } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
setupSampleReactProject();
const binDirPath= pathJoin(getProjectRoot(), "dist", "bin");
st.execSyncTrace(
//`node ${pathJoin(binDirPath, "build-keycloak-theme")} --external-assets`,
`node ${pathJoin(binDirPath, "build-keycloak-theme")}`,
{ "cwd": sampleReactProjectDirPath }
);
st.execSyncTrace(
`node ${pathJoin(binDirPath, "install-builtin-keycloak-themes")}`,
{ "cwd": sampleReactProjectDirPath }
);

View File

@ -1,30 +1,47 @@
import { 
replaceImportFromStaticInJsCode,
replaceImportFromStaticInCssCode,
replaceImportsFromStaticInJsCode,
replaceImportsInCssCode,
generateCssCodeToDefineGlobals
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
const { fixedJsCode } = replaceImportFromStaticInJsCode({
"ftlValuesGlobalName": "keycloakFtlValues",
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": `
function f() {
return a.p + "static/js/" + ({}[e] || e) + "." + {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function f2() {
return a.p +"static/js/" + ({}[e] || e) + "." + {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
`
`,
"urlOrigin": undefined
});
console.log({ fixedJsCode });
const { fixedJsCode: fixedJsCodeExternal } = replaceImportsFromStaticInJsCode({
"jsCode": `
function f() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({
function f2() {
return a.p+"static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
`,
"urlOrigin": "https://www.example.com"
});
console.log({ fixedJsCode, fixedJsCodeExternal });
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
"cssCode": `
.my-div {
@ -45,6 +62,6 @@ const { fixedCssCode, cssGlobalsToDefine } = replaceImportFromStaticInCssCode({
console.log({ fixedCssCode, cssGlobalsToDefine });
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ cssGlobalsToDefine });
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ cssGlobalsToDefine, "urlPathname": "/" });
console.log({ cssCodeToPrependInHead });

View File

@ -1,20 +1,14 @@
import { getProjectRoot } from "../bin/tools/getProjectRoot";
import * as st from "scripting-tools";
import { join as pathJoin, basename as pathBasename } from "path";
import { getProjectRoot } from "../bin/tools/getProjectRoot";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "../bin/tools/downloadAndUnzip";
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");
export function setupSampleReactProject() {
st.execSync(`rm -rf ${sampleReactProjectDirPath}`);
st.execSync(`mkdir ${sampleReactProjectDirPath}`);
const url = "https://github.com/garronej/keycloak-react-theming/releases/download/v0.0.1/sample_build_dir_and_package_json.zip";
[
`wget ${url}`,
...["unzip", "rm"].map(prg => `${prg} ${pathBasename(url)}`)
].forEach(cmd => st.execSync(cmd, { "cwd": sampleReactProjectDirPath }));
}
downloadAndUnzip({
"url": "https://github.com/garronej/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": sampleReactProjectDirPath
});
}

View File

@ -2,7 +2,7 @@
"compilerOptions": {
"module": "CommonJS",
"target": "es5",
"lib": ["es2015", "DOM"],
"lib": ["es2015", "DOM", "ES2019.Object"],
"esModuleInterop": true,
"declaration": true,
"outDir": "./dist",

927
yarn.lock

File diff suppressed because it is too large Load Diff