Compare commits

...

1531 Commits
v8.4.5 ... main

Author SHA1 Message Date
d24a8e99cc Fix badge 2025-04-09 16:33:19 +02:00
59d2d56091 Fix badge 2025-04-09 16:30:27 +02:00
8515b7060a Merge pull request #833 from keycloakify/all-contributors/add-wnmzzzz
docs: add wnmzzzz as a contributor for test
2025-04-09 16:29:50 +02:00
a076b3f4d0 docs: update .all-contributorsrc [skip ci] 2025-04-09 14:29:32 +00:00
1e3240ef35 docs: update README.md [skip ci] 2025-04-09 14:29:31 +00:00
d767080dfe Merge pull request #831 from wnmzzzz/patch-1
Add Story for AppInitiatedAction to UpdatePassword
2025-04-09 16:29:02 +02:00
b228eda488 Add Story for AppInitiatedAction to UpdatePassword 2025-04-07 08:42:46 +02:00
35f54964ce Bump version 2025-04-04 23:57:42 +02:00
759b834ccc Follow up on #827 2025-04-04 23:57:13 +02:00
137e12cbbb Merge pull request #830 from keycloakify/all-contributors/add-kodebach
docs: add kodebach as a contributor for code
2025-04-04 23:57:08 +02:00
57b08d9dea docs: update .all-contributorsrc [skip ci] 2025-04-04 21:56:54 +00:00
1dd7b673a1 docs: update README.md [skip ci] 2025-04-04 21:56:53 +00:00
b00ffc50c3 Merge pull request #827 from kodebach/fix-keycloak-36012
Fix double submit bug in OTP Form
2025-04-04 23:34:01 +02:00
f9db40d33d fix: https://github.com/keycloak/keycloak/issues/36012
adapted from https://github.com/keycloak/keycloak/pull/36096
2025-04-02 12:08:37 +00:00
4bc6a843d8 Merge pull request #820 from kingjan1999/fix-required-actions-whitespace
fix: add whitespace before required actions in info page
2025-03-21 13:21:02 +01:00
da3e7514f0 fix: add whitespace before required actions in info page 2025-03-21 11:37:08 +01:00
bc396bc41b Update runPrettier so that it will still work if we ever switch to ESM 2025-03-16 02:17:11 +01:00
947efe8d63 Bump version 2025-03-16 00:49:08 +01:00
64189bf8fe #815 2025-03-16 00:48:50 +01:00
400c630418 Bump version 2025-03-13 22:03:16 +01:00
402360b436 #814 https://github.com/keycloak/keycloak/issues/38029 2025-03-13 22:03:02 +01:00
9f001f1521 Bump version 2025-03-13 13:32:23 +01:00
368e3a32c5 #772 2025-03-13 13:32:08 +01:00
002e3d4b3d Bump version 2025-03-12 19:22:58 +01:00
f94f9b51c9 Fix Vitest VSCode extention 2025-03-12 19:22:32 +01:00
055b15bd46 Bump version 2025-03-12 00:53:05 +01:00
0e70b0b0de https://github.com/keycloak/keycloak/issues/38029 2025-03-12 00:52:50 +01:00
8faf9a3eed Bump version 2025-02-27 12:21:32 +01:00
075d9f9de5 #802 2025-02-27 12:21:32 +01:00
840079be32 Merge pull request #797 from keycloakify/all-contributors/add-bacongobbler
docs: add bacongobbler as a contributor for doc
2025-02-24 19:43:18 +01:00
50ae962f09 docs: update .all-contributorsrc [skip ci] 2025-02-24 18:43:07 +00:00
61aa1f9896 docs: update README.md [skip ci] 2025-02-24 18:43:06 +00:00
d88e0e4dd5 Bump version 2025-02-24 18:46:06 +01:00
18c36eb4de Help pepole debug when mvn build fails 2025-02-24 18:06:28 +01:00
80aeabad51 Bump version 2025-02-17 12:59:49 +01:00
419e1f473a Merge pull request #791 from keycloakify/all-contributors/add-EternalSide
docs: add EternalSide as a contributor for code
2025-02-17 12:59:19 +01:00
80988125e8 Merge pull request #789 from EternalSide/fix/add-passwordPolicy-maxLength
fix: add maxLength passwordPolicy in the getUserProfileApi
2025-02-17 12:59:08 +01:00
271ad2da71 docs: update .all-contributorsrc [skip ci] 2025-02-17 11:56:31 +00:00
b2732f2595 docs: update README.md [skip ci] 2025-02-17 11:56:30 +00:00
53820e1e34 fix: add maxLength passwordPolicy in the getUserProfileApi 2025-02-17 13:21:55 +03:00
09dd45e437 Bump version 2025-02-11 15:56:03 +01:00
1f654a7820 #785 2025-02-11 15:55:47 +01:00
0690f40bad Bump version 2025-02-02 18:26:31 +01:00
2285883149 Fix typo #778 2025-02-02 18:26:08 +01:00
af87e41bb8 Bump version 2025-01-25 18:31:05 +01:00
9ba884483d keycloakify-email isn't strictly bound to jsx-email 2025-01-25 18:30:52 +01:00
f5a300953a Bump version 2025-01-24 21:27:03 +01:00
ab9a962f58 #771 2025-01-24 21:26:43 +01:00
484adb607f Bump version 2025-01-23 16:41:49 +01:00
e1f38d4196 Fix 767 2025-01-23 16:41:02 +01:00
5de629acf2 Merge pull request #767 from mislavperi/main
Expanded podman support
2025-01-23 15:36:21 +00:00
8b4b24a036 consisteny 2025-01-23 14:42:53 +01:00
75ab130249 refactor to shorten the code 2025-01-23 14:42:03 +01:00
981ca7e9a4 expanded podman support 2025-01-23 10:55:51 +01:00
acb4e260a7 Bump version 2025-01-11 16:08:38 +01:00
ff20b0a844 Avoid re-building when shared files from SPAs are changed 2025-01-11 16:08:17 +01:00
1b77c69a01 Bump version 2025-01-10 19:07:35 +01:00
158275f5c2 Workaround bug with the versions subcomand of older npm versions 2025-01-10 19:07:11 +01:00
a085c8093e Bump version 2025-01-08 00:07:34 +01:00
cb358bd745 #754: PasswordWrapper fix for React 19 2025-01-08 00:06:45 +01:00
e788c46601 Bump version 2025-01-07 20:51:49 +01:00
d551b4bffb Add missing Patternfly fonts 2025-01-07 20:51:27 +01:00
c168c7b156 Bump version 2025-01-06 02:47:42 +01:00
7a46115042 Enable persisting email change on test user 2025-01-06 02:47:28 +01:00
249a7bde89 Fix adding comment to certain ftl files 2025-01-06 02:31:46 +01:00
813740a002 Bump version 2025-01-05 21:34:34 +01:00
7840c2a6f5 Fix previous release 2025-01-05 21:34:16 +01:00
8f6c0d36d9 Bump version 2025-01-05 21:09:24 +01:00
12690b892b Fix replacing of xKeycloakify.themeName in theme variant 2025-01-05 21:09:06 +01:00
d01b4b71c9 Bump version 2025-01-05 21:00:53 +01:00
c29e600786 Fix generateResource bug 2025-01-05 21:00:34 +01:00
6309b7c45d Bump version 2025-01-05 04:27:11 +01:00
7e7996e40c Fix auto enable themes when using start-keycloak 2025-01-05 04:26:45 +01:00
deaeab0f61 Infer META-INF/keycloak-themes.jar 2025-01-05 03:14:34 +01:00
6bd5451230 Bump version 2025-01-05 02:09:33 +01:00
fb2d651a6f Rely on @keycloakify/email-native for email initialization 2025-01-05 02:08:18 +01:00
4845d7c32d Support incorporating theme native theme, with theme variant support #733 2025-01-05 02:08:13 +01:00
c33c315120 Bump version 2025-01-03 22:58:12 +01:00
99b8f1e789 Update i18n account multi-page boilerplate 2025-01-03 22:57:57 +01:00
6af13e1405 Bump version 2025-01-03 22:39:01 +01:00
f59fa4238c Link to the documentation for implementing non builtin pages 2025-01-03 22:38:45 +01:00
248effc57d Bump version 2025-01-03 02:47:31 +01:00
9e540b2c1f Fix assets import from public in .svelte files 2025-01-03 02:47:31 +01:00
ab7b5ff490 Remove ringerhq 2025-01-03 01:06:29 +01:00
486f944e0f Bump version 2025-01-02 10:25:16 +01:00
6cc3d4c442 Fix conflict in --path shorthand with --project, --path shorthand is now -t (target) 2025-01-02 10:25:16 +01:00
083290c6d4 update broken link 2024-12-30 02:04:09 +01:00
cd1b55b850 Fix test 2024-12-27 01:49:38 +01:00
482ba6c639 Bump version 2024-12-27 01:41:29 +01:00
e2921b7e37 Fix auto add of postinstall script 2024-12-27 01:38:11 +01:00
c87b6153bb Fix some errors implementing the new account SPA feature 2024-12-27 01:36:29 +01:00
488dd2c6b9 Migrate to extention model for Account SPA 2024-12-26 15:55:59 +01:00
1ac678a368 Remove dead code 2024-12-26 15:24:12 +01:00
5866c802e5 Always initialize email with the latest keycloak version 2024-12-26 15:24:03 +01:00
fe892c840b Bump version 2024-12-24 17:40:31 +01:00
9685dfb55a Fix git integration bug 2024-12-24 17:39:54 +01:00
c1dc899bc1 Better naming convention 'uiModules' -> 'extensionModules' 2024-12-24 16:43:42 +01:00
d2da43c617 Implement --revert for own command 2024-12-24 01:10:32 +01:00
6de5fd4f96 Optimization and potentially prevend leaving the repo in a broken state 2024-12-23 18:49:00 +01:00
cc3d0d61dd Consistent naming scheme 'eject' -> 'own' 2024-12-23 18:34:42 +01:00
4403f00274 Reneame the 'eject-file' command to 'own' 2024-12-23 17:55:40 +01:00
eddfb8e634 Bump version 2024-12-22 21:58:07 +01:00
4f2790f6d3 Fixes on the admin initialization cmd 2024-12-22 21:57:52 +01:00
96690e1354 Generate the postinstall script as the first entry of the package.json 2024-12-22 21:25:51 +01:00
982f216a01 Do not take into account react in the resolution of ui modules peer dependencies for supporting React 19 2024-12-22 21:21:20 +01:00
13c21e8910 Implement initialize-admin-theme command 2024-12-22 17:09:15 +01:00
94b7d2b85b Bump version 2024-12-21 19:40:07 +01:00
9a4f89e69d Sort out version of keycloak not supported depending on which theme is implemented (start-keycloak cmd) 2024-12-21 19:39:10 +01:00
a5ba03cca0 Reload when .properties files are updated 2024-12-21 19:02:13 +01:00
5203813e7b Rebuild the theme when properties files changes 2024-12-21 15:22:19 +01:00
0e461fd072 Fix bugs in svg assets commenting for mirror files 2024-12-21 14:25:47 +01:00
326411ca5d Correctly generate i18n messages for admin UI 2024-12-21 12:09:29 +01:00
c39c450e90 Support generating eject comments for .svg files 2024-12-20 13:22:15 +01:00
3191954dda Support generating eject comment for .properties file 2024-12-20 13:03:58 +01:00
20c6d2ea86 Bump version 2024-12-19 19:07:24 +01:00
f43544e134 ensure no diff if config hasn't changed 2024-12-19 19:06:54 +01:00
474a863708 Correctly patch the security-admin-console client so that it can be run with HMR 2024-12-18 20:57:42 +01:00
0bacdca8fe Bump version 2024-12-17 18:04:22 +01:00
f023d6bca7 Fixes windows issues #747 2024-12-17 18:04:06 +01:00
150b01f1f3 Bump version 2024-12-17 10:44:38 +01:00
2b2bb20658 #746 2024-12-17 10:44:24 +01:00
70570faed6 Bump version 2024-12-16 18:04:01 +01:00
5d3b7c9a82 Add necessary token claim to access admin in dev mode 2024-12-16 18:04:01 +01:00
95b9b12a3b Try to fix error on windows 2024-12-16 18:04:01 +01:00
0e027055cb Bump version 2024-12-15 19:48:42 +01:00
e47b002535 #744 2024-12-15 19:48:23 +01:00
8dd6dcd1fc Merge pull request #745 from keycloakify/keycloak_config_persistance
Keycloak config persistance
2024-12-15 19:45:52 +01:00
10cfa1cf41 Update default realm configs 2024-12-15 19:45:05 +01:00
3938584aeb Update default realm configs 2024-12-15 18:43:53 +01:00
163b060dc5 Additional teaks 2024-12-15 18:15:36 +01:00
67f8ae41fc Update prepare realm script 2024-12-15 17:42:45 +01:00
b6e9fe2585 Update default realm config for kc 26 2024-12-15 13:28:05 +01:00
5b83bd8fa9 Update dump realm local script 2024-12-15 13:27:49 +01:00
d0f43b6318 Add logging and debug for backup configuration process 2024-12-15 13:11:01 +01:00
df338ed6a0 Improve ordering to minimize diff 2024-12-15 12:28:09 +01:00
295994d02a Use KC_BOOTSTRAP_ADMIN_ in newer keycloak 2024-12-15 11:57:45 +01:00
f9e15f93c4 Fix spelling mistake 2024-12-15 11:49:33 +01:00
2659cf391c Fix schema validation error 2024-12-15 11:47:59 +01:00
76416ddd5b Put persisted realm configs in .keycloakify 2024-12-15 11:45:00 +01:00
8e8a0ccf54 Store https://my-theme.keycloakify.dev as a constant 2024-12-15 11:38:50 +01:00
db0ec954df Fix zod schema error 2024-12-15 11:34:41 +01:00
dc942aa5de Implement cache for fetching available docker images tags 2024-12-15 08:53:54 +01:00
029cfcb591 Fix fetching of keycloak versions 2024-12-14 18:37:54 +01:00
b1b6919395 Assuming latest supported 2024-12-14 14:44:30 +01:00
9185740d35 Keycloak config persistance implemented (to test) 2024-12-14 14:36:11 +01:00
8d59fe7b67 Change structure 2024-12-13 12:16:41 +01:00
92b505dd56 Load custom extention for logging realm change 2024-12-13 12:07:21 +01:00
c0e6661d3d Add function to dump the realm config 2024-12-13 11:31:01 +01:00
0cae2c68d8 Add utils to edit the realm 2024-12-13 09:07:11 +01:00
1e43343529 Update keycloak 26 realm default config (fmt) 2024-12-12 11:19:06 +01:00
0a74dca7c2 Prettier ignore realm default config 2024-12-12 11:16:01 +01:00
a66a373256 Update dump-keycloak-realm internal script https://github.com/keycloak/keycloak/issues/33800 2024-12-10 04:12:56 +01:00
606cf7ad02 Bump version 2024-12-09 05:08:57 +01:00
5225749c7b React 19 compat #741 2024-12-09 05:06:47 +01:00
819e3833ad Bump version 2024-12-08 19:43:00 +01:00
b0ba37fcc4 Smarter appBuild script 2024-12-08 19:42:43 +01:00
f4829b557f Bump version 2024-12-06 00:38:23 +01:00
60a9b5a693 Improve i18n api typing 2024-12-06 00:38:09 +01:00
c323b94a8c Bump version 2024-12-04 00:09:14 +01:00
4bbc0241ec Do not crash when parser can't be inferred 2024-12-04 00:04:49 +01:00
5a7dacfcdd Bump version 2024-12-02 00:41:30 +01:00
7e05e1bf0c Use random port for dev server 2024-12-02 00:41:12 +01:00
1530ca32c8 Bump version 2024-12-01 00:07:28 +01:00
ed054f131a Merge pull request #736 from keycloakify/hmr_in_start_keycloak
Implement hot module replacement for developing Account SPA and Admin UI
2024-12-01 00:01:57 +01:00
ec74ceef4d Implement hot module replacement for developing Account SPA and Admin UI 2024-11-30 23:55:24 +01:00
fd3261cdf1 Bump version 2024-11-25 11:41:36 +01:00
b4b53d2552 Re export wide type def of the kcContext 2024-11-25 11:41:17 +01:00
0371d9ea7a Bump version 2024-11-23 08:49:17 +01:00
73031e74ec Make it so it's not required to manually call the copy-keycloak-resources-to-public script even in webpack projects 2024-11-23 08:48:06 +01:00
f71ab4635f Bump version 2024-11-22 06:16:42 +01:00
983db6780a #730 2024-11-22 06:16:15 +01:00
ea22107b9b Bump version 2024-11-21 07:12:53 +01:00
8e4a7fed9e Fix invalid dom nesting errors 2024-11-21 07:12:23 +01:00
30efd8fcf4 Bump version 2024-11-21 06:27:09 +01:00
f4c4e92ca1 #726 2024-11-21 06:26:54 +01:00
cfda99f5b0 Bump version 2024-11-19 03:49:32 +01:00
5063b1c7ab Rename LoginDeviceVerifyUserCode to LoginOauth2DeviceVerifyUserCode (as it should have been) 2024-11-19 03:49:01 +01:00
955b6cac45 Remove noisy stories 2024-11-19 03:22:49 +01:00
1fa3d6133c Remove unused template prop 2024-11-19 03:22:35 +01:00
023939a064 Bump version 2024-11-18 21:38:59 +01:00
de4490cf0f Update tsafe 2024-11-18 21:38:27 +01:00
e0cda43724 Change pattern for telling if linked 2024-11-18 21:34:10 +01:00
d4ac67dba8 Bump version 2024-11-18 18:44:57 +01:00
23f4b59559 Merge pull request #725 from keycloakify/all-contributors/add-tripheo0412
docs: add tripheo0412 as a contributor for doc
2024-11-18 17:44:35 +00:00
4a7ba4a1c9 docs: update .all-contributorsrc [skip ci] 2024-11-18 17:27:30 +00:00
a3e765e1fc docs: update README.md [skip ci] 2024-11-18 17:27:29 +00:00
ee3614dbf1 Merge pull request #723 from keycloakify/all-contributors/add-zvn2060
docs: add zvn2060 as a contributor for code
2024-11-18 17:24:28 +00:00
8099ec1ffe docs: update .all-contributorsrc [skip ci] 2024-11-18 17:24:15 +00:00
2e2b0ab3ae docs: update README.md [skip ci] 2024-11-18 17:24:14 +00:00
a1f15f2f6b Merge pull request #722 from zvn2060/main
Fix #721: mismatched LoginPasskeysConditionalAuthenticate
2024-11-18 17:23:15 +00:00
8fe74fe7ee Fix #721: mismatched LoginPasskeysConditionalAuthenticate 2024-11-18 13:59:44 +08:00
232be50225 Bump version 2024-11-18 04:46:58 +01:00
a00bb0c4db Fix logical error 2024-11-18 04:46:29 +01:00
5517d6baf4 Make runPrettier work when project is linked 2024-11-18 04:46:06 +01:00
e2975503a4 Automatically untrack files implemented by ui modules 2024-11-18 03:47:57 +01:00
1231c92198 Update comment 2024-11-18 03:19:13 +01:00
64ca0bc0ca Bump version 2024-11-18 01:09:46 +01:00
2bceb9385c Fix: creating directory 2024-11-18 00:26:48 +01:00
954bc43c22 Bump version 2024-11-17 23:51:02 +01:00
8c99aa4b9d gererate diffrent comment depending on the file type 2024-11-17 23:50:49 +01:00
884195d30d Bump version 2024-11-17 23:21:44 +01:00
17dd726158 Fix passing wrong path to npmInstall 2024-11-17 23:20:24 +01:00
7ee30b6a42 Fix managed gitignore 2024-11-17 23:13:45 +01:00
397f8133bf Fix comment formatting 2024-11-17 23:08:52 +01:00
c9abc6dc5c Fix crawl async 2024-11-17 23:05:41 +01:00
e78eafd1f1 Fix 2024-11-17 20:53:38 +01:00
e50f2bd692 Bump version 2024-11-17 19:35:37 +01:00
ed0428bd55 Remove runFormat 2024-11-17 19:34:57 +01:00
2a126d65c5 Merge pull request #717 from keycloakify/admin_theme_support
Run prettier of a file per file basis (and prepare for extension support)
2024-11-17 18:34:17 +00:00
30149ff1f2 Remove ignored file that where removed 2024-11-17 19:33:44 +01:00
32f471624a Remove ignored file that where removed 2024-11-17 19:31:29 +01:00
7f5eabb639 Format page when ejecting for the account 2024-11-17 19:25:53 +01:00
32fb1e2f71 Fix runPrettier script 2024-11-17 19:22:34 +01:00
7c3c6d3643 Fix misnamed kc.gen 2024-11-17 16:46:25 +01:00
b6d2154d56 Fix usage of deprecated node api 2024-11-17 16:45:14 +01:00
b8d4daf4c1 Temporarely restore runFormat (for merge conflicts) 2024-11-17 03:38:38 +01:00
c03623875a The admin theme does not support traditional eject 2024-11-17 03:35:01 +01:00
c423e4cacc Adapt the npmInstall script so that it works when packages are linked 2024-11-17 03:24:41 +01:00
c593f5cb97 Re-sync version with main 2024-11-16 21:38:31 +01:00
2ad36a8137 Fix runPrettier 2024-11-16 21:38:31 +01:00
bccaddc2de Bump version 2024-11-13 14:38:48 +01:00
97c12c8a12 Merge pull request #720 from sukvvon/fix/add-versatility-in-runFormat
fix(runFormat.ts): improve filtering in scriptNames
2024-11-13 13:38:25 +00:00
b349a819ba fix(runFormat.ts): improve filtering in scriptNames 2024-11-13 21:49:55 +09:00
792d4262c8 Bump version 2024-11-10 14:30:00 +01:00
37a9046a40 Merge pull request #718 from kathari00/fix_storyname
fix story
2024-11-10 13:29:39 +00:00
5ad29d9f43 fix story 2024-11-10 10:33:21 +00:00
645031543e Merge branch 'main' into admin_theme_support 2024-11-10 09:39:26 +00:00
b43c02f279 Release candidate 2024-11-10 10:35:19 +01:00
63877d53be Eject file command implementation 2024-11-09 20:33:53 +01:00
79a580b4a5 Bump version 2024-11-09 19:50:48 +01:00
994f1f8d3d #714 #713 2024-11-09 19:50:29 +01:00
a73281d46d Checkpoint 2024-11-09 14:02:19 +01:00
a60a0d0696 checkpoint 2024-11-09 09:35:41 +01:00
a2ea81b3b8 Bump version 2024-11-06 10:10:55 +01:00
a0461e3ef0 #711 2024-11-06 10:10:27 +01:00
93fcf96cde checkpoint 2024-11-03 01:56:41 +01:00
d7455fd100 Only format kc-gen file 2024-11-03 00:25:28 +01:00
af7a45d125 checkpoint 2024-11-02 22:39:03 +01:00
5357626317 Bump version 2024-10-31 11:05:43 +01:00
552c95c59e https://github.com/keycloakify/keycloakify/pull/705#issuecomment-2448689532 2024-10-31 11:05:25 +01:00
50590697ca Bump version 2024-10-30 15:51:32 +01:00
e261736fa3 Fix: kcContext.scripts can be undefined in error.ftl 2024-10-30 15:51:16 +01:00
db37320280 up 2024-10-27 00:10:39 +02:00
263f55fdd3 Bump version 2024-10-26 22:07:54 +00:00
2b7f8a24a3 Fix import.meta.env.BASE_URL not being corectly replaced when build in windows 2024-10-26 22:07:29 +00:00
b0aa0feab5 up 2024-10-26 22:06:37 +02:00
0e93d4ed09 Implement admin theme support (checkpoint) 2024-10-26 21:23:18 +02:00
dc4eac1a04 Bump version 2024-10-25 04:12:20 +02:00
53a427d190 Delegate add sotry 2024-10-25 04:12:07 +02:00
ae969f91ac Bump version 2024-10-25 02:57:41 +02:00
c83319d6f3 Tell if we should update kcGen based on the hash 2024-10-25 02:57:26 +02:00
329b4cb0fb Enable to link in any keycloakify starter 2024-10-25 02:56:57 +02:00
533f5992d1 coding style fix 2024-10-25 02:44:19 +02:00
cb103cc3e2 Bump version 2024-10-25 00:21:15 +00:00
afdf89fb12 Try to run format on behaf of the user when generating new files with the CLI 2024-10-25 00:20:35 +00:00
26a87b8eaa Remove debug log 2024-10-25 00:01:39 +00:00
25d31463f4 Make script delegation work on windows 2024-10-25 00:01:12 +00:00
2542c38c9b Make linking script work on windows server 2024-10-24 23:47:34 +00:00
7326038424 Fix linking script on windows 2024-10-24 23:21:12 +00:00
12e632d221 More sensible patch for building on windows 2024-10-24 22:38:26 +00:00
3fc2108214 Fix build for windows 2024-10-24 16:56:13 +00:00
babbe39494 Merge pull request #701 from nima70/main
Removed Error Message from Terms and Conditions Page
2024-10-20 21:10:56 +02:00
32b4585e39 I removed the local=en 2024-10-20 13:49:10 -04:00
43469a869c I removed the error message for terms and conditions page 2024-10-20 12:35:14 -04:00
6f823e6478 Bump version 2024-10-20 13:08:59 +02:00
e33693e20e Merge pull request #700 from nima70/main
Storybooks Second Release: Login Page Stories with Improved Coverage
2024-10-20 13:02:27 +02:00
ad3f091d4a Merge pull request #698 from keycloakify/decouple_userprofile_logic
Decouple userprofile logic
2024-10-20 12:52:48 +02:00
3ff01d186d account page test coverage improved 2024-10-19 19:10:32 -04:00
0cf8caa53b storybooks second release 2024-10-19 17:24:29 -04:00
25920c208d Release candidate 2024-10-19 22:33:34 +02:00
19da96113f Don't export internals 2024-10-19 22:33:34 +02:00
6e584e809e Merge branch 'main' into decouple_userprofile_logic 2024-10-19 22:29:48 +02:00
4185188a5b Release candidate 2024-10-19 22:28:10 +02:00
4273322ed5 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:30 +02:00
ba0532c95d docs: update README.md [skip ci] 2024-10-19 22:27:30 +02:00
3a2fe597ba Bump version 2024-10-19 22:27:30 +02:00
dda77952a0 #694 Probably some shell handle double quote differently 2024-10-19 22:27:30 +02:00
d2e518d96b #693 #692 2024-10-19 22:27:30 +02:00
f3a97b2538 Bump version 2024-10-19 22:27:29 +02:00
cacd017244 #696 2024-10-19 22:27:29 +02:00
f5b15a5ef6 Fix Phase two links 2024-10-19 22:27:29 +02:00
de620dca56 Fix light mode rendering 2024-10-19 22:27:29 +02:00
8decf4a3c9 Add phaseTwo as sponsor 2024-10-19 22:27:29 +02:00
831326952b Resize zone2 logo 2024-10-19 22:27:29 +02:00
27da578446 Bump version 2024-10-19 22:27:29 +02:00
2c1cca168f Resolve package.json path relative to the package.json 2024-10-19 22:27:29 +02:00
e498fb784b Bump version 2024-10-19 22:27:29 +02:00
2917719315 Add dir=rtl attribut to html when using a RTL language 2024-10-19 22:27:29 +02:00
9ed90995e4 typesafety fix 2024-10-19 22:27:29 +02:00
0f99bb5bdc fix: added parameter type for story context on register page 2024-10-19 22:27:29 +02:00
1f4d4473e4 Bump version 2024-10-19 22:27:29 +02:00
5332001ff4 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:29 +02:00
22241fd7ad docs: update README.md [skip ci] 2024-10-19 22:27:29 +02:00
ddeade9775 Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-10-19 22:27:29 +02:00
f1cb165bdd Bump version 2024-10-19 22:27:29 +02:00
9873353990 Fix initialize-email-theme 2024-10-19 22:27:29 +02:00
b879569b81 Announcement about Keycloak 26 2024-10-19 22:27:29 +02:00
c3e821088b Bump version 2024-10-19 22:27:29 +02:00
dc4f386e7a Fix vite quitting if custom handler implemented 2024-10-19 22:27:29 +02:00
a40810b364 Bump version 2024-10-19 22:27:29 +02:00
1690629717 Fix: check for delegation of the eject-page command 2024-10-19 22:27:29 +02:00
9a6a71c8bc Fix litle inconsistency 2024-10-19 22:27:29 +02:00
d626699f08 Bump version 2024-10-19 22:27:29 +02:00
6aa60e685b Release candidate 2024-10-19 22:27:29 +02:00
9910762abc Add initialize-email-theme, initialize-account-theme and copy-keycloak-resources-to-public to commands that can be delegated to a custom handler 2024-10-19 22:27:29 +02:00
182fb430f1 Fix dead code 2024-10-19 22:27:29 +02:00
bda20e2fbe Release candidate 2024-10-19 22:27:29 +02:00
bc586eceef Make sure the update-kc-gen command is delegated when building with vite 2024-10-19 22:27:29 +02:00
128b27221a Release candidate 2024-10-19 22:27:29 +02:00
2dfb4eda9d No need to handle non react environement with custom handler support 2024-10-19 22:27:29 +02:00
fed6af4dfe Release candidate 2024-10-19 22:27:29 +02:00
c4ee6cd85c Fix not handling correctly exit cause 2024-10-19 22:27:29 +02:00
8fc307bd8d Release candidate 2024-10-19 22:27:29 +02:00
9e9ffcd586 add debug logs 2024-10-19 22:27:29 +02:00
49b064b5f2 Release candidate 2024-10-19 22:27:29 +02:00
ef6f5a4c23 Add other missing declaration files 2024-10-19 22:27:28 +02:00
e92562fd44 Release candidate 2024-10-19 22:27:28 +02:00
fe65ddb5f8 Fix missing exports 2024-10-19 22:27:28 +02:00
ffd405c6db Release candidate 2024-10-19 22:27:28 +02:00
9e41868e0d Implement custom handler cli hook 2024-10-19 22:27:28 +02:00
ca6accc889 Bump version 2024-10-19 22:27:28 +02:00
dfe2e1562a Fix cache issue 2024-10-19 22:27:28 +02:00
ab43bb73d7 Bump version 2024-10-19 22:27:28 +02:00
22b0b95e54 Update readme, support keycloak 26 2024-10-19 22:27:28 +02:00
290ad8b592 Update version ranges for Multi-Page account theme 2024-10-19 22:27:28 +02:00
d5519dbb55 Release candidate 2024-10-19 22:27:28 +02:00
4de9e059e9 Aditional context exclusion 2024-10-19 22:27:28 +02:00
e573aff6ae Release candidate 2024-10-19 22:27:28 +02:00
908e083dee Update version target range 2024-10-19 22:27:28 +02:00
ec29724997 Fix link in CONTRIBUTING.md 2024-10-19 22:27:28 +02:00
88756e9807 Bump version 2024-10-19 22:27:28 +02:00
80d8a0c4e3 ['select-radiobuttons'/'multiselect-checkboxes'] fixed 'inputOptionLabels' 2024-10-19 22:27:28 +02:00
7241f0c741 Bump version 2024-10-19 22:27:28 +02:00
8565eb3fb8 Update tsafe 2024-10-19 22:27:28 +02:00
87198f6e56 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
fa934da442 docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
6c4dc711d2 Put Kathi as first contributor 2024-10-19 22:27:28 +02:00
1f2a755a97 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
a0e3dc163a docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
810dc6ceb5 Bump version 2024-10-19 22:27:28 +02:00
7203c742be Avoid modifying BASE_URL for App context 2024-10-19 22:27:28 +02:00
2fd04cfb61 Bump version 2024-10-19 22:27:28 +02:00
9c44d13f73 Update tsafe (provide ESM distribution) 2024-10-19 22:27:27 +02:00
d6436a58a2 update ci 2024-10-19 22:27:27 +02:00
613167f3a6 Bump version 2024-10-19 22:27:27 +02:00
ab0c281d98 Fix allegated vulnerability 2024-10-19 22:27:27 +02:00
c84dc281a2 Bump version 2024-10-19 22:27:27 +02:00
835833a61b Remove unessesary reference to react specific construct in KcContext 2024-10-19 22:27:27 +02:00
9af542ec89 Bump version 2024-10-19 22:27:27 +02:00
06e33196bb Refactor: Make ClassKey importable without having react as a dependency 2024-10-19 22:27:27 +02:00
36dd324139 complete decoupling of user profile form validation logic 2024-10-19 10:18:22 +02:00
52d4fe920c Merge pull request #697 from keycloakify/all-contributors/add-marvinruder
docs: add marvinruder as a contributor for bug
2024-10-19 03:21:05 +02:00
0d090d50d4 Bump version 2024-10-19 02:28:32 +02:00
e57232edde #694 Probably some shell handle double quote differently 2024-10-19 02:28:11 +02:00
dfe1e7ddd1 docs: update .all-contributorsrc [skip ci] 2024-10-19 00:24:37 +00:00
5ffc42c9db docs: update README.md [skip ci] 2024-10-19 00:24:36 +00:00
c63648a1b0 #693 #692 2024-10-19 02:19:41 +02:00
80fd4095c4 Bump version 2024-10-17 23:23:52 +02:00
31d7a938f2 #696 2024-10-17 23:23:26 +02:00
ee1b6868f8 Fix Phase two links 2024-10-17 19:54:14 +02:00
7c7e5544e4 Fix light mode rendering 2024-10-16 05:13:52 +02:00
06fe26fbe7 Add phaseTwo as sponsor 2024-10-16 04:10:17 +02:00
c932c7d8f6 Resize zone2 logo 2024-10-16 03:37:00 +02:00
d56c536446 Bump version 2024-10-13 00:55:23 +02:00
f5a9a28124 Resolve package.json path relative to the package.json 2024-10-13 00:55:06 +02:00
b86039536e Bump version 2024-10-12 17:33:44 +02:00
59c4675e8a Add dir=rtl attribut to html when using a RTL language 2024-10-12 17:30:30 +02:00
fbf6a329df typesafety fix 2024-10-11 23:55:07 +02:00
ddec3118a4 Merge pull request #688 from keycloakify/fix/missing-type-register-story
fix: added parameter type for story context on register page
2024-10-08 23:58:06 +02:00
94e2786297 fix: added parameter type for story context on register page 2024-10-08 16:50:11 -05:00
ecfdff5454 Bump version 2024-10-07 23:53:05 +02:00
c598a58ec9 Merge pull request #685 from keycloakify/all-contributors/add-nima70
docs: add nima70 as a contributor for code, and test
2024-10-07 23:47:25 +02:00
3e38beb190 docs: update .all-contributorsrc [skip ci] 2024-10-07 21:47:11 +00:00
61a86e8e82 docs: update README.md [skip ci] 2024-10-07 21:47:10 +00:00
9c8e127fa0 Merge pull request #672 from nima70/main
Changes:
2024-10-07 23:46:13 +02:00
eb23b40e5c Bump version 2024-10-07 21:03:04 +02:00
9e19aafcd0 Fix initialize-email-theme 2024-10-07 21:02:51 +02:00
4cdf26b6f9 Announcement about Keycloak 26 2024-10-07 20:56:03 +02:00
88de58cc22 Bump version 2024-10-06 22:56:42 +02:00
f6b48c88b9 Fix vite quitting if custom handler implemented 2024-10-06 22:55:18 +02:00
12534e57ad Bump version 2024-10-06 22:09:21 +02:00
52e33bba2d Fix: check for delegation of the eject-page command 2024-10-06 22:08:43 +02:00
d63e5f4e54 Fix litle inconsistency 2024-10-06 15:37:32 +02:00
8d31866a0b Bump version 2024-10-06 15:09:53 +02:00
7d818f217a Merge pull request #683 from keycloakify/feat_custom_handler
Feat custom handler
2024-10-06 15:09:26 +02:00
7156665684 Release candidate 2024-10-06 13:19:12 +02:00
5045c5e8bf Add initialize-email-theme, initialize-account-theme and copy-keycloak-resources-to-public to commands that can be delegated to a custom handler 2024-10-06 13:18:30 +02:00
9de2ed9eaf Fix dead code 2024-10-06 12:44:46 +02:00
096cf7a570 Release candidate 2024-10-06 09:07:10 +02:00
a04f07d149 Make sure the update-kc-gen command is delegated when building with vite 2024-10-06 09:06:49 +02:00
63775b2866 Release candidate 2024-10-06 06:45:06 +02:00
e8609de7b4 No need to handle non react environement with custom handler support 2024-10-06 06:44:53 +02:00
e62aa89d72 Release candidate 2024-10-06 06:42:04 +02:00
77f12a940d Fix not handling correctly exit cause 2024-10-06 06:41:51 +02:00
0fe49e3d6e Release candidate 2024-10-05 22:29:13 +02:00
881386a123 add debug logs 2024-10-05 22:28:36 +02:00
7b9aec4ed0 Release candidate 2024-10-05 21:39:32 +02:00
cf18f9d06c Add other missing declaration files 2024-10-05 21:39:14 +02:00
052936f769 Release candidate 2024-10-05 21:23:57 +02:00
590de7a67b Fix missing exports 2024-10-05 21:23:17 +02:00
7f608ad8ad Release candidate 2024-10-05 20:31:41 +02:00
35b012b937 Implement custom handler cli hook 2024-10-05 20:30:09 +02:00
e3bd7f3bc5 Bump version 2024-10-04 16:56:17 +02:00
e14f187fc0 Fix cache issue 2024-10-04 16:56:02 +02:00
da495b90ae Bump version 2024-10-04 13:00:15 +02:00
8d9b80f549 Update readme, support keycloak 26 2024-10-04 12:59:56 +02:00
2e9da33622 Merge pull request #681 from keycloakify/keycloak-26
Update version target range
2024-10-04 12:58:50 +02:00
6f416ad335 Update version ranges for Multi-Page account theme 2024-10-04 12:58:31 +02:00
4e982ee898 Release candidate 2024-10-04 12:44:22 +02:00
bcb514ae9c Aditional context exclusion 2024-10-04 12:44:03 +02:00
cfdad8d71d Release candidate 2024-10-04 12:17:54 +02:00
39ad1eb8d1 Update version target range 2024-10-04 12:17:08 +02:00
3d1d2e316b Merge pull request #680 from pnzrr/pnzrr-patch-1
Fix link in CONTRIBUTING.md
2024-10-04 06:58:52 +02:00
dd217e8a46 Fix link in CONTRIBUTING.md 2024-10-03 21:04:02 -06:00
1339a96ea4 Bump version 2024-10-02 23:36:58 +02:00
616e834c90 Merge pull request #678 from johanjk/main
respect inputOptionLabels
2024-10-02 23:36:23 +02:00
80eaa77acc ['select-radiobuttons'/'multiselect-checkboxes'] fixed 'inputOptionLabels' 2024-10-02 16:16:16 +02:00
ce3135c83b Bump version 2024-10-02 13:44:22 +02:00
09abc73068 Update tsafe 2024-10-02 13:42:38 +02:00
037d623550 Merge pull request #676 from keycloakify/all-contributors/add-luca-peruzzo
docs: add luca-peruzzo as a contributor for code, and test
2024-10-02 11:05:58 +02:00
8c8d2fd6a8 docs: update .all-contributorsrc [skip ci] 2024-10-02 09:05:35 +00:00
153a99d63f docs: update README.md [skip ci] 2024-10-02 09:05:34 +00:00
939e3ca7ea Put Kathi as first contributor 2024-10-02 11:02:25 +02:00
a0dc7eeb7c Merge pull request #675 from keycloakify/all-contributors/add-kathari00
docs: add kathari00 as a contributor for code, test, and doc
2024-10-02 11:00:06 +02:00
c21d072231 docs: update .all-contributorsrc [skip ci] 2024-10-02 08:59:49 +00:00
2e10ec8073 docs: update README.md [skip ci] 2024-10-02 08:59:48 +00:00
1177d6770c Bump version 2024-10-01 11:59:39 +02:00
d492a393fe Merge pull request #674 from keycloakify/dont_touch_base_url
Avoid modifying BASE_URL for App context
2024-10-01 11:59:14 +02:00
77952337c5 Avoid modifying BASE_URL for App context 2024-10-01 11:52:40 +02:00
6716fcb881 Bump version 2024-09-30 18:10:26 +02:00
302fe8d7cd Update tsafe (provide ESM distribution) 2024-09-30 18:10:09 +02:00
2ea5e34e81 update ci 2024-09-30 17:57:41 +02:00
d7103b1ad9 Bump version 2024-09-30 11:49:33 +02:00
9f8a36fe93 Fix allegated vulnerability 2024-09-30 11:48:57 +02:00
47ca811878 Bump version 2024-09-30 01:22:49 +02:00
8cacb21f1b Remove unessesary reference to react specific construct in KcContext 2024-09-30 01:22:37 +02:00
a0c95207cf Bump version 2024-09-30 01:10:45 +02:00
da3023cf5e Refactor: Make ClassKey importable without having react as a dependency 2024-09-30 00:31:27 +02:00
5892cf2ba7 Decouple user profile form logic so it can be consumed in angular 2024-09-30 00:19:37 +02:00
c9d7fc1b6e Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-09-29 04:35:02 -04:00
94779c3476 Bump version 2024-09-28 00:43:55 +02:00
802a6ab5ec Explicitely prohibit space and special character in theme names 2024-09-28 00:34:24 +02:00
04307c8226 Remove dead code 2024-09-28 00:17:17 +02:00
ff6b91b801 refactor 2024-09-28 00:05:19 +02:00
c8ca598465 Refactor 2024-09-27 23:45:14 +02:00
9444b897ee #669 2024-09-27 23:37:23 +02:00
3d1951b72c Merge together generateResourcesForMainTheme and generateResourcesForThemeVariant 2024-09-27 23:05:51 +02:00
acc27ae448 #668 2024-09-26 20:34:00 +02:00
e6993214ff Bump version 2024-09-25 10:26:46 +02:00
2f02a4379c Enable i18n in Single-Page account theme 2024-09-25 10:26:25 +02:00
b57d014e9a Bump version 2024-09-24 19:47:01 +02:00
f57f311aab Fix async io not awaited and don't crash if .ftl files does not exist for some reason 2024-09-24 19:47:01 +02:00
4f11415107 Merge pull request #667 from keycloakify/all-contributors/add-uchar
docs: add uchar as a contributor for test, and code
2024-09-23 05:00:23 +02:00
346fd7175f Only publish storybook if we are on main 2024-09-23 04:36:00 +02:00
7c02d77057 docs: update .all-contributorsrc [skip ci] 2024-09-23 02:31:26 +00:00
d3fd4b6bbf docs: update README.md [skip ci] 2024-09-23 02:31:25 +00:00
43ef527810 Bump version 2024-09-23 00:29:17 +02:00
a6032a1387 Merge pull request #653 from keycloakify/i18n_extraLanguages_and_perThemeVariantTranslations
Start implementing per theme variant translations and ability to add extra languages
2024-09-23 00:23:55 +02:00
23179cac53 Fix last bug 2024-09-23 00:19:34 +02:00
954c3319bb Fix bug in label resolution 2024-09-22 23:46:45 +02:00
eb6ec0275d Remove debug log 2024-09-22 22:53:31 +02:00
890f8bc2d5 Fix: Forget to create a dir before writing files 2024-09-22 22:53:13 +02:00
26b8dd9cda Improve intentionality 2024-09-22 22:48:31 +02:00
c07af8491c Complete statical parsing of withExtraLanguages 2024-09-22 22:46:56 +02:00
10d4da9fbf No need to escape since we sanitize 2024-09-22 22:18:24 +02:00
95e861099f Integrate kcSanitize 2024-09-22 20:41:18 +02:00
6dc51dfab3 Fix some bugs in the vendoring script 2024-09-22 20:21:07 +02:00
ddb0af1dcb Vendor dompurify, use isomorphic-dompurify only for tests 2024-09-22 20:12:11 +02:00
b6e9043d91 Reorganize kcSanitarize 2024-09-22 18:56:05 +02:00
7c553ee10d Restore package.json and yarn.lock 2024-09-22 18:29:29 +02:00
2a6b14adc6 Merge pull request #666 from uchar/fix/dangerouslySetInnerHTML
Fix/dangerously set inner html
2024-09-22 18:27:25 +02:00
159a5f60d0 Add missing scope in ftl template 2024-09-22 18:22:11 +02:00
08f03b3118 Merge branch 'main' into i18n_extraLanguages_and_perThemeVariantTranslations 2024-09-22 18:15:25 +02:00
f137960f96 Reneame useStylesAndScript to useInitialize 2024-09-22 18:12:46 +02:00
e5ab46727a Make the i18n API more type safe 2024-09-22 17:14:03 +02:00
8d2679b76e Progess in parsing of the extra languages provided by the user 2024-09-22 15:39:32 +02:00
b0b6b994ed Almost done, left to extract the extra language resources 2024-09-22 04:39:24 +02:00
bb163132fe Fix minor inconsistency 2024-09-21 23:42:59 +02:00
439bed2f24 Update account theme i18n.ts boilerplate 2024-09-21 23:41:08 +02:00
5a233d8878 Avoid too many types declaration indirections 2024-09-21 23:35:44 +02:00
20cdbb6185 Rename .create() by .build() for i18nBuilder 2024-09-21 23:21:15 +02:00
b3c4208e44 Rename i18nInitializer by i18nBuilder 2024-09-21 23:09:12 +02:00
8623037224 Various little adjustments relative to the new i18n API 2024-09-21 22:35:30 +02:00
e8d3d3d741 Automatically generate account i18n code 2024-09-21 21:44:14 +02:00
cc700f0ba0 Untrack account i18n, code will be generated automatically 2024-09-21 21:33:57 +02:00
801a5cce17 Enable to add label to extra message not in the default set 2024-09-21 21:33:04 +02:00
2a3ad58c18 Throw an error if providing translation for a language that is already supported 2024-09-21 18:17:43 +02:00
969744f4cb Complete runtime API implementation 2024-09-21 17:59:16 +02:00
40ebbdebeb Fix some type errors 2024-09-21 04:45:00 +02:00
eb64886dcf Generate LanguageTage.ts 2024-09-21 04:36:48 +02:00
81fc9d57bd remove async from sanitize 2024-09-18 18:37:17 +03:30
66b480f837 use textarea on client for decode 2024-09-18 11:13:49 +03:30
7e6a84ce19 Add more tests 2024-09-17 09:39:07 +03:30
68e7642827 Remove extra comment 2024-09-17 01:11:14 +03:30
b37c7ccc8a Merge with master 2024-09-17 01:01:45 +03:30
b7c9ba8ffd Merge branch 'main' of https://github.com/uchar/keycloakify into fix/dangerouslySetInnerHTML 2024-09-17 01:01:02 +03:30
c8a31c4b6a Add KCSantisizer 2024-09-17 00:56:46 +03:30
fb6f450bfe Bump version 2024-09-16 14:30:50 +02:00
9a97d86ff9 #638 #631 Follow up 2024-09-16 14:30:27 +02:00
a5e3ecb38b Bump version 2024-09-16 13:50:08 +02:00
9b22d94600 Remove previous .keycloakify dir 2024-09-16 13:49:54 +02:00
a42ddb959b Bump version 2024-09-16 13:38:05 +02:00
b4e94d3c00 Use keycloakify-dev-resources instead of .keycloakify 2024-09-16 13:37:29 +02:00
aad89a2001 Start implementing per theme variant translations and ability to add extra languages 2024-09-15 16:55:18 +02:00
07e4f99f80 Bump version 2024-09-13 12:47:01 +02:00
715562c750 Merge pull request #646 from BII-GmbH/main 2024-09-13 12:46:16 +02:00
ef4f4d8374 allow docker start script to work with podman 2024-09-13 10:58:52 +02:00
e15f13646c GitHub pages does not serve dotfile, patch storybook build #645 2024-09-10 19:31:03 +02:00
8677c17f29 #645 2024-09-10 19:01:20 +02:00
21ee42b5a4 Bump version 2024-09-10 10:41:35 +02:00
d20964ec94 Merge pull request #632 from keycloakify/passkey-conditional-authenticate
Passkey conditional authenticate
2024-09-10 10:40:57 +02:00
3155f5da66 registrationDisabled is optional 2024-09-10 10:12:33 +02:00
50e38b6a10 Complete rework of WebauthnRegister 2024-09-10 09:57:47 +02:00
72c31776d7 Update WebauthnAuthenticate 2024-09-09 08:49:59 +02:00
7456750828 Support the new recapcha without breaking for older keycloak 2024-09-09 08:25:00 +02:00
b8a08f0789 Social is now optional on the kcContext 2024-09-09 07:51:49 +02:00
28990a12da Fix LoginRecoveryAuthnCodeConfig 2024-09-09 07:39:45 +02:00
7e5abe8589 Fix LoginPasskesConditionalAuthenticate 2024-09-09 06:59:11 +02:00
1d57f4b4dc Include missing dependency file 2024-09-08 18:16:49 +02:00
9f875160ea Make template initialization not ejected by default 2024-09-08 17:31:55 +02:00
785ed095bc Repatriate keycloak v24 scripts 2024-09-08 14:41:45 +02:00
359e93a1ba Fix path error 2024-09-08 13:00:40 +02:00
ee6322aae4 Extract only required files 2024-09-08 12:52:29 +02:00
98d3d1967a Fix import error 2024-09-08 12:09:19 +02:00
01c3b148e6 Group all build time generated resource under a 'res' directory 2024-09-08 12:06:49 +02:00
77d3a5190d Refactor checkpoint 2024-09-08 12:00:07 +02:00
93c1c56279 Refactor checkpoint 2024-09-08 00:06:47 +02:00
8340608045 Merge branch 'main' into passkey-conditional-authenticate 2024-09-07 15:27:51 +02:00
5502a74994 Bump version 2024-09-05 01:23:13 +02:00
c5ef4c973b #598 2024-09-05 01:19:50 +02:00
dbae909903 Pulling the message resources of the account theme from the installed keycloak-account-ui version 2024-09-05 00:32:21 +02:00
74317a1f3c Remove extra "v" when initializing single page account theme 2024-09-05 00:31:33 +02:00
569e933f02 Release candidate 2024-09-02 03:37:36 +02:00
46c40d713a Fix broken path 2024-09-02 03:37:16 +02:00
f3602219f3 Release candidate 2024-09-02 03:27:09 +02:00
c6b52acf2f #631 2024-09-02 03:26:39 +02:00
7260589136 Always generates the pages for legacy keycloak no matter what. 2024-08-30 19:27:56 +02:00
b2e9ddaa4f Bump version 2024-08-30 15:35:45 +02:00
4338b3ecb7 #627 2024-08-30 15:35:21 +02:00
0f81d9f146 Update readme 2024-08-29 01:22:16 +02:00
9980b10a83 Bump version 2024-08-28 17:09:21 +02:00
6bfd388827 Fix bug package.json not properly updated when initializing the single page account theme with webpack 2024-08-28 17:08:11 +02:00
8203ed687b Bump version 2024-08-28 14:43:40 +02:00
f394e06e4d Fix bug in type validation for webpack when initializing the account theme 2024-08-28 14:43:25 +02:00
8db35a81da Bump version 2024-08-27 17:41:04 +02:00
2e0ebfcf58 https://github.com/keycloakify/keycloakify-starter/issues/25 2024-08-27 17:40:48 +02:00
51d2ff85e0 Bump version 2024-08-26 18:20:32 +02:00
8b54426b89 Fix invalid dom nesting 2024-08-26 18:20:32 +02:00
fa346c5b1f Merge pull request #621 from keycloakify/all-contributors/add-liamlows
docs: add liamlows as a contributor for code, and doc
2024-08-26 04:27:06 +02:00
d87788980d docs: update .all-contributorsrc [skip ci] 2024-08-26 02:26:54 +00:00
1e4319498c docs: update README.md [skip ci] 2024-08-26 02:26:53 +00:00
48501407fc Release v10 🎉 2024-08-26 04:05:40 +02:00
01cbdee2ca Release candidate 2024-08-25 19:02:33 +02:00
b70c0af0a9 Add users to provided realm configuration if none exists 2024-08-25 19:02:00 +02:00
dcaee9cb7f Release candidate 2024-08-25 03:19:58 +02:00
1d8b6c7792 Fix logical error in multivalued attributes 2024-08-25 03:19:34 +02:00
c98dbe84c6 Add missing space before the * 2024-08-25 02:54:46 +02:00
1785916d32 download and extract actually just for downloading and extracting 2024-08-24 23:15:54 +02:00
c6cf564842 Release candidate 2024-08-23 19:01:56 +02:00
380b739017 Don't pin the patch version in the docker tag 2024-08-23 19:01:37 +02:00
c3f3c55303 Release candidate 2024-08-23 18:45:56 +02:00
2c01018529 #618 2024-08-23 18:36:40 +02:00
dd2edf3013 Merge pull request #616 from keycloakify/all-contributors/add-oliviergoulet5
docs: add oliviergoulet5 as a contributor for code
2024-08-22 00:56:15 +02:00
7f3cdf9fac Release candidate 2024-08-22 00:55:39 +02:00
f75a91fbc1 docs: update .all-contributorsrc [skip ci] 2024-08-21 22:55:00 +00:00
f151086bb1 docs: update README.md [skip ci] 2024-08-21 22:54:59 +00:00
7c833e6f10 Merge pull request #615 from oliviergoulet5/fix-array-operations
Fix array comparison and improve type check
2024-08-22 00:53:48 +02:00
885e8314e8 Fix array comparison and type check 2024-08-21 17:13:06 -04:00
3bdd955ab6 Release candidate 2024-08-19 02:11:31 +02:00
9499587bad Fix formating bug of Docker command being run 2024-08-19 02:10:59 +02:00
0879ddba7c Release candidate 2024-08-19 00:25:54 +02:00
106a1dd4c7 Support parsing of the KC_HTTP_RELATIVE_PATH option 2024-08-19 00:25:41 +02:00
5580248bcd Release candidate 2024-08-19 00:00:22 +02:00
c9c10b8fba Fix issue with the port in the start-keycloak command 2024-08-19 00:00:08 +02:00
ed254922e9 Relase candidate 2024-08-18 23:46:12 +02:00
4b7d1e2cec Fix bug in docker command 2024-08-18 23:45:58 +02:00
775ae57258 Release candidate 2024-08-18 21:10:37 +02:00
96e4cd79ee Enable to configure the port via the build options 2024-08-18 21:10:18 +02:00
bb70f7df4f Release candidate 2024-08-18 20:56:34 +02:00
602de2e407 Fix bug with spaces in docker run command 2024-08-18 20:56:25 +02:00
225ced989c Release candidate 2024-08-18 19:20:57 +02:00
ab53698f34 Merge pull request #612 from keycloakify/extensions
keycloak start command options support in config
2024-08-18 19:20:31 +02:00
02f2124126 keycloak start command options support in config 2024-08-18 19:19:35 +02:00
66623e3324 Release candidate 2024-08-16 08:48:21 +02:00
4cc886fd04 Update misleading note in the readme 2024-08-16 08:48:06 +02:00
a10b490245 Release candidate 2024-08-15 22:40:34 +02:00
b947b8a00d Display name and displayNameHtml are always provided 2024-08-15 22:40:08 +02:00
60fa240a4d #611 2024-08-15 22:38:45 +02:00
e05cd87b7c Release candidate 2024-08-14 18:32:21 +02:00
8e41c905ed Add the icons to the social provider in the story 2024-08-14 18:31:55 +02:00
e21f607ab0 Merge pull request #609 from keycloakify/all-contributors/add-madmadson
docs: add madmadson as a contributor for code
2024-08-14 16:36:59 +02:00
34af5abb82 docs: update .all-contributorsrc [skip ci] 2024-08-14 14:36:45 +00:00
fc1cdb5dc9 docs: update README.md [skip ci] 2024-08-14 14:36:44 +00:00
069a0cc980 Release candidate 2024-08-14 16:34:45 +02:00
78363727e1 Add correct fetch options to octokit 2024-08-14 16:34:22 +02:00
23b16746f6 Release candidate 2024-08-14 15:48:37 +02:00
6edf9c3d15 Fix div duplication 2024-08-14 15:48:16 +02:00
2e371d2078 Fix linking script for windows 2024-08-14 07:11:16 +02:00
b70b478e25 Pin cheerio to a given version 2024-08-13 14:58:46 +02:00
97ad132086 Update to latest typescript v4 release 2024-08-13 09:31:23 +02:00
2c5c54bf46 Don't use default import for cheerio (prepare for v1) 2024-08-13 09:25:06 +02:00
c0ca078b43 Release candidate 2024-08-13 00:20:54 +02:00
53e94d04f6 Improve message related to pnpm dlx 2024-08-13 00:17:26 +02:00
dd198f9f06 Tell pepole they can explicitely provide the keycloak version 2024-08-13 00:17:26 +02:00
43f455f4d0 Provide the proxy options to oktokit 2024-08-13 00:17:26 +02:00
d9132ea5a5 Merge pull request #603 from keycloakify/debug_fetch_proxy
Debug fetch proxy
2024-08-07 19:28:04 +02:00
d5c7e2547b Release candidate 2024-08-07 19:01:15 +02:00
13b87de06c Remove debug log 2024-08-07 19:00:57 +02:00
83bdbb7a7e Release candidate 2024-08-07 16:07:25 +02:00
89320b8d51 Fix get proxy option 2024-08-07 16:07:07 +02:00
5fa9c3879c Release candidate 2024-08-07 11:48:02 +02:00
c0cd76d40e Debug log for proxy config 2024-08-07 11:46:05 +02:00
01f60f8013 Release candidate 2024-08-07 07:51:07 +02:00
91ad0712af Make defaultuser english in keycloak 25 2024-08-07 07:33:48 +02:00
2cb1b36725 Release candidate 2024-08-07 06:22:22 +02:00
67ce66765f Enable delete account in default Keycloak realm configuration 2024-08-07 06:21:59 +02:00
c8cc453942 Release candidate 2024-08-06 06:42:02 +02:00
3f835f152f #602 2024-08-06 06:41:25 +02:00
35e8a853e0 Release candidate 2024-08-02 14:09:29 +02:00
d084a4bf4a Fix bug spaces in path keycloak-start 2024-08-02 14:09:09 +02:00
2a6b79e097 Release candidate 2024-07-31 18:46:10 +02:00
5d786c922f Enable the errors to be displayed immediately and not after focus is lost 2024-07-31 18:45:48 +02:00
26bd5dd534 Release candidate 2024-07-31 11:57:38 +02:00
b4df0ce52c Set the default user locale to english 2024-07-31 11:57:13 +02:00
386a8d7cd7 Rework the storybook 2024-07-29 05:12:31 +02:00
5221fb3479 Prevent reload loop in storybook 2024-07-29 02:48:57 +02:00
2871f63f25 Mention account Single Page in the storybook 2024-07-29 00:29:36 +02:00
4c282d0559 Release candidate 2024-07-28 20:01:27 +02:00
4ac14dc074 Prevent exposing too much information in the kcContext.realm of the single page account UI 2024-07-28 20:01:11 +02:00
fcdbb04ea6 Do not make select theme type when there's only one option 2024-07-28 19:37:15 +02:00
14f283cf49 Do not enable to add story when single page account theme 2024-07-28 19:33:27 +02:00
efc459663a Adapt eject-page for Single-Page account ui 2024-07-28 19:24:00 +02:00
d459aaf943 Add hint on how to enable 2024-07-28 18:32:22 +02:00
921c7d5441 Restore the CI setup for main 2024-07-27 18:03:15 +02:00
7d7e648968 Merge pull request #538 from keycloakify/keycloak_24
Keycloakify v10 (Keycloak v24 & 25 support and much more)
2024-07-27 17:56:06 +02:00
96fc779ec8 Release candidate 2024-07-27 17:50:58 +02:00
9605e17e96 Fix generaion of entrypoint 2024-07-27 17:50:31 +02:00
111c1675f9 Release candidate 2024-07-27 01:14:03 +02:00
d547ec3126 #596 2024-07-27 01:13:47 +02:00
0ce6a7be7f #597 2024-07-27 01:07:04 +02:00
1e5eae69e9 Update README.md 2024-07-27 00:52:57 +02:00
89d9208f44 Fix storybook build 2024-07-27 00:44:59 +02:00
3e80aaf242 Fix vitest setup 2024-07-25 20:03:03 +02:00
86c3159ded Release candidate 2024-07-25 19:58:24 +02:00
230e05abc0 Fix tsconfig exclusion 2024-07-25 19:53:36 +02:00
ff2e6e6432 Fix spelling in directory structure 2024-07-25 19:53:36 +02:00
dc00be9be6 Don't run npm install when linked 2024-07-25 19:53:36 +02:00
77249d8a58 Feat: Initialize account theme (before debug) 2024-07-25 19:53:36 +02:00
b9ee0afe7f Fix bug in download and extract archive 2024-07-25 19:53:36 +02:00
db23ab0bc2 Introduce build option: accountThemeImplementation 2024-07-25 19:53:36 +02:00
13dc47533c Release candidate 2024-07-24 17:00:11 +02:00
0091a888bc #594 2024-07-24 16:59:54 +02:00
724b585004 Release candidate 2024-07-23 14:58:54 +02:00
c0d127e4f4 #593 2024-07-23 14:58:39 +02:00
1638577d98 Update sponsors section 2024-07-20 12:37:05 +02:00
951c202fd0 Merge pull request #589 from lokmeinmatz/keycloak_24
fix: Typo InputFiledByType to InputFieldByType
2024-07-17 14:30:17 +02:00
a578b86715 Release candidate 2024-07-17 13:58:55 +02:00
b6b384854e Remove tsafe usage from ejectable page 2024-07-17 13:58:39 +02:00
dac937060d fix: Typo InputFiledByType to InputFieldByType 2024-07-17 09:35:59 +02:00
c628183773 Release candidate 2024-07-14 17:55:02 +02:00
eaacaa6966 (BREAKING CHANGE) When classes are overloaded disable default paterlyfly classes 2024-07-14 17:54:44 +02:00
9a09e280c9 Release candidate 2024-07-14 17:45:54 +02:00
70ac07d861 css replace: Don't choke on parenthesis in urls 2024-07-14 17:45:34 +02:00
dabe372360 Release candidate 2024-07-14 16:58:51 +02:00
d8e3fdeb14 Always use quotes in CSS urls 2024-07-14 16:58:35 +02:00
a147084458 Release candidate 2024-07-14 08:39:36 +02:00
b25e171412 Add the CLEAR special class to remove Paterlyfly classes 2024-07-14 08:39:19 +02:00
60aaa03202 Annotate i18n nodes 2024-07-14 08:11:17 +02:00
3392ab8385 Release candidate 2024-07-13 19:34:23 +02:00
f172b94467 Use uppercase for constants 2024-07-13 19:33:59 +02:00
ca549fe8d8 There's no need to decodeHtmlEntities on everything 2024-07-13 19:02:13 +02:00
0b6f56a774 Naming convention consistency 2024-07-13 18:42:27 +02:00
e5bcff12cb Exclude ream.attributes since it's never used 2024-07-13 18:29:30 +02:00
2754900f7a Refactor of the FreeMarker template 2024-07-13 18:17:21 +02:00
54f43d3331 Remove debug message 2024-07-13 11:02:12 +02:00
4900200c06 Release candidate 2024-07-13 09:27:10 +02:00
6d82a74db4 Improve incremental build time 2024-07-13 09:26:48 +02:00
2d7f21b021 Release candidate 2024-07-13 09:07:36 +02:00
4292c0c642 Rework i18n 2024-07-13 09:07:11 +02:00
9dca515a42 Add group annotations to Attribute 2024-07-13 08:00:16 +02:00
b577cd9829 Update storybook story SelectAuthenticator.stories.ts 2024-07-11 18:23:08 +02:00
704682cbbe Release candidate 2024-07-11 17:58:41 +02:00
858f0d77c0 #585 2024-07-11 17:58:26 +02:00
31ef6063f2 Add missing font and optimize keycloak theme resources extraction 2024-07-11 17:49:58 +02:00
f3bd81c55b Release candidate 2024-07-10 22:18:46 +02:00
24bb4902c2 Includes in the kcContext missing realm defined translations #582 2024-07-10 22:18:24 +02:00
ca7821cfad Remove unused import 2024-07-10 01:26:55 +02:00
a73b25580e Release candidate 2024-07-09 15:00:21 +02:00
82e179730e Fix error related to npm config get 2024-07-09 14:59:59 +02:00
b5d5002061 Mock kcContext.url.resourcePath for account v3 2024-07-09 14:04:25 +02:00
2ab2c9e05e Release candidate 2024-07-08 15:21:23 +02:00
b1e9ba3ac6 Fix buil when paths with spaces 2024-07-08 15:21:03 +02:00
5822ed0185 Relase candidate 2024-07-08 14:54:37 +02:00
17b295788d Generate i18n messages for account v3 2024-07-08 14:54:09 +02:00
6cd5b958c7 Fix typo 2024-07-08 00:21:40 +02:00
df92cc5f73 Fix 2024-07-08 00:21:31 +02:00
03106cdee3 Release candidate 2024-07-07 18:45:38 +02:00
c4638daf1b Support building account v3 2024-07-07 18:45:14 +02:00
e2f5eb79ad Release candidate 2024-07-05 19:55:14 +02:00
b6c8e9bca0 Remove debug console log 2024-07-05 19:55:02 +02:00
573839019e Release candidate 2024-07-04 20:00:03 +02:00
815bf10ae0 Add line break 2024-07-04 19:59:28 +02:00
7c257d97a7 #577 2024-07-04 19:53:57 +02:00
59f8814660 Add missing patternfly image 2024-07-04 19:26:48 +02:00
1a6993099f Release candidate 2024-07-01 19:15:01 +02:00
f62ded3c8e Fix saml-post-form.ftl storybook 2024-07-01 19:15:01 +02:00
4eca6366cc Merge branch 'main' into keycloak_24 2024-06-30 07:13:20 +00:00
51a45b355d Remove script only used in CI 2024-06-29 19:41:36 +02:00
e5765cb902 Release candidate 2024-06-29 19:38:57 +02:00
6e922d2033 Merge pull request #575 from keycloakify/fix/keycloak_24-added-applications-story
Added & Fixed Applications Page Under Account
2024-06-29 17:36:55 +00:00
5d1695ada8 fix: added in applications.ftl story and fixed issue with double comma when realmRolesAvailable and resourceRolesAvailable both present 2024-06-28 16:36:56 -05:00
6e3ce29067 Relase candidate 2024-06-28 19:03:37 +02:00
2b9bbc4cef Ensure pnpm dlx isn't used 2024-06-28 19:03:19 +02:00
9557145f72 Bump version 2024-06-28 19:01:13 +02:00
249877b9c5 Better wording for assertNoPnpmDlx 2024-06-28 19:01:00 +02:00
ff2321fde5 Bump version 2024-06-28 18:51:06 +02:00
1edd6e4193 No pnpm dlx 2024-06-28 18:50:45 +02:00
c7d47f128e Release candidate 2024-06-28 07:16:39 +02:00
14cb07efb2 Make terms acceptance a required field on the Register page 2024-06-28 07:16:17 +02:00
a51724208c Release candidate 2024-06-28 06:47:02 +02:00
050e2b2b99 Improve Register page default stories 2024-06-28 06:46:26 +02:00
3706f15f7e Fix bug resolving user profile translations 2024-06-28 06:46:12 +02:00
bdde9162d9 Merge pull request #572 from keycloakify/fix/readme-update-discord
Fix/readme update discord
2024-06-25 16:38:31 -05:00
99b4933536 Merge branch 'main' into fix/readme-update-discord 2024-06-25 16:32:59 -05:00
c5caf7e0da fix: fix for previous discord readme addition, forgot the <a> tag 2024-06-25 16:32:21 -05:00
bcc5308cfb Merge pull request #571 from keycloakify/fix/readme-update-discord
Modified Readme.md to have a more visible discord invitation link
2024-06-25 21:29:50 +00:00
9fb902db5c fix: modified the readme using a slightly more visible discord invitation link 2024-06-25 16:27:22 -05:00
7461e38034 Release candidate 2024-06-25 22:51:21 +02:00
dccd85a151 Fix readExtraPage 2024-06-25 22:51:07 +02:00
910604fdad Use vite template by default 2024-06-25 22:50:51 +02:00
508cb9158e Release candidate 2024-06-24 03:58:55 +02:00
915c500d32 Feedback when running keycloakfy build 2024-06-24 03:58:42 +02:00
60bd6621c8 Release candidate 2024-06-24 02:43:03 +02:00
b5f6262763 Support cd in running build script in webpack 2024-06-24 02:42:44 +02:00
2b8c4422de Release candidate 2024-06-23 22:48:01 +02:00
a686432c65 Shell: true for windows 2024-06-23 22:47:45 +02:00
449e625877 Fix storybook build 2024-06-23 22:39:29 +02:00
1ac07dafde Release candidate 2024-06-23 21:23:44 +02:00
3878e28b56 Improve monorepo project support, work if there only a package.json at the root (like NX) 2024-06-23 21:23:06 +02:00
cf6bc8666b Include fsevents.node in npm bundle 2024-06-23 21:10:11 +02:00
f76063eb40 Make it easier to link to another starter 2024-06-23 20:54:08 +02:00
ed52c5824d Give immediate feedback if projectDirPath is wrong 2024-06-23 16:56:24 +02:00
9333400322 Remove unused buildContext prop 2024-06-23 02:07:34 +02:00
3689cfcc0d Consistency 2024-06-23 02:06:45 +02:00
b73eceb535 Release candidate 2024-06-23 00:46:01 +02:00
5dc3453fc9 Enable user profile in default keycloak 23 configuration 2024-06-23 00:45:26 +02:00
cef1139a4b Release candidate 2024-06-23 00:37:26 +02:00
ac96959947 Add missing fieldNames from synthetic user attributes 2024-06-23 00:37:06 +02:00
4d73d877ba move used defined exclusions down 2024-06-23 00:18:03 +02:00
9f1186302e Release candidate 2024-06-22 20:12:22 +02:00
319dcc0d15 Stable i18n messages across Keycloak versions 2024-06-22 20:12:02 +02:00
e99fdb8561 Log what file have changed when linking dynamically in starter 2024-06-22 20:11:34 +02:00
f37a342a63 Release candidate 2024-06-22 17:18:52 +02:00
09a039894d Remove React as peer dpendency so that Keycloakify can be more easily used in Vue and Angular projects 2024-06-22 17:18:08 +02:00
3efbb1a9fd Release candidate 2024-06-22 17:05:37 +02:00
920ee62ee3 Implement fallback to english for messages bundle provided via Keycloakify 2024-06-22 17:05:14 +02:00
1ace44fe31 Rename extraMessages -> messageBundle 2024-06-22 17:03:59 +02:00
a60f05415b Export fallback language tag ("en") as a constant 2024-06-22 17:03:44 +02:00
42c9d39e02 Release candidate 2024-06-22 17:01:48 +02:00
a8186f1ed9 Don't use tsafe directly in ejectable components 2024-06-22 17:01:45 +02:00
c2ff515a17 Enable termsText to be extended via local message bundle 2024-06-22 14:09:11 +02:00
960c3ba558 Release candidate 2024-06-22 02:53:51 +02:00
454a9cd01c Remove useDownloadTerms see: https://docs.keycloakify.dev/terms-and-conditions, remove react-markdown 2024-06-22 02:53:30 +02:00
7d42ce1c87 Release candidate 2024-06-21 22:07:50 +02:00
57f6f980cf Update terms storybook 2024-06-21 22:07:36 +02:00
8cba3aae2c Release candidate 2024-06-21 21:25:41 +02:00
01b32f78ed Allow to override termsText 2024-06-21 21:24:04 +02:00
b6066dfd5f Release candidate 2024-06-21 20:28:32 +02:00
3ad554ed59 #569 2024-06-21 20:28:14 +02:00
6aacc6361b Release candidate 2024-06-21 02:13:48 +02:00
638e4e6410 Set the terms to empty string when building 2024-06-21 02:13:31 +02:00
aa9b7cccc7 Rework Terms 2024-06-21 02:01:55 +02:00
41739c8528 Bump version 2024-06-20 04:28:33 +02:00
89b32dc7fc Fix wrong code snippet 2024-06-20 04:28:12 +02:00
44aec23251 Release candidate 2024-06-19 22:41:42 +02:00
12fd6160c5 Fix inline CSS in html 2024-06-19 22:41:25 +02:00
8819abc418 Release candidate 2024-06-19 03:56:13 +02:00
96b627095c https://github.com/adbayb/termost/pull/31 2024-06-19 03:52:57 +02:00
239f98aa9c fmt 2024-06-19 01:49:13 +00:00
f5d0511662 Update README.md 2024-06-19 03:36:00 +02:00
75582d2a26 Update README.md 2024-06-19 03:33:44 +02:00
dba004f924 Release candidate 2024-06-19 01:41:45 +02:00
5423a07c47 Patch CSS for Keycloak by using relative paths instead of css variables 2024-06-19 01:41:22 +02:00
aba725372e Release candidate 2024-06-18 22:41:08 +02:00
a61aa9dd5d Add missing fonts from the account theme's default assets 2024-06-18 16:41:09 +02:00
74349b20ce Adding missing font from default theme resources 2024-06-17 13:26:32 +02:00
09ab9a1c8f Fix storybook build 2024-06-17 13:03:39 +02:00
abfe5789a3 Publish new storybook 2024-06-17 12:53:06 +02:00
67ebac496d Release candidate 2024-06-17 00:07:53 +02:00
60a2bf173b Add missing base font face 2024-06-17 00:07:38 +02:00
4e03f07864 Do not includes all shared source, it's bundled already 2024-06-17 00:00:41 +02:00
aef1709d7f Release candidate 2024-06-16 18:27:37 +02:00
2f590f7be2 Add missing file in npm bundle 2024-06-16 18:27:18 +02:00
d5fa6ca89a Fix unit tests 2024-06-16 17:55:06 +02:00
8eaaffb25a Release candidate 2024-06-16 15:19:44 +02:00
28c5e2bab2 Rename use 'dist' instead of 'build' for basenameOfTheKeycloakifyResourcesDir 2024-06-16 15:19:27 +02:00
e212039f2c Release cadidate 2024-06-16 14:59:11 +02:00
99b0b67f77 Add publicDirpath option for webpack 2024-06-16 14:58:51 +02:00
6ec9ba3c01 Add version in build options 2024-06-16 14:53:18 +02:00
d7960a7dcf Release candidate 2024-06-16 14:05:38 +02:00
2a6e9af9c9 Enable to use an other directory than build/assets in webpack 2024-06-16 14:05:23 +02:00
327e4d1f90 Add doc link 2024-06-16 11:48:39 +02:00
fffadd7b9e Release candidate 2024-06-16 11:11:53 +02:00
aaaf0d2e77 Add missing declaration files 2024-06-16 11:11:35 +02:00
9f9a9b8c90 Release candidate 2024-06-16 02:30:09 +02:00
1f6edb3c0c Use the configured jar file basename if any 2024-06-16 02:19:56 +02:00
142efb4f99 Do leave artifact in the build directory when using start-keycloak 2024-06-16 01:41:47 +02:00
532655d2d5 Rename jarTargets -> keycloakVersionTargets 2024-06-16 01:34:06 +02:00
287edabd90 Enable to build only for specific keycloak version 2024-06-16 01:29:15 +02:00
7aaedbe2ce Release candidate 2024-06-15 17:40:51 +02:00
4cae1c673c Use getAlgorithmKey in account 2024-06-15 17:33:27 +02:00
8e01d836a9 Cherrypick what resource from the default theme we keep 2024-06-15 17:32:58 +02:00
f6dc8f0741 Memoize getImplementThemeTypes 2024-06-15 14:45:22 +02:00
3a976d08d2 Release candidate 2024-06-15 14:40:56 +02:00
50e83b1eb5 Only build for specific keycloak version in start-keycloak 2024-06-15 14:30:18 +02:00
61fbbb0b09 Refactor how we update META-INF and how we read what theme types are implemented 2024-06-15 14:23:35 +02:00
9e70e5c12e Suggest 'npm run' instead of 'yarn' to be more generic 2024-06-15 11:27:03 +02:00
69d9b64468 Use tsx instead of ts-node 2024-06-15 11:23:53 +02:00
0620d29880 spawn in shell in local scripts 2024-06-15 01:06:06 +02:00
b52dc74d9b Release candidate 2024-06-14 23:59:16 +02:00
a46aef2e7e Use shell for Window resolution of envs 2024-06-14 23:58:54 +02:00
736806a53d Relase candidate 2024-06-14 22:25:23 +02:00
f1475e5cdf Settle on calling the global 'kcContext' and reduce levels of indirections 2024-06-14 22:24:51 +02:00
d04724c70a fetchProxyOptions compatibility Window OS 2024-06-14 21:53:17 +02:00
bacaadc16d Remove dead file 2024-06-14 21:52:46 +02:00
c51dd235f0 Release candidate 2024-06-14 21:31:26 +02:00
92f2c9857e Fix the linking script 2024-06-14 21:31:03 +02:00
3998cc7f8b Fix for the linking script on windows OS 2024-06-14 20:45:52 +02:00
c126d080bc Make tests pass on windows OS 2024-06-14 19:06:48 +02:00
bc05f1714d Fix windows OS compatibility issue 2024-06-14 18:59:25 +02:00
e98becb94b Release candidate 2024-06-13 22:58:50 +02:00
250b94c8b5 Fix missing build option for webpack 2024-06-13 22:58:32 +02:00
47f03f6833 Improve stories 2024-06-13 00:47:18 +02:00
6e7ae48f78 Update sotry 2024-06-13 00:30:07 +02:00
526dbcc0e7 Improve stories 2024-06-12 23:22:21 +02:00
1abc5a5643 Release candidate 2024-06-12 23:11:46 +02:00
c81c350136 Improve mock and stories 2024-06-12 23:11:06 +02:00
f90dc8bc7e fix syntax error 2024-06-12 22:52:53 +02:00
072e22d072 Exclude kcContext.execution 2024-06-12 22:18:55 +02:00
59807c1bb0 Patch only required on the login page 2024-06-12 22:17:58 +02:00
7c19e1f1f7 Fix wrong condition for displaying error in the template 2024-06-12 21:38:48 +02:00
3b9f915f57 Fix logical error in generating pom file 2024-06-12 20:39:03 +02:00
d85cc530d4 remove debug log 2024-06-12 20:25:44 +02:00
2bb27c7642 More compact ftl output 2024-06-12 20:13:44 +02:00
e90e003204 Fully remove comments #542 2024-06-12 20:12:11 +02:00
b1e58e1add Refactor how userFromField is passed down to the client 2024-06-12 19:41:05 +02:00
0fd836314a Release candidate 2024-06-12 14:48:26 +02:00
0bc3f08cc1 Rename generateSrcMainResources -> generateResources 2024-06-12 14:48:08 +02:00
a78af5080a Fix environement variables all on the same line 2024-06-12 14:43:53 +02:00
074e465284 Release candidate 2024-06-12 12:02:13 +02:00
bc8165d0ae Fix usage of dirname instead of basename 2024-06-12 12:01:55 +02:00
ba8561d75a Release candidate 2024-06-12 10:50:13 +02:00
b2d381ba4b Apply the name of the theme in the preconfigured realm 2024-06-12 10:50:00 +02:00
d39353d332 Release candidate 2024-06-12 09:20:25 +02:00
ee916af48e Provide default message for the info page 2024-06-12 09:20:10 +02:00
da1dc0309b Release candidate 2024-06-12 08:57:59 +02:00
30f4e7d833 Add PasswordPolicies on every page where there's user profile 2024-06-12 08:57:40 +02:00
cf3a86fb9b Release candidate 2024-06-11 21:22:34 +02:00
e1633f43f4 Apply same strategy for UserProfileFormField than for TempateProps for extendability 2024-06-11 21:21:58 +02:00
5b64cfc23c Release candidate 2024-06-11 20:50:31 +02:00
19709cf085 Only types are capitalized 2024-06-11 20:50:11 +02:00
b8bb6c4f02 Fix build 2024-06-11 20:40:00 +02:00
b7a543f8cb Do not export PageProps in the index 2024-06-11 20:30:39 +02:00
04b4e19563 Release candidate 2024-06-11 20:27:53 +02:00
ffb27fc66d Extract Props from UserProfileFormFields so it's ejectable 2024-06-11 20:27:03 +02:00
8b5f7eefda Release candidate 2024-06-11 19:14:19 +02:00
c750bf4ee8 Export PageProps 2024-06-11 19:14:04 +02:00
aa74019ef6 Fix build 2024-06-11 19:08:36 +02:00
9be6d9f75f Release candidate 2024-06-11 17:27:40 +02:00
81ebb9b552 Prevent the jar to be corrupted when rebuild 2024-06-11 17:19:36 +02:00
5e13b8c41f Exclude Keycloak 22 from test panel 2024-06-11 17:12:12 +02:00
dd1ed948ec Update Keycloak 25 default realm config 2024-06-11 16:26:03 +02:00
8b93f701cf Add realms configurations for Keycloak majors 2024-06-11 16:19:54 +02:00
2f0084de5b Pass the input options translation to the kcContext 2024-06-11 16:10:54 +02:00
2ef9828625 Start with keycloak 18 for local container 2024-06-11 11:39:03 +02:00
89db8983a7 Fix exception in terms.ftl 2024-06-11 11:37:45 +02:00
287dd9bd31 Refactor + attributes with options rendered by default as select inputs 2024-06-11 09:22:50 +02:00
9a92054c1a Remove unused dependency 2024-06-10 21:06:02 +02:00
4189036213 Fix storybook 2024-06-10 21:05:17 +02:00
2c0a427ba5 Fix the script to export realm 2024-06-10 20:51:00 +02:00
77b488d624 Fix the formatNumber function 2024-06-10 20:14:14 +02:00
5249e05746 Release candidate 2024-06-10 19:36:11 +02:00
1e7a0dd7a6 Enable to add files to the jar with the post build options 2024-06-10 19:35:56 +02:00
fd67f2402a Release candidate 2024-06-10 17:30:20 +02:00
60a65ede2f Preserve ordering on user attributes 2024-06-10 17:30:00 +02:00
1fa659ce61 Release candidate 2024-06-10 16:01:56 +02:00
0ab903dbc7 Add new build target for Kc 25 https://github.com/p2-inc/keycloak-account-v1/pull/13 2024-06-10 15:29:08 +02:00
70b0a04793 Release candidate 2024-06-10 15:08:46 +02:00
c0df9aa939 Remove logs 2024-06-10 09:32:07 +02:00
60a1886942 Fix path error 2024-06-10 09:28:31 +02:00
1ebf97871b Fix logical error 2024-06-10 09:26:47 +02:00
72e321aa32 Fix update of the build process checkpoint 2024-06-10 09:24:16 +02:00
b0f602b565 Fix post build script 2024-06-10 09:12:24 +02:00
84c774503d Build rework checkpoint 2024-06-10 07:57:12 +02:00
9bbc7cc651 Release candidate 2024-06-09 15:04:47 +02:00
458083fb6d Prettier stable generated code 2024-06-09 15:04:31 +02:00
8dcfc840b4 Remove useless 'as const' 2024-06-09 14:34:41 +02:00
9d06a3a6ad Release candidate 2024-06-09 14:33:42 +02:00
86cd08b954 Add missing file to the NPM bundle 2024-06-09 14:33:29 +02:00
144c3cc082 Release candidate 2024-06-09 11:53:41 +02:00
802cef41a6 Rename KcApp to KcPage 2024-06-09 11:53:25 +02:00
e128e8f0a9 Release candidate 2024-06-09 11:25:05 +02:00
8a25b93ab2 Rename Fallback to DefaultPage 2024-06-09 11:24:50 +02:00
7a040935e9 i18n need to be passed as props if we want to be able to ovewrite 2024-06-09 11:20:45 +02:00
2015882688 Avoid loop rebuild in watch mode 2024-06-09 10:28:06 +02:00
379301eb9d Release candidate 2024-06-09 09:50:27 +02:00
5d86b05cdb Fix eject-page script 2024-06-09 09:50:02 +02:00
73c99d3157 Fix scripts 2024-06-09 09:39:16 +02:00
acba197c94 Release candidate 2024-06-09 09:36:31 +02:00
2441d8ed8a Fix tests 2024-06-09 09:36:16 +02:00
9c123f37c8 Make of doMakeUserConfirmPassword a prop of UserProfileFormFields 2024-06-09 09:34:39 +02:00
b48dbd99cf Enable to pass a path to a file for exclusions #525 2024-06-09 09:20:55 +02:00
25c8599d8f Rename BuildOptions -> BuildContext 2024-06-09 09:15:45 +02:00
3453a17c15 Rename reactAppRootDirPath -> projectDirPath and reactAppBuildDirPath -> projectBuildDirPath 2024-06-09 09:03:43 +02:00
6e95dacd3a Remove todo 2024-06-09 08:52:52 +02:00
a286e252e9 Enable to pass environement variables to test docker container 2024-06-09 08:50:59 +02:00
a8997e92c3 Improve cache strategy for getKcClsx 2024-06-09 08:37:23 +02:00
89137153a0 Remove isStorybook util 2024-06-09 08:37:02 +02:00
e3382de8e0 Fix boolean logic error 2024-06-09 08:30:57 +02:00
1a48681591 getClassName -> kcClsx 2024-06-09 08:27:07 +02:00
8f006f0009 Apply the new way i18n is implemented to every pages 2024-06-09 04:43:18 +02:00
77e32aad2a Make useGetClassName not a hook 2024-06-08 18:54:27 +02:00
8d365dae53 Refactor i18n, make component use the hook directly 2024-06-08 17:55:05 +02:00
01fb89674c Refactor i18n so that we don't have to wait for translations to be downloaded to render the page 2024-06-08 15:50:04 +02:00
e3144adc61 Release candidate 2024-06-08 14:21:20 +02:00
c9fb0ca6ae Rename extention types 2024-06-08 14:20:56 +02:00
82d7e1371e Add code gen for environement variables an theme name 2024-06-08 14:02:07 +02:00
e1341dfdba Release candidate 2024-06-08 07:17:29 +02:00
7f917311d8 Export ClassKey in the index instead of PageProps 2024-06-08 07:17:11 +02:00
2bfb856f07 Inconsistency fix 2024-06-08 04:10:04 +02:00
702f52f1c9 Only add the lang annotation if the lang is different from the current 2024-06-07 08:05:35 +02:00
7ba8649940 Release candidate 2024-06-07 08:03:28 +02:00
485ca28a29 Enable the lang of the term to be undefined 2024-06-07 08:03:13 +02:00
33460afaf2 Release candidate 2024-06-07 02:20:27 +02:00
2421ac2c11 Make the user return the actual language of the terms for accesibility 2024-06-07 02:20:12 +02:00
f0cdb0b80b Fix build 2024-06-06 09:31:27 +02:00
2af953927e Release candidate 2024-06-06 09:14:27 +02:00
dcb9fbd0f7 fixes for the add-story command 2024-06-06 09:13:58 +02:00
5bc1f6479d fmt 2024-06-06 09:13:13 +02:00
f3e4bca468 Add script to copy over the stories 2024-06-06 07:41:01 +02:00
54645f5cff Update storybook setup for portability 2024-06-06 07:28:34 +02:00
a7f3e00821 Remove createKcContextMock from the index 2024-06-06 06:27:28 +02:00
108c281b0c Enable to eject Template.tsx and UserProfileFormFields.tsx 2024-06-06 06:12:05 +02:00
58892cbb56 Change first level build target of bin 2024-06-06 06:11:34 +02:00
dae1053ca8 Consistency with the starter 2024-06-06 06:10:41 +02:00
83a9778c30 Release candidate 2024-06-06 04:36:16 +02:00
c52157bfb9 Remove dependency to powerhooks 2024-06-06 04:35:57 +02:00
62bf846d5f Release candidate 2024-06-06 02:29:25 +02:00
148f7fa316 Rollback unarrowing of the getKcContextMock return type 2024-06-06 02:29:09 +02:00
f488327885 Release candidate 2024-06-06 01:31:17 +02:00
593b929254 #525 2024-06-06 01:31:00 +02:00
9218e97315 Relase candidate 2024-06-06 00:33:51 +02:00
beb0e8bd77 Add missing translations 2024-06-06 00:33:34 +02:00
cace66e9f8 No longer need for a eslint rule ignore for with default vite starter 2024-06-05 23:30:28 +02:00
ef850c71fd Release candidate 2024-06-05 23:24:44 +02:00
aa8dc1919f Make getKcContext mock return type less narrow 2024-06-05 23:24:21 +02:00
c7c9b19853 Fix case error 2024-06-05 23:08:40 +02:00
68c26e0f5b Fix case issue 2024-06-05 22:55:09 +02:00
6bcdf286ef Add missing export 2024-06-05 22:53:14 +02:00
d9345396e8 Fix build 2024-06-05 22:48:13 +02:00
4c423900d4 Release candidate 2024-06-05 21:44:54 +02:00
504419b26d Fix storybook 2024-06-05 21:44:26 +02:00
6e058eafed It's less confusing to use "#" as urls in mock kcContext 2024-06-05 21:44:14 +02:00
08fc9d8631 Fix tests by updating vitest 2024-06-05 21:23:34 +02:00
e8a11991a0 Rename kcContext -> KcContext and improve consistency 2024-06-05 21:13:58 +02:00
3e6d679838 Release candidate 2024-06-05 18:50:26 +02:00
4dad859c4d Fix storybook 2024-06-05 18:50:00 +02:00
ef9c933ca8 Relase candidate 2024-06-05 18:42:50 +02:00
0461190a67 Do not export default the Fallback component 2024-06-05 18:42:32 +02:00
06b3211b08 Ease up the instentiation of i18n 2024-06-05 18:41:53 +02:00
2033a9ce0c Release candidate 2024-06-05 18:15:25 +02:00
fca18d9209 Add missing file to the NPM bundle 2024-06-05 18:15:13 +02:00
4f99088449 Release candidate 2024-06-05 06:11:18 +02:00
b1da684008 Re implement asset fetching 2024-06-05 06:10:11 +02:00
89fb6de2d5 Full ordering of stories 2024-06-05 06:09:42 +02:00
b665bae3bb Another improvement on storybook switching from one page to another 2024-06-05 01:32:31 +02:00
0b5a7544ca Address white falshes in storybook 2024-06-05 01:02:17 +02:00
183826ca0d Improve terms story 2024-06-04 04:06:29 +02:00
e507aace6b Change ordering of stories 2024-06-04 04:06:09 +02:00
43c93ef0b4 Update the intro story 2024-06-04 02:05:09 +02:00
093e51e092 Fix escaping error 2024-06-04 01:49:26 +02:00
17e1655eaf Fix recaptcha in storybook 2024-06-04 01:39:54 +02:00
6b570f2b9a Update register story 2024-06-03 23:54:08 +02:00
f239d105a7 Fix missing key 2024-06-03 23:53:53 +02:00
776d8378e3 Shorter white flash when changing stories 2024-06-03 23:40:33 +02:00
dd770cd7c6 Remove unessesary stories 2024-06-03 23:40:21 +02:00
4b3de54e18 Make it more conveignent to run storybook 2024-06-03 23:26:04 +02:00
5741cd1b2b Lower the priority of the without password story. 2024-06-03 23:25:37 +02:00
b780d7136e Fix mistake after using attributesByName instead of attributes 2024-06-03 23:25:02 +02:00
3c28a05746 Fix copy-keycloak-resources-to-public 2024-06-03 22:45:09 +02:00
57ac5badba Update the euristic for getting the NPM workspace root. 2024-06-03 22:37:22 +02:00
e873eb5123 Rollback typescript because updating storybook would add a one month delay to the release 2024-06-03 22:36:54 +02:00
c1a63edd71 Refactor kcContext, avoid having mocks in the dist https://github.com/keycloakify/keycloakify/discussions/299#discussioncomment-9616747 2024-06-03 18:28:34 +02:00
37a060c4db Change ordering of pages 2024-06-03 01:23:41 +02:00
157e4ac485 Add missing storybook pages 2024-06-03 01:23:28 +02:00
ba4d9675a8 More homogeneous storybook setup 2024-06-03 00:11:19 +02:00
e011fb094c Factorize parameters in storybook 2024-06-02 22:37:04 +02:00
f55a934939 Complete migration of storybook from @lordvlad #274 2024-06-02 22:29:53 +02:00
96a88fe865 Fix add remove button for multifield attributes 2024-06-02 00:31:08 +02:00
6cdb83d730 Fix the way we handle multivalued single fileld (multiselct, multiselect-checkboxes) 2024-06-02 00:24:07 +02:00
95f06df45d Extenalize some core logic from the ejectable component 2024-06-01 22:54:17 +02:00
ec52b357d5 Fix logical error with radibuttons 2024-05-30 23:23:16 +02:00
d84546cd7d Correct error validation password policy 2024-05-30 22:50:06 +02:00
4eee4156da Release candidate 2024-05-28 01:27:01 +02:00
70f475d13e Remove some more noise in the kcContext 2024-05-28 01:26:33 +02:00
3a50a61b12 First test against the key for faster ftl rendering 2024-05-28 01:08:02 +02:00
a217f617d8 Remove profile.attributesByName from the kcContext 2024-05-28 01:05:35 +02:00
fdfcd78f02 Watches more files that are relevent to the keycloak theme 2024-05-28 00:55:46 +02:00
56d6d8001a Fix #549 after test 2024-05-28 00:23:48 +02:00
c3ee8e10e6 Release candidate 2024-05-27 23:45:07 +02:00
2f42732deb #549 Done 2024-05-27 23:44:41 +02:00
956b8260e7 Release candidate 2024-05-27 18:33:48 +02:00
b7954f87e0 Fix error creating dir that might exist already 2024-05-27 18:33:16 +02:00
540ce55dc2 Release candidate 2024-05-27 17:23:08 +02:00
d71a2c98d1 Force initial build on keycloakify start 2024-05-27 17:22:55 +02:00
cb9cec676d Accomodate for Angular 2024-05-27 17:21:06 +02:00
9f2755bc7f Most of the work done for #549 2024-05-27 17:18:06 +02:00
fbe5a1f477 Remove dependency to react-markdown in the main bundle. 2024-05-27 01:09:49 +02:00
338642094d Remove dependency to evt in the component library 2024-05-27 00:12:51 +02:00
a3270d10f0 Release candidate 2024-05-26 22:34:01 +02:00
4c5924556a Re arange the output of start-keycloak 2024-05-26 19:55:59 +02:00
99a9b62c6c Fix logical error 2024-05-26 19:50:25 +02:00
1497672a4e Fix logical error 2024-05-26 19:40:13 +02:00
01161fd8ef up 2024-05-26 19:31:09 +02:00
68f5ee42e6 Make hot reloading when testing account theme with older Keycloak version work 2024-05-26 19:29:12 +02:00
53955a0713 Build the app when running npx keycloak start-keycloak 2024-05-26 17:58:41 +02:00
2271fd43b8 First step toward implementing #549 2024-05-26 16:21:04 +02:00
6a44cfb876 Release candidate 2024-05-26 12:42:28 +02:00
37c90d53e0 Fix tests 2024-05-26 12:42:13 +02:00
9a5ac5f13f Readability improvement 2024-05-26 12:38:00 +02:00
6603852355 Remove premature optimization 2024-05-26 12:24:35 +02:00
5670a71e6b Fix error with Set initialization 2024-05-26 12:19:47 +02:00
332b1f74d9 Enable to safely build jars with maven in parallel 2024-05-26 11:00:02 +02:00
c28caaa495 Fix hot reloading when testing in keycloak 2024-05-25 22:36:03 +02:00
74fed835e8 Remove web module from the account resources 2024-05-25 20:01:59 +02:00
6bb7f7dc16 Using POO do not increace performances 2024-05-25 12:30:21 +02:00
84bb2338d1 Filter out many of the unused keycloak resources 2024-05-25 12:19:03 +02:00
caa42538a1 Make sure the cache dont get corrupted if any operation is canceled mid way 2024-05-25 11:41:46 +02:00
f5b9a8de55 Node types def are lying 2024-05-25 11:02:22 +02:00
dd33f554da Resource fetching optimization 2024-05-24 17:26:38 +02:00
7e84d0b108 Just make sure compilations run don't overlap 2024-05-23 20:53:42 +02:00
9e1217fbf0 Use the default debounce of chokidar-cli 2024-05-20 21:58:47 +02:00
26d3c7f9e0 Better understanding of what's running 2024-05-20 21:47:06 +02:00
76542e6859 up 2024-05-20 21:10:58 +02:00
3c4bbf8aa7 Fix build and watch (hopefully for good this time) 2024-05-20 21:07:59 +02:00
ccc5ac6a1f Update prettier configuration 2024-05-20 19:30:15 +02:00
b34f86d2f0 Fix some bugs in start-keycloak 2024-05-20 19:30:04 +02:00
ee5f73519a Fix build script 2024-05-20 19:29:38 +02:00
22e7ff1424 Update prettier configuration 2024-05-20 15:50:58 +02:00
7a89888d11 Restore realm configuration 2024-05-20 15:34:07 +02:00
64fe15cf8c #554 2024-05-20 15:24:35 +02:00
336813646f Also watch the source files of Keycloakify for improving the experience of the maintainers 2024-05-20 10:16:38 +02:00
53a18c462a Move a wrong assertion down two lines 2024-05-20 10:08:00 +02:00
b893eee086 Don't trust the type system to much, we don't know for sure what the kcContext is actually like 2024-05-20 10:07:34 +02:00
792020dd18 Release candidate 2024-05-20 02:43:17 +02:00
0c11ba05af Fix auto re-build in start-keycloak 2024-05-20 02:42:57 +02:00
2cf82f510e Improve start-keycloak command 2024-05-20 02:27:40 +02:00
7c0cbe3a31 Rename function for consistency 2024-05-20 02:27:26 +02:00
08e659cf01 Better cli insight with download-keycloak-default-theme 2024-05-20 02:26:14 +02:00
97c3f4fa5f Fix build jar script 2024-05-20 02:25:45 +02:00
06a24d35cb https://github.com/keycloakify/keycloakify/issues/550 2024-05-19 23:17:45 +02:00
9b6d1a957f Release candidate 2024-05-19 10:46:42 +02:00
189bd4697a Rename scripts 2024-05-19 10:46:26 +02:00
d52252cd55 Fix typo 2024-05-19 10:35:02 +02:00
303bbc8431 Do not upload if admin token empty 2024-05-19 10:33:01 +02:00
727dc471c2 Better formatting for download-builtin-keycloak-theme 2024-05-19 10:32:38 +02:00
45b5c21ab5 Update eject-keycloak-page formatting 2024-05-19 10:23:38 +02:00
adddce7764 Release candidate 2024-05-19 09:52:44 +02:00
b35a9f8f61 Update keywords 2024-05-19 09:52:26 +02:00
34b46a9280 Add missing file in the NPM bundle 2024-05-19 09:51:41 +02:00
383a9953e2 Update enable_short_import_path script version 2024-05-19 09:51:16 +02:00
94bc7127fa Update action-gh-release to v2 2024-05-19 09:38:02 +02:00
289f0efd24 Release candidate 2024-05-19 09:33:20 +02:00
3f15586dae Update ci workflow 2024-05-19 09:33:02 +02:00
b8a33dabd4 Release candidate 2024-05-19 09:02:20 +02:00
3a4b44a83c Avoid breaking node retrocompatibility with dist patch 2024-05-19 09:02:04 +02:00
8c6303f016 Update action version 2024-05-19 08:57:45 +02:00
b71f3f559a Update ts-ci actions 2024-05-19 08:55:47 +02:00
6541460821 Release candidate 2024-05-19 08:51:36 +02:00
e63cd68a3e Patch deprecated Buffer API usage in vite-plugin 2024-05-19 08:51:13 +02:00
5602dc58ff Release candidate 2024-05-19 08:36:57 +02:00
8ed3561a55 Keep the bin executable when it's not bundled 2024-05-19 08:36:33 +02:00
4e72bc3a72 Update enable_short_import_path script 2024-05-19 08:35:54 +02:00
45c41ad321 Update ts-ci 2024-05-19 08:28:11 +02:00
14eccc761b Release candidate 2024-05-19 05:30:17 +02:00
6b24c4284f Update most dependencies 2024-05-19 05:29:58 +02:00
ba44de87d6 Ease testing local build in the starter 2024-05-19 05:29:20 +02:00
846181cfc7 Release candidate 2024-05-19 04:54:38 +02:00
2a8849dd11 We can now use ESM only packages in src/bin 2024-05-19 04:54:15 +02:00
5a879ece3c Release candidate 2024-05-19 04:45:43 +02:00
ffd734cc2d Patch ncc https://github.com/vercel/ncc/issues/484 2024-05-19 04:45:11 +02:00
ac414489ff Fix logical error 2024-05-19 04:44:38 +02:00
10da0cab47 https://github.com/adbayb/termost/issues/30 2024-05-19 04:26:01 +02:00
59f8119047 Do not manually bundle 2024-05-19 04:21:35 +02:00
a28a1531d9 https://github.com/adbayb/termost/pull/31 2024-05-19 04:03:03 +02:00
977d0dc761 Bundle bin 2024-05-19 04:02:36 +02:00
a0de1b38ce Release candidate 2024-05-18 11:40:26 +02:00
401f390e5b Improve loggin 2024-05-18 11:40:09 +02:00
e69febe0c0 Check if docker installed and running 2024-05-18 11:09:04 +02:00
eba4ddbbd8 Improve help formatting 2024-05-18 10:53:14 +02:00
49bb276c78 Rename start-keycloak-container -> start-keycloak 2024-05-18 10:48:47 +02:00
b48bccb706 Print a message if Maven not installed 2024-05-18 10:46:48 +02:00
a2160fc8ce Remove the --silent cli option 2024-05-18 10:26:56 +02:00
af829e9ac9 Improve login 2024-05-18 10:24:55 +02:00
2c5473da27 Colors in the terminal 2024-05-18 10:02:14 +02:00
e248a58201 Resolve XDG_CACHE_HOME relative to CWD 2024-05-18 09:18:40 +02:00
d85ab889b3 Support "~" in getAbsoluteAndInOsFormat 2024-05-18 09:15:39 +02:00
49eae307cd Create the cache dir path if not exist 2024-05-18 09:00:57 +02:00
a2563f0b7d Safely load command scripts 2024-05-18 08:56:11 +02:00
c8d2866ada Remove unessesary log 2024-05-18 08:12:34 +02:00
2f8d89012b Implement caching for promptKeycloakVersion 2024-05-18 08:11:20 +02:00
a3d9016cfe Remove debug log 2024-05-18 07:55:17 +02:00
24c14ea8f6 Improve log formatting 2024-05-18 07:53:41 +02:00
c93e787393 Fix docker command 2024-05-18 07:53:06 +02:00
b6cc3ee022 Create the .gitignore first 2024-05-18 04:48:04 +02:00
30e4112f79 Add missing period 2024-05-18 04:39:02 +02:00
067e148897 Improve helper text when inititializing email theme 2024-05-18 04:37:19 +02:00
7fc6f7a7ae Add context when prompting for the keycloak version number 2024-05-18 04:33:31 +02:00
60fcb5fe10 Update octokit 2024-05-18 04:24:02 +02:00
627ccd024c Release candidate 2024-05-18 04:18:15 +02:00
9fa1460400 Make it clear that the copy-keycloak-resources-to-public command is for CRA 2024-05-18 04:17:45 +02:00
c780e9b9f5 Greatly improve the DX with eject-keycloak-page 2024-05-18 04:14:36 +02:00
c2f15a569f Don't fail if the pages directory do not exist when ejecting 2024-05-18 03:41:12 +02:00
8311eaba1d Log cli errors https://github.com/adbayb/termost/issues/30 2024-05-18 03:35:55 +02:00
22aa48e343 Fail when wrong options https://github.com/adbayb/termost/issues/29 2024-05-18 03:12:07 +02:00
87cd37c467 Update watch script 2024-05-18 01:49:51 +02:00
a78a884c6e Update typescript version 2024-05-18 01:28:54 +02:00
0bfd73bcc6 Release candidate 2024-05-18 01:17:31 +02:00
113bb35a2b Remove legacy command 2024-05-18 01:17:31 +02:00
9b27f25f6c Implement stary-keycloak-container 2024-05-18 01:17:31 +02:00
e09acedf67 factorize access to META-INF/keycloak-themes.json 2024-05-18 01:17:31 +02:00
a80449333c Rename generateTheme to generateSrcMainResources 2024-05-18 01:17:31 +02:00
64f71d4265 Remove legacy branch from renovate 2024-05-18 01:17:31 +02:00
fb44700dd5 Don't over factorize 2024-05-18 01:17:31 +02:00
7d61be231e use cacheDirPath for temporary directories 2024-05-18 01:17:31 +02:00
f935922241 Stop passing as parameter the non variadic srcMainResource path 2024-05-18 01:17:31 +02:00
d5bb7679ca Make it clearer what's going on when calling post build script 2024-05-18 01:17:31 +02:00
b2a00737d3 Do not trickle down parameters as much 2024-05-18 01:17:31 +02:00
3cd3e08ede Introduce start-keycloak-container command 2024-05-18 01:17:31 +02:00
14fe55e5c4 Factorize getBuildOptions 2024-05-18 01:17:31 +02:00
8cf0f96401 improve error message 2024-05-18 01:17:31 +02:00
7da612c083 fix import error 2024-05-18 01:17:31 +02:00
afcc3fd0ce Move getNpmWorkspaceRootDirPath to tools 2024-05-18 01:17:31 +02:00
ab9e163105 Remove files that are no longer nessesary 2024-05-18 01:17:31 +02:00
63f9c815e0 Remove the need for creating a temporary vite.json file 2024-05-18 01:17:31 +02:00
6b7e5b6bc3 Only propose latest major in cli select 2024-05-18 01:17:31 +02:00
2ec22f07d5 Fallback to the build command https://github.com/adbayb/termost/issues/28 2024-05-18 01:17:31 +02:00
d4f03b6b9e bin general reresh, introducing termost 2024-05-18 01:17:31 +02:00
931e002b12 Release candidate 2024-05-18 01:17:31 +02:00
247f9b0c9d Fix error rending 2024-05-18 01:17:31 +02:00
0a72791022 Fix useUserProfileForm reducer 2024-05-18 01:17:31 +02:00
4fbb5f2023 Release candidate 2024-05-18 01:17:31 +02:00
7589b204fe Less verbose js coments (for start) #542 2024-05-18 01:17:31 +02:00
aa43abb544 Fix vim motion typo 2024-05-18 01:17:31 +02:00
85df0c2c6d Improve a little bit the readability of the rendered template 2024-05-18 01:17:31 +02:00
9bcfa58ec0 Fix error in ftl templat 2024-05-18 01:17:31 +02:00
11b2c6651d #545 2024-05-18 01:17:31 +02:00
6d57872e85 Update build scripts 2024-05-18 01:17:31 +02:00
8f98eff9b5 Remove debug file 2024-05-18 01:17:31 +02:00
e18a34c987 Update build script 2024-05-18 01:17:31 +02:00
19f1b4b14e Converts all functions without arguments at the same place in the ftl template 2024-05-18 01:17:31 +02:00
497f747d69 Forget to add displayRequiredFields on some pages 2024-05-18 01:17:31 +02:00
5e2debc695 Fix language menu select in templates 2024-05-18 01:17:31 +02:00
3ce571daab Remove --feature=declarative-user-profile from testing container launch script 2024-05-18 01:17:31 +02:00
1165477360 Fix inputs using value instead of defaultValue 2024-05-18 01:17:31 +02:00
05a223d5f0 Add missing mock value 2024-05-18 01:17:31 +02:00
0b21c04d82 Relase candidate 2024-05-18 01:17:31 +02:00
21454b9168 Pass totp.policy.getAlgorithmKey() to the freemarker template 2024-05-18 01:17:31 +02:00
699a3c8bf6 Fix path error in generate theme variant 2024-05-18 01:17:31 +02:00
f9fc0cda95 Release candidate 2024-05-18 01:17:31 +02:00
9249932a25 Fix build jar script 2024-05-18 01:17:31 +02:00
4f6e60683b Fix non closed tag 2024-05-18 01:17:31 +02:00
ad3cf3fab8 Fix several logical errors 2024-05-18 01:17:31 +02:00
a1b4ef10db Remove debug log 2024-05-18 01:17:31 +02:00
10fd863408 Update the exceptions 2024-05-18 01:17:31 +02:00
908ca9feda Better portability 2024-05-18 01:17:31 +02:00
4c4987ee7c Fix storybook build 2024-05-18 01:17:31 +02:00
bbe23b911a Release candidate 2024-05-18 01:17:26 +02:00
8d21425ae0 route the pages removed in kc 24 at low level 2024-05-18 01:15:58 +02:00
78517164d4 Done with multi target build 2024-05-18 01:15:58 +02:00
3ad694d4de Only buildJar function left to implement 2024-05-18 01:15:58 +02:00
824076f730 Implement generateThemeVariants 2024-05-18 01:15:58 +02:00
5a7d452429 Refactor 2024-05-18 01:15:58 +02:00
88fa63d848 Checkpoint 2024-05-18 01:15:58 +02:00
a26a813ad5 Checkpoint 2024-05-18 01:15:58 +02:00
385cb85309 Multi target build (checkpoint before futher refactor) 2024-05-18 01:15:58 +02:00
d4f5a1fff4 Remove retrocompatiblity before re-introducing it 2024-05-18 01:15:58 +02:00
771322aa97 Use Keycloak 24.0.4 assets (and use kc 24 in testing container) 2024-05-18 01:15:58 +02:00
f1177469c2 Update the getKcContext function 2024-05-18 01:15:58 +02:00
b122957ec0 Update account theme template 2024-05-18 01:15:58 +02:00
ec421e62ff More sensible mock date for UpdateEmail 2024-05-18 01:15:58 +02:00
e9f7f9d091 Provide mocks data for the new pages 2024-05-18 01:15:58 +02:00
cf39095351 Forgot to actually insert the script 2024-05-18 01:15:58 +02:00
94b618626d Add webauthn-error.ftl page 2024-05-18 01:15:58 +02:00
1e50427d62 Update saml-post-form.ftl page 2024-05-18 01:15:58 +02:00
73dfb7b91b Update the logout-confirm.ftl page 2024-05-18 01:15:58 +02:00
88aaa18a24 Add the login-x509-info.ftl page 2024-05-18 01:15:58 +02:00
3909e50d49 Update login-verify-email.ftl page 2024-05-18 01:15:58 +02:00
8683cf88fe Update login-username.ftl page 2024-05-18 01:15:58 +02:00
d5376b80c2 Update login-update-password.ftl page 2024-05-18 01:15:58 +02:00
3ec5aa84ad Update login-reset-password.ftl page 2024-05-18 01:15:58 +02:00
c80c399e6b Add login-reset-otp.ftl page 2024-05-18 01:15:58 +02:00
9001e254cc Add login-recovery-authn-code-input.ftl page 2024-05-18 01:15:58 +02:00
754b5be768 Fix type confusion 2024-05-18 01:15:58 +02:00
064f3b2041 Add login-recovery-authn-code-config.ftl page 2024-05-18 01:15:58 +02:00
c6874194a0 fmt 2024-05-18 01:15:58 +02:00
43092ce81a Fix ftl template not correctly parsing numbers 2024-05-18 01:15:58 +02:00
1abf0bb0d7 Update page login-password.ftl 2024-05-18 01:15:58 +02:00
1eb01ca9ff Update the login-page-expired.ftl page 2024-05-18 01:15:58 +02:00
324b1fed5d Update the login-otp.ftl page 2024-05-18 01:15:58 +02:00
ac238ff2b0 Rename src/login/pages/LoginDeviceVerifyUserCode.tsx to src/login/pages/LoginOauth2DeviceVerifyUserCode.tsx to respect naming convention 2024-05-18 01:15:58 +02:00
1d4cf2a446 Update login-oauth-grant.ftl page 2024-05-18 01:15:58 +02:00
89ddfa18b7 Update login-config-totp.ftl 2024-05-18 01:15:58 +02:00
7b60ab50b1 Update info.ftl 2024-05-18 01:15:58 +02:00
250d1d66dd Update idp-review-user-profile.ftl page 2024-05-18 01:15:58 +02:00
08cd62d924 add frontchannel-logout.ftl page 2024-05-18 01:15:58 +02:00
4f7a1c784f Remove the usePrepareTemplate hook 2024-05-18 01:15:58 +02:00
8d0d17910c Update error.ftl 2024-05-18 01:15:58 +02:00
34e1621b84 Add delete-account-confirm.ftl page 2024-05-18 01:15:58 +02:00
93d90d0ba6 Add code.ftl page 2024-05-18 01:15:58 +02:00
d5f3c789df Prevent multiple loading of the same script 2024-05-18 01:15:58 +02:00
652643f189 Add webauthn-register.ftl page 2024-05-18 01:15:58 +02:00
5cfb289736 update webauthn-autenticate.ftl 2024-05-18 01:15:58 +02:00
570fbd5632 Effort toward reconsiliating the server templating and the react world 2024-05-18 01:15:58 +02:00
88efe4a523 New mechanism for dynamically loading css and js (checkpoint) 2024-05-18 01:15:58 +02:00
a1db79ff47 Do not restrict to any perticular version of React 2024-05-18 01:15:58 +02:00
bac159a42c update evt 2024-05-18 01:15:58 +02:00
54b129630e Refactor terms 2024-05-18 01:15:58 +02:00
fdead071e7 Remove Register_legacy 2024-05-18 01:15:58 +02:00
0cfa8de0ad Update update-email.ftl page 2024-05-18 01:15:58 +02:00
9adfa2200a Update SelectAuthenticator.tsx 2024-05-18 01:15:58 +02:00
f5ab145906 Remove misleading comment 2024-05-18 01:15:58 +02:00
3eeba99152 Add delete-credential.ftl page 2024-05-18 01:15:58 +02:00
58dfd3c25c Factorise LoginUserProfile and LoginUpdateProfile 2024-05-18 01:15:58 +02:00
f97d33ffc1 Refactor and handle legacy login-update-profile.ftl 2024-05-18 01:15:58 +02:00
75212e643c Remove comment 2024-05-18 01:15:58 +02:00
22a0c9f401 Remove unused variable 2024-05-18 01:15:58 +02:00
7772550438 Login page overhaul 2024-05-18 01:15:58 +02:00
a887844a37 Load scripts after component rendered #470 2024-05-18 01:15:58 +02:00
b61f442a15 Update readFieldNameUsage for new messagePerField methods 2024-05-18 01:15:58 +02:00
0e20a26d6c Handle password field hide/reveal 2024-05-18 01:15:58 +02:00
b629af8dee Remove dead file 2024-05-18 01:15:58 +02:00
f0ffb3fc10 Fully retrocompatible, factorized Register page 🚀 2024-05-18 01:15:58 +02:00
96f0e6df2a File structure update 2024-05-18 01:15:58 +02:00
fb4a7d2ba3 Done with the new Register page (not yet retrocompatible) 2024-05-18 01:15:58 +02:00
0b0321474d Download terms when kcContext.termsAcceptanceRequired is set to true 2024-05-18 01:15:58 +02:00
a633423b72 Do not inject password field when password isn't required 2024-05-18 01:15:58 +02:00
f5781e8ee7 Add TermsAcceptance component 2024-05-18 01:15:58 +02:00
2c318cf64f Actually use the doUseDefaultCss param in useClassName 2024-05-18 01:15:58 +02:00
be330886da Complete UserProfileFormFields 2024-05-18 01:15:58 +02:00
73a39bedf5 Apply number unformat during validation if any 2024-05-18 01:15:58 +02:00
d04950cbc9 Load number unformat for pre form submission 2024-05-18 01:15:58 +02:00
b4d924adfa Almost done with UserProfileFormField.tsx 2024-05-18 01:15:58 +02:00
3f1316183d Good progress on UserProfileFormFields component 2024-05-18 01:15:58 +02:00
b17724fdda Done with select tag 2024-05-18 01:15:58 +02:00
41c2685dc4 Multivalued attributes that uses a single field have an inputType that starts with "multiselect" 2024-05-18 01:15:58 +02:00
b450e3db65 If required multivalued single file must have at least one value 2024-05-18 01:15:58 +02:00
352d2a7bc8 use valueOrValues to simplify type definitions 2024-05-18 01:15:58 +02:00
47f2bc9cd7 We have a polyfill for Array.every 2024-05-18 01:15:58 +02:00
2db0e8f68a Register form hook finally completed 2024-05-18 01:15:58 +02:00
f7d733b407 Checkpoint validation supporting various multi valued fields 2024-05-18 01:15:58 +02:00
4b78ef52e0 Progress checkpoint on useUserProfileForm 2024-05-18 01:15:58 +02:00
f42e6764b7 Checkpoint before refactor again 2024-05-18 01:15:58 +02:00
8f627aa382 Feature TextArea 2024-05-18 01:15:58 +02:00
9c6e3da304 Extract field errors into a separate component 2024-05-18 01:15:58 +02:00
319927e1dc Extract form group label into a separate component 2024-05-18 01:15:58 +02:00
4909928d3a Progress on form reactivity 2024-05-18 01:15:58 +02:00
423d031210 Simplify the API of useUserProfileForm 2024-05-18 01:15:58 +02:00
ab5269ddaf Start refactor of UserProfilesFormFields 2024-05-18 01:15:58 +02:00
96a6e81235 Implement password policy validation 2024-05-18 01:15:58 +02:00
6d8b0e0539 do not use custom validator to check if password confirmation matches password 2024-05-18 01:15:58 +02:00
f09ea971cf Dot not create fake attribute field, hide password confirm at an higher level 2024-05-18 01:15:58 +02:00
8030bf42ff Big refactor of useFormValidator into useUserProfileForm 2024-05-18 01:15:58 +02:00
008fa2b0c4 Add multivalued field validator 2024-05-18 01:15:58 +02:00
9040704659 Update KcContext type def, use an ext to get password policies. 2024-05-18 01:15:58 +02:00
7e793cabe8 Refactor useFormValidation 2024-05-18 01:15:58 +02:00
f1a0887e9b checkpoint update on useFormValidation 2024-05-18 01:15:58 +02:00
f6bdd92f9e Update Login template for Keycloak 24 2024-05-18 01:15:58 +02:00
a0367066b4 Feat polifill for getFirstError and make existsError accept more than one field (kcContext.messagePerField) 2024-05-18 01:15:58 +02:00
00651c0c3c Drop compat with Keycloak prior to v12 #359 2024-05-18 01:15:58 +02:00
a7a3ec711b Fully sync login template with Keycloak 24 2024-05-18 01:15:58 +02:00
de5bc82382 Update css classes keys to reflect Keycloak 24 2024-05-18 01:15:58 +02:00
138208bf82 Update prepare template for Keycloak 24 2024-05-18 01:15:58 +02:00
0796b3dedf Bump version 2024-05-17 17:54:23 +02:00
662a76bbb6 Merge pull request #553 from giorgoslytos/feat/federated-identity-account-page
Addition of Federated Identity Account page
2024-05-17 13:56:27 +00:00
a664195625 fix: Use of 'Meta' from storybook instead of 'ComponentMeta' 2024-05-17 12:17:17 +03:00
e533e127bf feat: Addition of stories for account pages that were missing them 2024-05-17 12:16:36 +03:00
346e3df009 feat: Addition of FederatedIdentity Account Page 2024-05-17 12:14:47 +03:00
19ba0873f5 fix: Prune 2024-05-16 17:11:45 +03:00
fb4acc62c4 fix: Addition of prop logoutUrl 2024-05-16 17:09:03 +03:00
fd538e95ca fix: Correction of import of useGetClassName 2024-05-16 14:05:58 +03:00
def2d8b75b fix: Correction of Application page 2024-05-16 14:05:28 +03:00
586b28af1c fix: Correction of types on KcContext for account pages 2024-05-16 12:53:31 +03:00
585c279d10 Bump version 2024-04-17 15:57:39 +02:00
51bc65e671 Avoid logging expected error messages to the console 2024-04-17 15:57:13 +02:00
ff1758cdce Update README.md 2024-04-13 04:50:34 +02:00
72a3c37e84 Update the downlaod-builtin-keycloak-theme for v24 2024-04-13 01:12:36 +02:00
c99cdf5566 Bump version 2024-04-10 03:33:46 +02:00
ad339710f1 Add corect document title and html lang property in account theme 2024-04-10 03:33:33 +02:00
00d2d12056 Bump version 2024-04-10 03:20:02 +02:00
6a7b472c0e #535 2024-04-10 03:19:46 +02:00
6991d868be Set html lang property and page title in usePrepareTemplate 2024-04-10 01:38:59 +02:00
85cc665d17 Bump version 2024-04-09 17:17:27 +02:00
5bb22fc345 #534 2024-04-09 17:17:12 +02:00
5417dc1bed Add notice about Keycloak 24 2024-04-09 14:42:43 +02:00
ee20d33724 Bump version 2024-03-19 14:57:47 +01:00
7887bd2b67 Filter out some Ftl context 2024-03-19 14:57:32 +01:00
168582efea Bump version 2024-03-16 05:42:50 +01:00
2c55d13f91 Add some more options for Keycloak version when downloading builtin theme (in the prompt) 2024-03-16 05:42:27 +01:00
23e5f553d4 #522 2024-03-16 05:34:55 +01:00
bc44eadcec Bump version 2024-03-05 19:43:06 +01:00
a3e3136600 #519 2024-03-05 19:42:49 +01:00
c7bfcee8d2 Update README.md 2024-03-04 21:46:00 +01:00
12ebd19716 Bump version 2024-03-02 18:45:21 +01:00
aec9ffa5db Merge pull request #515 from giorgoslytos/feat/additional-account-pages
feat: addition of extra account pages
2024-03-02 17:54:45 +01:00
2e6321342e Merge branch 'main' into feat/additional-account-pages 2024-03-02 13:41:48 +02:00
6e71da62f0 Bump version 2024-03-02 09:29:50 +01:00
5bf33aae75 Better support for environnement variables 2024-03-02 09:29:36 +01:00
06b2dc63ff Bump version 2024-03-02 09:02:17 +01:00
1bb0c9dfc2 Add generic type for kcContext.properties 2024-03-02 09:01:57 +01:00
a2b167e120 Bump version 2024-02-29 21:57:54 +01:00
0909a4b7cc Remove dead code, (I didn't fully understand how it landed here in the first place) 2024-02-29 21:57:37 +01:00
fd7d2bb9bf Bump version 2024-02-27 23:55:40 +01:00
63c40fd816 Add type definition for the user property in the kcContext of the terms.ftl page 2024-02-27 23:55:06 +01:00
0569fa5e58 Bump version 2024-02-27 23:32:07 +01:00
ba74952e0b #513 2024-02-27 23:19:55 +01:00
20c28f785a Bump version 2024-02-27 06:52:17 +01:00
e9b249ddc7 Fix ftl script bug and definitively address #512 and #432 2024-02-27 06:52:17 +01:00
604bb484a3 Update README.md 2024-02-25 21:02:31 +01:00
010c93793a Bump version 2024-02-24 05:38:59 +01:00
dc1d4a66f4 keycloakifyBuildDirPath was non absolute 2024-02-24 05:38:42 +01:00
8ef633d7ef Bump version 2024-02-24 05:09:08 +01:00
2176d33da1 Support generating source map 2024-02-24 05:08:39 +01:00
5b794e2d22 Bump version 2024-02-23 20:02:41 +01:00
ccd75d56c5 Run the post build script in the react app directory 2024-02-23 19:38:01 +01:00
b700066833 Release candidate 2024-02-23 19:22:28 +01:00
546ee006d3 Rename postBuildScript to postBuild, make the params of the Vite plugin optional 2024-02-23 19:22:14 +01:00
7f333a6a36 Release candidate 2024-02-23 19:16:57 +01:00
ae757ee371 Enable to provide the configuration to the Vite plugin, enable user to provide a post build script #148 2024-02-23 19:16:53 +01:00
69936750d5 Bump version 2024-02-21 20:43:34 +01:00
442bfa4ed6 Update types to reflect what is actually there on the kcContext 2024-02-21 20:43:18 +01:00
79e25e69bb feat: addition of Log account page 2024-02-21 13:44:33 +02:00
b95c12772d style: Add spaces between words 2024-02-21 13:36:55 +02:00
de47525d7c feat: Addition of Application Account page 2024-02-19 17:29:46 +02:00
f49d20e47c feat: Totp account page fixed and completed 2024-02-19 11:57:03 +02:00
33b9917229 fix: locales in account totp page 2024-02-19 08:58:27 +02:00
2a88e6802f Bump version 2024-02-18 11:28:29 +01:00
bcc8b12e13 Enable to statically build storybook in Vite project 2024-02-18 11:28:17 +01:00
9b974505eb Update ci 2024-02-17 04:15:31 +01:00
29b1c26771 Bump version 2024-02-17 03:47:03 +01:00
02db20d98b Enable to release on v8 branch 2024-02-17 03:47:03 +01:00
757354df7d Follow up on #406 2024-02-17 03:47:03 +01:00
319d7dbe94 feat: Addition of Totp account page 2024-02-16 17:40:12 +02:00
feb8eaf95a Merge branch 'keycloakify:main' into additional-account-pages 2024-02-13 22:27:25 +02:00
563518cf46 Remove poll 2024-02-13 15:07:14 +01:00
7c42d9082a Remove broken badge 2024-02-13 04:47:14 +01:00
040284af71 Reference Vite doc 2024-02-13 04:46:36 +01:00
34f64184d9 Bump version 2024-02-13 01:33:31 +01:00
b9abd74156 Create a .gitignore that matches all in the build_keycloak directory 2024-02-13 01:33:15 +01:00
a1c0bfda6c Bump version 2024-02-13 01:13:26 +01:00
617dcef09d Merge pull request #499 from keycloakify/vite
Vite
2024-02-13 01:04:54 +01:00
d9c406800a Fix tests 2024-02-12 23:57:21 +01:00
54b869def1 Release candidate 2024-02-12 23:43:21 +01:00
d80a583979 Better test for isStorybook 2024-02-12 23:42:31 +01:00
99bfd7379b Release candidate 2024-02-12 13:43:34 +01:00
5f257382fa Prevent accumulation of assets in build dir 2024-02-12 13:43:12 +01:00
e3e6847c82 Prevent users from having to use any 2024-02-12 13:27:07 +01:00
4ee0823acb Bump version 2024-02-12 01:47:21 +01:00
d466123b1c Use Keycloak 23.0.6 2024-02-12 01:41:08 +01:00
21cbc14a48 Release candidate 2024-02-12 01:34:54 +01:00
b2f2c3e386 Disalow releative basename in vite config 2024-02-12 01:34:34 +01:00
b03340ed10 Do not dynamically import "en" to make vite happy 2024-02-12 00:33:12 +01:00
5b563d8e9b Improve privacy on the buildinfo file that might be served 2024-02-12 00:32:18 +01:00
2790487fc7 Release candidate 2024-02-11 23:59:23 +01:00
ad5a368065 Run vite configResolved in dev mode as well 2024-02-11 23:59:10 +01:00
7c0a631a9a Fix crash in vite-plugin 2024-02-11 23:54:28 +01:00
4a8920749a release candidate 2024-02-11 23:48:09 +01:00
8ab118dd06 Remove path-browserify dependency 2024-02-11 23:47:58 +01:00
e6661cb898 Fix build 2024-02-11 23:22:37 +01:00
1671850714 Release candidate 2024-02-11 20:21:05 +01:00
d568bafe04 Remove unessesary file 2024-02-11 20:20:52 +01:00
43dcce8478 Add cache to getNpmWorkspaceRootDirPath 2024-02-11 20:20:38 +01:00
ad70a4cffd Make Vite run copy-keaycloak-resources-to-public 2024-02-11 20:15:18 +01:00
6d4a948dd8 minor refactor 2024-02-11 19:25:52 +01:00
839ba6a964 Improve monorepo support 2024-02-11 18:28:58 +01:00
b5cfdb9d0a Store vite plugin output in cache dir path 2024-02-11 16:17:58 +01:00
9706338182 Release candidate 2024-02-11 12:05:05 +01:00
05f52c3d23 Remove unessesary log 2024-02-11 12:04:50 +01:00
df3acb6932 Release candidate 2024-02-11 10:38:12 +01:00
b3c242595e Fix PUBLIC_URL typing 2024-02-11 10:37:22 +01:00
26985f8d81 Release candidate 2024-02-11 00:21:21 +01:00
05e5e4efec When is storybook, don't print mock related warning in console 2024-02-11 00:21:04 +01:00
e88be30fc8 Merge branch 'keycloakify:main' into additional-account-pages 2024-02-10 22:50:42 +02:00
4d67f16e94 Release candidate 2024-02-10 20:40:59 +01:00
334ec1870a Throw if unrecognized bundler when getting BASE_URL for mocks 2024-02-10 20:40:41 +01:00
ef5e4fccd3 Replace the common asset path url in the ftl (upstream) to help pepole figure out what's going on 2024-02-10 20:04:08 +01:00
8535edcfd4 Relase candidate 2024-02-10 19:51:39 +01:00
bda76200d7 Base url that works everywhere in mocks 2024-02-10 19:51:24 +01:00
db0dc96cc7 Release candidate 2024-02-09 17:56:57 +01:00
6d62b5a150 Don't replace process.env.PUBLIC_URL 2024-02-09 17:56:28 +01:00
217439d673 Export polyfill of process.env.PUBLIC_URL 2024-02-09 17:52:26 +01:00
1f79a8f7dc Deprecate keycloakJsAdapter 2024-02-09 17:52:03 +01:00
7596786b18 Release candidate 2024-02-08 23:37:14 +01:00
2540b06c94 Tweak the resources of the default theme that are kept 2024-02-08 23:36:52 +01:00
43eeaf3002 Remove misleading comment in start-keycloak-container script 2024-02-08 14:41:07 +01:00
037cd150de Release candidate 2024-02-08 14:10:10 +01:00
ae0b059217 Fix a couple of bug in donwnload-builtin-keycloak-theme 2024-02-08 14:09:55 +01:00
8255ce1158 Release candidate 2024-02-08 00:56:55 +01:00
5bf905723c Feature -p or --project option for ease of use in monorepo setups #449 2024-02-08 00:56:33 +01:00
3e336f4937 Release candidate 2024-02-08 00:12:50 +01:00
cd1cc37916 Explicitely exclude a number of node deps from Keycloak commons for bundle size 2024-02-08 00:12:10 +01:00
4ad7183d7e Prevent crashing when github is not up to date yet 2024-02-07 21:22:58 +01:00
e1b52e7439 Implement remote caching mechanism to prevent full download of the Keycloak sources 2024-02-07 21:17:09 +01:00
dca8c9f9d7 Merge main, fix runtime error in scripts, fix clean build 2024-02-07 20:01:26 +01:00
22496e36eb feat: Addition of Sessions page 2024-02-07 15:18:27 +02:00
7e4eba6376 Update README, add poll 2024-02-07 10:29:46 +01:00
f642a56eaa Release candidate 2024-02-06 08:00:07 +01:00
c091089830 Apply #502 from main 2024-02-06 07:59:21 +01:00
18900d20e1 Update All contributors 2024-02-06 07:56:06 +01:00
6c622b1580 Remove TODO comment 2024-02-06 07:55:32 +01:00
4290cd23b2 Remove what seems to be dead code (not used in the starter nor in onyxia, besite, looks fishy) '//TODO: Write a test case for this' for ref and .chunk.css", 2024-02-06 07:29:48 +01:00
5076c1e93f Unit test passing 2024-02-06 07:28:03 +01:00
884b701fc6 Removing keycloak-resources dir from dist dir after build 2024-02-05 09:26:54 +01:00
73a8ec0295 Building version 2024-02-05 08:52:58 +01:00
a29b6097a4 Reintroduce doBuildRetrocompatAccountTheme (for now) 2024-02-04 10:25:48 +01:00
a9231e2ed8 Bump version 2024-02-04 06:31:10 +01:00
5f4669a7a6 Merge pull request #502 from giorgoslytos/fix/login-otp-radio-inputs 2024-02-03 10:53:58 +01:00
75c54df109 Fix some errors in base account v1 theme 2024-02-03 08:30:06 +01:00
2a07f7151d Merge pull request #503 from keycloakify/all-contributors/add-giorgoslytos
docs: add giorgoslytos as a contributor for code
2024-02-03 08:13:16 +01:00
b6ecff2dd3 docs: update .all-contributorsrc [skip ci] 2024-02-03 07:11:46 +00:00
83df27ec99 docs: update README.md [skip ci] 2024-02-03 07:11:45 +00:00
ca255985c0 fix: radio inputs on login-otp page 2024-02-02 14:35:14 +02:00
82f34c38f6 Bump version 2024-02-01 06:22:54 +01:00
694b4c8027 Reintroduce doBuildRetrocompatAccountTheme (for now) and fix multiple things in the account default theme 2024-02-01 06:22:33 +01:00
bd25621b2c Remove dead code 2024-01-31 21:56:46 +01:00
fde34be270 Compiling build 2024-01-30 07:10:53 +01:00
7c7ce159fe Complete build option 2024-01-30 06:55:26 +01:00
5a57bb59e5 Refactor 2024-01-30 06:38:26 +01:00
cd278f4ab5 Refactor 2024-01-30 06:37:49 +01:00
8b24e23721 refactor 2024-01-30 06:04:05 +01:00
22fa1411bf Moving on 2024-01-30 05:54:36 +01:00
2799a52d0c Implement router for js remplacer depending of the bundle (vite or webpack) 2024-01-30 01:24:44 +01:00
4c2e01a7a8 Done with implementation of webpack js replacer test case for support of non root base 2024-01-30 00:56:47 +01:00
7267d2ef38 Add a test case for the code that enable the import to work in webpack if base isn't / 2024-01-30 00:37:30 +01:00
1eb6b154f7 Rearenge test case 2024-01-30 00:15:08 +01:00
f55d61bf0b Rename test case file 2024-01-30 00:10:59 +01:00
5b350274bd Fundation 2024-01-30 00:06:17 +01:00
b6d2f9f691 Disable tests for now 2024-01-27 19:00:45 +01:00
81106b5deb Release candidate 2024-01-27 18:51:05 +01:00
66e595e649 Vite investigations 2024-01-27 18:49:29 +01:00
33b7bb6184 Bump version 2024-01-26 04:09:54 +01:00
7d9130b2af Remove the doBuildRetrocompatAccountTheme and the retrocompat_ option in dropdown 2024-01-26 03:39:48 +01:00
482d71743b Bump version 2024-01-26 02:03:06 +01:00
1db37a4727 Do not include retrocompat_ in META-INF when using doBuildRetrocompatAccountTheme false 2024-01-26 02:02:45 +01:00
194d16ff91 Bump version 2024-01-26 00:56:45 +01:00
b1e2284c0e Fix condition for displaying info in login page 2024-01-26 00:55:50 +01:00
70d1aa70a3 Bump version 2024-01-18 18:09:59 +01:00
3b17d6e0ab Merge pull request #496 from keycloakify/all-contributors/add-Moulyy
docs: add Moulyy as a contributor for code
2024-01-18 18:09:10 +01:00
9a5819b93b docs: update .all-contributorsrc [skip ci] 2024-01-18 16:45:36 +00:00
a260cd67b0 docs: update README.md [skip ci] 2024-01-18 16:45:35 +00:00
64111fb0ec Merge pull request #495 from Moulyy/fix/add_password_visibility_classkey
Add "kcInputGroup" in ClassKey type definition and useGetClassName
2024-01-18 17:43:48 +01:00
faf2be23d9 Add "kcInputGroup" in ClassKey type definition and the patternfly class associated in useGetClassName 2024-01-18 17:31:16 +01:00
0eb4a6a315 Merge pull request #494 from alexted/patch-1
Update README.md
2024-01-18 16:59:17 +01:00
85673250ed Update README.md
extra "v" deleted  in several places of changelog highlights
2024-01-18 17:48:10 +02:00
09daa741ce Install pnpm if needed 2024-01-18 01:02:14 +01:00
55e2379aab Bump version 2024-01-18 00:57:02 +01:00
9937977203 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-18 00:56:26 +01:00
c897e7491a Use message of Keycloak version 23.0.4 #493 2024-01-18 00:54:09 +01:00
0a74a95283 Bump version 2024-01-15 11:02:00 +01:00
74ef2c3dff Merge pull request #489 from law108000/main
refactor: point the import statement to keycloakify instead of internal
2024-01-15 11:01:14 +01:00
9976dfacc0 Merge pull request #491 from keycloakify/all-contributors/add-law108000
docs: add law108000 as a contributor for code
2024-01-15 10:59:17 +01:00
659f8ddc7a docs: update .all-contributorsrc [skip ci] 2024-01-15 09:56:30 +00:00
9e4cc2ae57 docs: update README.md [skip ci] 2024-01-15 09:56:29 +00:00
a27d78fcdf refactor: point the import statement to keycloakify instead of internal
align import statement with other page templates
2024-01-15 14:03:01 +08:00
e507435bcb Bump version 2024-01-10 13:01:23 +01:00
d5f234909f Merge branch 'main' of https://github.com/keycloakify/keycloakify 2024-01-10 12:43:11 +01:00
c17f721625 Bump version 2024-01-10 12:43:04 +01:00
600705130f #483: Enable the Template prop not to be lazy 2024-01-10 12:42:48 +01:00
5c5dce1422 Bump version 2023-12-18 13:19:54 +01:00
53585bf2f0 Merge pull request #476 from BlackVoid/feature/document-title
Fixes #473: Set document title using the "loginTitle" translation
2023-12-18 13:18:51 +01:00
116f88a503 Fixes #473: Set document title using the "loginTitle" translation 2023-12-18 11:56:37 +01:00
aaba8cd2c7 Bump version 2023-12-14 15:34:13 +01:00
b67aeb0d3a Fix tests #471 2023-12-14 15:33:57 +01:00
f620562d68 docs: update .all-contributorsrc [skip ci] 2023-12-14 15:09:10 +01:00
5231d0eaa1 docs: update README.md [skip ci] 2023-12-14 15:09:09 +01:00
cb470e3573 Handle CSS with multiple urls 2023-12-12 14:51:09 +00:00
0a0f90aa2e Bump version 2023-12-04 14:10:25 +01:00
635207d12c Generate a README alongside the jars to indicate why there's two jar file 2023-12-04 14:10:10 +01:00
5e4a829413 Bump version 2023-12-01 00:54:36 +01:00
b13b3fd92e Mention the nessesity to use retrocompat- with older Keycloak versions 2023-12-01 00:54:19 +01:00
564dc8e6f1 Bump version 2023-12-01 00:03:58 +01:00
6e4cced8c6 Rename original- to retrocompat- 2023-12-01 00:03:44 +01:00
29a4a5027c Bump version 2023-11-30 22:37:13 +01:00
ee327448b4 Delete original- jar 2023-11-30 22:36:58 +01:00
d078960c5c Bump version 2023-11-30 18:53:17 +01:00
2e8cd375fc Merge pull request #462 from BlackVoid/fix/client-attributes
Fixes #460: Fixes KcContext to contain attributes for client object
2023-11-30 18:52:26 +01:00
1f6751cb01 Fixes #460: Fixes KcContext to contain attributes for client object 2023-11-30 17:17:58 +01:00
3cca4e31cd Merge pull request #463 from keycloakify/all-contributors/add-BlackVoid
docs: add BlackVoid as a contributor for code
2023-11-30 17:10:29 +01:00
b93902800c docs: update .all-contributorsrc [skip ci] 2023-11-30 16:06:55 +00:00
70f6bb3fda docs: update README.md [skip ci] 2023-11-30 16:06:54 +00:00
c075cb6311 Merge pull request #461 from BlackVoid/fix/template
Fixes #459: Use Template from props
2023-11-30 17:03:36 +01:00
d7db85b062 Fixes #459: Use Template from props 2023-11-30 16:29:53 +01:00
b442e7d958 Merge pull request #457 from keycloakify/all-contributors/add-xgp
docs: add xgp as a contributor for code
2023-11-28 13:06:04 +01:00
a495ae637f docs: update .all-contributorsrc [skip ci] 2023-11-28 12:05:49 +00:00
94748a96a9 docs: update README.md [skip ci] 2023-11-28 12:05:48 +00:00
7657429054 Release v9 2023-11-28 12:53:37 +01:00
2ff6dbf975 Update README 2023-11-28 12:53:10 +01:00
4f34628c14 Merge pull request #414 from keycloakify/v9
v9: Support Keycloak 22 and decoupling account / login builtin resources
2023-11-28 12:18:07 +01:00
6ff2111cee #389 https://github.com/p2-inc/keycloak-account-v1/issues/3 2023-11-28 12:17:16 +01:00
85957980f6 update CI 2023-11-26 16:24:57 +01:00
a6dcfe2c87 Release candidate 2023-11-26 16:14:25 +01:00
c32d590fbb #389 https://github.com/xgp/keycloak-account-v1/issues/3 2023-11-26 16:10:34 +01:00
ab41462f71 Actually resolve conflict with main 2023-11-26 15:07:27 +01:00
951f16b1a5 Merge pull request #456 from keycloakify/v9_tmp
Rebase v9 to main
2023-11-26 14:46:28 +01:00
b5818888bb Merge branch 'v9' into v9_tmp 2023-11-26 14:45:23 +01:00
e06ef01f72 Merge branch 'main' into v9 2023-09-22 15:52:23 +02:00
7de54a2cc4 Fix tests 2023-09-04 03:26:30 +02:00
c788b8cc82 Release candidate 2023-09-04 02:49:58 +02:00
cb8db1a541 Fix build 2023-09-04 02:49:32 +02:00
8a7a551c3b Fix mock path error in account 2023-09-04 02:38:19 +02:00
84d180b810 Fix bug with asset paths 2023-09-04 02:34:10 +02:00
de261a27ca Do not display that the jar have been created if we don't create it. 2023-09-04 02:29:16 +02:00
28288a8f7b Build retrocompatible account theme 2023-09-04 02:16:55 +02:00
cd8548fc32 Remove extraThemeNames option in favor of extending themeName to accept array 2023-09-04 01:19:21 +02:00
37dbd49589 Rename extraThemeNames to themeVariantNames 2023-09-04 00:53:57 +02:00
5af8d67b62 Refactor and update docker script 2023-09-04 00:25:36 +02:00
72e6309c4a Fix warning 2023-09-03 23:32:21 +02:00
18f0f3cce1 Refactor build option managment 2023-09-03 23:26:34 +02:00
8c3e9ff192 Remove inhouse bundler, we actually need Maven to build now 2023-09-03 21:10:20 +02:00
21d6d27435 Rename build option, update readme 2023-09-03 21:02:51 +02:00
39ff7913d6 https://github.com/xgp/keycloak-account-v1/issues/3 2023-09-03 07:14:57 +02:00
420 changed files with 54915 additions and 13462 deletions

View File

@ -186,6 +186,183 @@
"contributions": [
"code"
]
},
{
"login": "xgp",
"name": "Garth",
"avatar_url": "https://avatars.githubusercontent.com/u/244253?v=4",
"profile": "https://github.com/xgp",
"contributions": [
"code"
]
},
{
"login": "BlackVoid",
"name": "Felix Gustavsson",
"avatar_url": "https://avatars.githubusercontent.com/u/673720?v=4",
"profile": "https://github.com/BlackVoid",
"contributions": [
"code"
]
},
{
"login": "msiemens",
"name": "Markus Siemens",
"avatar_url": "https://avatars.githubusercontent.com/u/1873922?v=4",
"profile": "https://m-siemens.de/",
"contributions": [
"code"
]
},
{
"login": "law108000",
"name": "Rlok",
"avatar_url": "https://avatars.githubusercontent.com/u/8112024?v=4",
"profile": "https://github.com/law108000",
"contributions": [
"code"
]
},
{
"login": "Moulyy",
"name": "Moulyy",
"avatar_url": "https://avatars.githubusercontent.com/u/115405804?v=4",
"profile": "https://github.com/Moulyy",
"contributions": [
"code"
]
},
{
"login": "madmadson",
"name": "Tobias Matt",
"avatar_url": "https://avatars.githubusercontent.com/u/798831?v=4",
"profile": "https://github.com/madmadson",
"contributions": [
"code"
]
},
{
"login": "oliviergoulet5",
"name": "Olivier Goulet",
"avatar_url": "https://avatars.githubusercontent.com/u/17685861?v=4",
"profile": "https://github.com/oliviergoulet5",
"contributions": [
"code"
]
},
{
"login": "liamlows",
"name": "Liam Lowsley-Williams",
"avatar_url": "https://avatars.githubusercontent.com/u/1365914?v=4",
"profile": "https://github.com/liamlows",
"contributions": [
"code",
"doc"
]
},
{
"login": "uchar",
"name": "Omid",
"avatar_url": "https://avatars.githubusercontent.com/u/5172296?v=4",
"profile": "https://www.linkedin.com/in/oes-rioniz/",
"contributions": [
"test",
"code"
]
},
{
"login": "kathari00",
"name": "Katharina Eiserfey",
"avatar_url": "https://avatars.githubusercontent.com/u/42547712?v=4",
"profile": "https://github.com/kathari00",
"contributions": [
"code",
"test",
"doc"
]
},
{
"login": "luca-peruzzo",
"name": "Luca Peruzzo",
"avatar_url": "https://avatars.githubusercontent.com/u/69015314?v=4",
"profile": "https://github.com/luca-peruzzo",
"contributions": [
"code",
"test"
]
},
{
"login": "nima70",
"name": "Nima Shokouhfar",
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
"profile": "https://github.com/nima70",
"contributions": [
"code",
"test"
]
},
{
"login": "marvinruder",
"name": "Marvin A. Ruder",
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
"profile": "https://mruder.dev",
"contributions": [
"bug"
]
},
{
"login": "zvn2060",
"name": "HI_OuO",
"avatar_url": "https://avatars.githubusercontent.com/u/45450852?v=4",
"profile": "https://github.com/zvn2060",
"contributions": [
"code"
]
},
{
"login": "tripheo0412",
"name": "Tri Hoang",
"avatar_url": "https://avatars.githubusercontent.com/u/25382052?v=4",
"profile": "https://github.com/tripheo0412",
"contributions": [
"doc"
]
},
{
"login": "EternalSide",
"name": "Lesha",
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
"profile": "http://t.me/AAT_L",
"contributions": [
"code"
]
},
{
"login": "bacongobbler",
"name": "Matthew Fisher",
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
"profile": "https://blog.bacongobbler.com",
"contributions": [
"doc"
]
},
{
"login": "kodebach",
"name": "Klemens Böswirth",
"avatar_url": "https://avatars.githubusercontent.com/u/23529132?v=4",
"profile": "https://github.com/kodebach",
"contributions": [
"code"
]
},
{
"login": "wnmzzzz",
"name": "wnmzzzz",
"avatar_url": "https://avatars.githubusercontent.com/u/117174301?v=4",
"profile": "https://github.com/wnmzzzz",
"contributions": [
"test"
]
}
],
"contributorsPerLine": 7,

View File

@ -1,4 +1,3 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

25
.github/release.yaml vendored
View File

@ -1,25 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes 🛠
labels:
- breaking
- title: Exciting New Features 🎉
labels:
- feature
- title: Fixes 🔧
labels:
- fix
- title: Documentation 🔧
labels:
- docs
- title: CI 👷
labels:
- ci
- title: Other Changes
labels:
- '*'

View File

@ -3,7 +3,6 @@ on:
push:
branches:
- main
- v*
pull_request:
branches:
- main
@ -14,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again.
run: yarn format:check
- name: If this step fails run 'npm run format' then commit again.
run: npm run _format --list-different
test:
runs-on: ${{ matrix.os }}
needs: test_lint
@ -28,30 +27,29 @@ jobs:
os: [ ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- run: yarn build
- run: yarn test
#- run: yarn test:keycloakify-starter
- run: npm run build
- run: npm run test
storybook:
runs-on: ubuntu-latest
if: github.event_name == 'push'
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '18'
- uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook
- run: npm run build-storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>"
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./storybook-static -u "github-actions-bot <actions@github.com>"
check_if_version_upgraded:
name: Check if version upgrade
@ -71,7 +69,7 @@ jobs:
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
steps:
- uses: garronej/ts-ci@v2.1.0
- uses: garronej/ts-ci@v2.1.2
id: step1
with:
action_name: is_package_json_version_upgraded
@ -89,7 +87,7 @@ jobs:
needs:
- check_if_version_upgraded
steps:
- uses: softprops/action-gh-release@v1
- uses: softprops/action-gh-release@v2
with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
@ -106,18 +104,18 @@ jobs:
- create_github_release
- check_if_version_upgraded
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: yarn build
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
- run: npm run build
- run: npx -y -p denoify@1.6.13 enable_short_npm_import_path
env:
DRY_RUN: "0"
- uses: garronej/ts-ci@v2.1.0
- uses: garronej/ts-ci@v2.1.2
with:
action_name: remove_dark_mode_specific_images_from_readme
- name: Publishing on NPM

4
.gitignore vendored
View File

@ -48,8 +48,8 @@ jspm_packages
.idea
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
/src/login/i18n/messages_defaultSet/
/src/account/i18n/
# VS Code devcontainers
.devcontainer

View File

@ -6,10 +6,11 @@ node_modules/
/src/tools/types/
/build_keycloak/
/.vscode/
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
/src/login/i18n/messages_defaultSet/
/src/account/i18n/messages_defaultSet/
/dist_test
/sample_react_project/
/sample_custom_react_project/
/keycloakify_starter_test/
/.storybook/static/keycloak-resources/
/.storybook/static/keycloak-resources/
/src/bin/start-keycloak/*.json

View File

@ -1,11 +1,24 @@
{
"printWidth": 150,
"printWidth": 90,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
"arrowParens": "avoid",
"overrides": [
{
"files": "*.tsx",
"options": {
"printWidth": 150
}
},
{
"files": "useUserProfileForm.tsx",
"options": {
"printWidth": 150
}
}
]
}

View File

@ -1,76 +0,0 @@
import React from "react";
import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
import { useDarkMode } from "storybook-dark-mode";
import { darkTheme, lightTheme } from "./customTheme";
import "./static/fonts/WorkSans/font.css";
export function DocsContainer({ children, context }) {
const isStorybookUiDark = useDarkMode();
const theme = isStorybookUiDark ? darkTheme : lightTheme;
const backgroundColor = theme.appBg;
return (
<>
<style>{`
body {
padding: 0 !important;
background-color: ${backgroundColor};
}
.docs-story {
background-color: ${backgroundColor};
}
[id^=story--] .container {
border: 1px dashed #e8e8e8;
}
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(3) {
visibility: collapse;
}
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
font-size: 13px;
}
`}</style>
<BaseContainer
context={{
...context,
"storyById": id => {
const storyContext = context.storyById(id);
return {
...storyContext,
"parameters": {
...storyContext?.parameters,
"docs": {
...storyContext?.parameters?.docs,
"theme": isStorybookUiDark ? darkTheme : lightTheme
}
}
};
}
}}
>
{children}
</BaseContainer>
</>
);
}
export function CanvasContainer({ children }) {
return (
<>
<style>{`
body {
padding: 0 !important;
}
`}</style>
{children}
</>
);
}

View File

@ -1,35 +0,0 @@
import { create } from "@storybook/theming";
const brandImage = "logo.png";
const brandTitle = "Keycloakify";
const brandUrl = "https://github.com/keycloakify/keycloakify";
const fontBase = '"Work Sans", sans-serif';
const fontCode = "monospace";
export const darkTheme = create({
"base": "dark",
"appBg": "#1E1E1E",
"appContentBg": "#161616",
"barBg": "#161616",
"colorSecondary": "#8585F6",
"textColor": "#FFFFFF",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
});
export const lightTheme = create({
"base": "light",
"appBg": "#F6F6F6",
"appContentBg": "#FFFFFF",
"barBg": "#FFFFFF",
"colorSecondary": "#000091",
"textColor": "#212121",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
});

33
.storybook/customTheme.ts Normal file
View File

@ -0,0 +1,33 @@
const brandImage = "logo.png";
const brandTitle = "Keycloakify";
const brandUrl = "https://github.com/keycloakify/keycloakify";
const fontBase = '"Work Sans", sans-serif';
const fontCode = "monospace";
export const darkTheme = {
base: "dark",
appBg: "#1E1E1E",
appContentBg: "#161616",
barBg: "#161616",
colorSecondary: "#8585F6",
textColor: "#FFFFFF",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
};
export const lightTheme: typeof darkTheme = {
base: "light",
appBg: "#F6F6F6",
appContentBg: "#FFFFFF",
barBg: "#FFFFFF",
colorSecondary: "#000091",
textColor: "#212121",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
};

View File

@ -1,15 +1,13 @@
module.exports = {
"stories": [
"../stories/**/*.stories.@(ts|tsx|mdx)"
stories: [
"../stories/**/*.stories.tsx"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
addons: [
"storybook-dark-mode",
"@storybook/addon-a11y"
],
"core": {
"builder": "webpack5"
core: {
builder: "webpack5"
},
"staticDirs": ["./static"]
staticDirs: ["./static", "../dist/res/public"]
};

View File

@ -1,6 +1,6 @@
import { addons } from '@storybook/addons';
addons.setConfig({
"selectedPanel": 'storybook/a11y/panel',
"showPanel": false,
selectedPanel: 'storybook/a11y/panel',
showPanel: false
});

View File

@ -0,0 +1,23 @@
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">
<style>
body.sb-show-main.sb-main-padded {
padding: 0;
}
body:not(.kcBodyClass) {
background-color: #393939;
}
body.sb-show-preparing-docs > .sb-wrapper {
visibility: hidden;
}
body .sb-preparing-story {
visibility: hidden;
}
</style>

View File

@ -1,125 +1,149 @@
import { darkTheme, lightTheme } from "./customTheme";
import { DocsContainer, CanvasContainer } from "./Containers";
import { create as createTheme } from "@storybook/theming";
export const parameters = {
"actions": { "argTypesRegex": "^on[A-Z].*" },
"controls": {
"matchers": {
"color": /(background|color)$/i,
"date": /Date$/,
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
"backgrounds": { "disable": true },
"darkMode": {
"light": lightTheme,
"dark": darkTheme,
backgrounds: { disable: true },
darkMode: {
light: createTheme(lightTheme),
dark: createTheme(darkTheme),
},
"docs": {
"container": DocsContainer
controls: {
disable: true,
},
"controls": {
"disable": true,
actions: {
disable: true
},
"actions": {
"disable": true
},
"viewport": {
"viewports": {
viewport: {
viewports: {
"1440p": {
"name": "1440p",
"styles": {
"width": "2560px",
"height": "1440px",
name: "1440p",
styles: {
width: "2560px",
height: "1440px",
},
},
"fullHD": {
"name": "Full HD",
"styles": {
"width": "1920px",
"height": "1080px",
fullHD: {
name: "Full HD",
styles: {
width: "1920px",
height: "1080px",
},
},
"macBookProBig": {
"name": "MacBook Pro Big",
"styles": {
"width": "1024px",
"height": "640px",
macBookProBig: {
name: "MacBook Pro Big",
styles: {
width: "1024px",
height: "640px",
},
},
"macBookProMedium": {
"name": "MacBook Pro Medium",
"styles": {
"width": "1440px",
"height": "900px",
macBookProMedium: {
name: "MacBook Pro Medium",
styles: {
width: "1440px",
height: "900px",
},
},
"macBookProSmall": {
"name": "MacBook Pro Small",
"styles": {
"width": "1680px",
"height": "1050px",
macBookProSmall: {
name: "MacBook Pro Small",
styles: {
width: "1680px",
height: "1050px",
},
},
"pcAgent": {
"name": "PC Agent",
"styles": {
"width": "960px",
"height": "540px",
pcAgent: {
name: "PC Agent",
styles: {
width: "960px",
height: "540px",
},
},
"iphone12Pro": {
"name": "Iphone 12 pro",
"styles": {
"width": "390px",
"height": "844px",
iphone12Pro: {
name: "Iphone 12 pro",
styles: {
width: "390px",
height: "844px",
},
},
"iphone5se": {
"name": "Iphone 5/SE",
"styles": {
"width": "320px",
"height": "568px",
iphone5se: {
name: "Iphone 5/SE",
styles: {
width: "320px",
height: "568px",
},
},
"ipadPro": {
"name": "Ipad pro",
"styles": {
"width": "1240px",
"height": "1366px",
ipadPro: {
name: "Ipad pro",
styles: {
width: "1240px",
height: "1366px",
},
},
"Galaxy s9+": {
"name": "Galaxy S9+",
"styles": {
"width": "320px",
"height": "658px",
name: "Galaxy S9+",
styles: {
width: "320px",
height: "658px",
},
}
},
},
"options": {
"storySort": (a, b) =>
options: {
storySort: (a, b) =>
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
},
};
export const decorators = [
(Story) => (
<CanvasContainer>
<Story />
</CanvasContainer>
),
];
const { getHardCodedWeight } = (() => {
const orderedPagesPrefix = [
"Introduction",
"login/login.ftl",
"login/register-user-profile.ftl",
"login/register.ftl",
"login/terms.ftl",
"login/error.ftl",
"login/code.ftl",
"login/delete-account-confirm.ftl",
"login/delete-credential.ftl",
"login/frontchannel-logout.ftl",
"login/idp-review-user-profile.ftl",
"login/info.ftl",
"login/login-config-totp.ftl",
"login/login-idp-link-confirm.ftl",
"login/login-idp-link-email.ftl",
"login/login-oauth-grant.ftl",
"login/login-otp.ftl",
"login/login-page-expired.ftl",
"login/login-password.ftl",
"login/login-reset-otp.ftl",
"login/login-reset-password.ftl",
"login/login-update-password.ftl",
"login/login-update-profile.ftl",
"login/login-username.ftl",
"login/login-verify-email.ftl",
"login/login-x509-info.ftl",
"login/logout-confirm.ftl",
"login/saml-post-form.ftl",
"login/select-authenticator.ftl",
"login/update-email.ftl",
"login/webauthn-authenticate.ftl",
"login/webauthn-error.ftl",
"login/webauthn-register.ftl",
"login/login-oauth2-device-verify-user-code.ftl",
"login/login-recovery-authn-code-config.ftl",
"login/login-recovery-authn-code-input.ftl",
"account/account.ftl",
"account/password.ftl",
"account/federatedIdentity.ftl",
"account/log.ftl",
"account/sessions.ftl",
"account/totp.ftl",
];
function getHardCodedWeight(kind) {

View File

@ -1,3 +1,3 @@
Looking to contribute? Thank you! PR are more than welcome.
Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.
Please refers to [this documentation page](https://docs.keycloakify.dev/faq-and-help/contributing) that will help you get started.

376
README.md
View File

@ -2,11 +2,11 @@
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
</p>
<p align="center">
<i>🔏 Create Keycloak themes using React 🔏</i>
<i>🔏 Keycloak Theming for the Modern Web 🔏</i>
<br>
<br>
<a href="https://github.com/garronej/keycloakify/actions">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
</a>
<a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/keycloakify">
@ -14,15 +14,15 @@
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/keycloakify">
</a>
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
</a>
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
<a href="https://discord.gg/kYFZG7fQmn">
<img src="https://img.shields.io/discord/1097708346976505977"/>
</a>
<p align="center">
Check out our discord server!<br/>
<a href="https://discord.gg/mJdYJSdcm4">
<img src="https://dcbadge.limes.pink/api/server/kYFZG7fQmn"/>
</a>
</p>
<p align="center">
<a href="https://www.keycloakify.dev">Home</a>
-
@ -36,34 +36,65 @@
<p align="center">
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
<br/>
<br/>
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
</p>
> Whether or not React is your preferred framework, Keycloakify
> offers a solid option for building Keycloak themes.
> It's not just a convenient way to create a Keycloak theme
> when using React; it's a well-regarded solution that many
> developers appreciate.
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
> 📣 🛑 Account themes generated by Keycloakify are not currently compatible with Keycloak 22.
> We are working on a solution. [Follow progress](https://github.com/keycloakify/keycloakify/issues/389).
> **Login and email themes are not affected**.
> UPDATE: [The PR](https://github.com/keycloak/keycloak/pull/22317) that should future proof Keycloakify account themes has been
> merged into Keycloak! 🥳 Credit to @xgp. We are now waiting for a new Keycloak release to be published.
> 📣 **Keycloakify 26 Released**
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/features/compiler-options/keycloakversiontargets).
Keycloakify is fully compatible with Keycloak, starting from version 11 and is anticipated to maintain compatibility with all future versions.
You can update your Keycloak, your Keycloakify generated theme won't break. (Well except for Keycloak 22's Account theme obviously but this was hopefully a one time debacle)
To understand the basis of my confidence in this, you can [visit this discussion thread where I've explained in detail](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791).
## Sponsors
## Sponsor 👼
Project backers, we trust and recommend their services.
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
Their dedicated support helps us continue the development and maintenance of this project.
<br/>
[Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github) provides the following services:
<div align="center">
- Simplify and secure your Keycloak Identity and Access Management. Keycloak as a Service.
- Custom theme building for your brand using Keycloakify.
![Logo Dark](https://github.com/user-attachments/assets/d8f6b6f5-3de4-4adc-ba15-cb4074e8309b#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/20736d6f-f22d-4a9d-9dfe-93be209a8191#gh-light-mode-only)
</div>
<br/>
<p align="center">
<i><a href="https://phasetwo.io/?utm_source=keycloakify"><strong>Keycloak as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-?utm_source=keycloakify">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/?utm_source=keycloakify">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/?utm_source=keycloakify">Keycloak support</a> to businesses of all sizes.</i>
</p>
<br/>
<br/>
<br/>
<div align="center">
![Logo Dark](https://github.com/user-attachments/assets/dd3925fb-a58a-4e91-b360-69c2fa1f1087#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/6c00c201-eed7-485a-a887-70891559d69b#gh-light-mode-only)
</div>
<br/>
<p align="center">
<a href="https://www.zone2.tech/services/keycloak-consulting">
<i><strong>Keycloak Consulting Services</strong> - Your partner in Keycloak deployment, configuration, and extension development for optimized identity management solutions.</i>
</a>
</p>
<div align="center">
@ -78,13 +109,11 @@ Their dedicated support helps us continue the development and maintenance of thi
</div>
<p align="center">
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud IAM</a> and use promo code <code>keycloakify5</code></i>
<br/>
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company. </a>
<br/>
Use code <code>keycloakify5</code> at checkout for a 5% discount.
</p>
Thank you, [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), for your support!
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
@ -119,6 +148,31 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zavoloklom"><img src="https://avatars.githubusercontent.com/u/4151869?v=4?s=100" width="100px;" alt="Sergey Kupletsky"/><br /><sub><b>Sergey Kupletsky</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=zavoloklom" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/rome-user"><img src="https://avatars.githubusercontent.com/u/114131048?v=4?s=100" width="100px;" alt="rome-user"/><br /><sub><b>rome-user</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=rome-user" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/celinepelletier"><img src="https://avatars.githubusercontent.com/u/82821620?v=4?s=100" width="100px;" alt="Céline Pelletier"/><br /><sub><b>Céline Pelletier</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=celinepelletier" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xgp"><img src="https://avatars.githubusercontent.com/u/244253?v=4?s=100" width="100px;" alt="Garth"/><br /><sub><b>Garth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=xgp" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/BlackVoid"><img src="https://avatars.githubusercontent.com/u/673720?v=4?s=100" width="100px;" alt="Felix Gustavsson"/><br /><sub><b>Felix Gustavsson</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=BlackVoid" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madmadson"><img src="https://avatars.githubusercontent.com/u/798831?v=4?s=100" width="100px;" alt="Tobias Matt"/><br /><sub><b>Tobias Matt</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=madmadson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/liamlows"><img src="https://avatars.githubusercontent.com/u/1365914?v=4?s=100" width="100px;" alt="Liam Lowsley-Williams"/><br /><sub><b>Liam Lowsley-Williams</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=liamlows" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/oes-rioniz/"><img src="https://avatars.githubusercontent.com/u/5172296?v=4?s=100" width="100px;" alt="Omid"/><br /><sub><b>Omid</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kathari00"><img src="https://avatars.githubusercontent.com/u/42547712?v=4?s=100" width="100px;" alt="Katharina Eiserfey"/><br /><sub><b>Katharina Eiserfey</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luca-peruzzo"><img src="https://avatars.githubusercontent.com/u/69015314?v=4?s=100" width="100px;" alt="Luca Peruzzo"/><br /><sub><b>Luca Peruzzo</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nima70"><img src="https://avatars.githubusercontent.com/u/5094767?v=4?s=100" width="100px;" alt="Nima Shokouhfar"/><br /><sub><b>Nima Shokouhfar</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://mruder.dev"><img src="https://avatars.githubusercontent.com/u/18495294?v=4?s=100" width="100px;" alt="Marvin A. Ruder"/><br /><sub><b>Marvin A. Ruder</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/issues?q=author%3Amarvinruder" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kodebach"><img src="https://avatars.githubusercontent.com/u/23529132?v=4?s=100" width="100px;" alt="Klemens Böswirth"/><br /><sub><b>Klemens Böswirth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kodebach" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wnmzzzz"><img src="https://avatars.githubusercontent.com/u/117174301?v=4?s=100" width="100px;" alt="wnmzzzz"/><br /><sub><b>wnmzzzz</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=wnmzzzz" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>
@ -127,259 +181,3 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
# Changelog highlights
## 8.0
- Much smaller .jar size. 70.2 MB -> 7.8 MB.
Keycloakify now detects which of the static resources from the default theme are actually used by your theme and only include those in the .jar.
- Build time: The first build is slowed but the subsequent build are faster. [Update your CI so that the cache is persisted across CI build](https://github.com/keycloakify/keycloakify-starter/commit/bc378d5afb67e796f520afbc348185f3e319d9d0).
### Breaking changes
There are very few breaking changes in this major version.
- The [`--external-assets` build option has been removed](https://docs.keycloakify.dev/v/v7/build-options#external-assets-deprecated) it was a performance optimization that is no longer relevant now that
we have lazy loading.
- `kcContext.usernameEditDisabled` is now `kcContext.usernameHidden`, the type was lying, it has been updated to reflect what's actually on the `kcContext` at runtime.
If you want to see in detail what should be updated [see issue](https://github.com/keycloakify/keycloakify/pull/399), or you can search and replace `usernameEditDisabled` -> `usernameHidden` it'll do the trick.
- The `usePrepareTemplate` prototype has been changed, you can search and replace:
`src/keycloak-theme/login/Template.tsx`
```ts
url,
"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"],
```
by
```ts
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
`${url.resourcesPath}/css/login.css`
],
```
and
`src/keycloak-theme/account/Template.css`
```ts
url,
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
"styles": ["css/account.css"],
```
by
```ts
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
],
```
## 7.15
- The i18n messages you defines in your theme are now also maid available to Keycloak.
In practice this mean that you can now customize the `kcContext.message.summary` that
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
are used to display specific error on some field of the form.
[See video](https://youtu.be/D6tZcemReTI)
## 7.14
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
## 7.13
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
It's now mandatory to [adopt the new directory structure](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
## 7.12
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.
There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.extrathemenames).
## 7.9
- Separate script for copying the default theme static assets to the public directory.
Theses assets are only needed for testing your theme locally in Storybook or with a `mockPageId`.
You are now expected to have a `"prepare": "copy-keycloak-resources-to-public",` in your package.json scripts.
This script will create `public/keycloak-assets` when you run `yarn install` (If you are using another package manager
like `pnpm` makes sure that `"prepare"` is actually ran.)
[See the updated starter](https://github.com/keycloakify/keycloakify-starter/blob/94532fcf10bf8b19e0873be8575fd28a8958a806/package.json#L11). `public/keycloak-assets` shouldn't be tracked by GIT and is automatically ignored.
## 7.7
- Better storybook support, see [the starter project](https://github.com/keycloakify/keycloakify-starter).
## 7.0 🍾
- Account theme support 🚀
- It's much easier to customize pages at the CSS level, you can now see in the browser dev tool the customizable classes.
- New interactive CLI tool `npx eject-keycloak-page`, that enables to select the page you want to customize at the component level.
- There is [a Storybook](https://storybook.keycloakify.dev)
- [Remember me is fixed](https://github.com/keycloakify/keycloakify/pull/272)
## 6.13
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
## 6.12
Massive improvement in the developer experience:
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
- A lot of comments have been added in the code of the starter to make it easier to get started.
- The doc has been updated: https://docs.keycloakify.dev
- A lot of improvements in the type system.
## 6.11.4
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/239).
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
## 6.10.0
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
## 6.8.4
- `@emotion/react` is no longer a peer dependency of Keycloakify.
## 6.8.0
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
individual page (`<Login />`, `<RegisterUserProfile />`, ...) it enables to customize only the header and footer for
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/keycloakify/issues/191).
## 6.7.0
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/185).
## 6.6.0
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/184).
## 6.5.0
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/183).
## 6.4.0
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded.
- First print much quicker, use of React.lazy() everywhere.
- Real i18n API.
- Actual documentation for build options.
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
## 5.7.0
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
## 5.6.4
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
## v5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## v5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
## v5.0.0
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
New i18n system.
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
## v4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## v4.8.0
[Email template customization.](#email-template-customization)
## v4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## v4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
Register with user profile enabled: Out of the box `options` validator support.
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
## v4.6.0
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
## v4.5.3
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
with this new method your theme wont break on minor Keycloakify update.
## v4.3.0
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
Every time a page is added it's a breaking change for non CSS-only theme.
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## v3
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
## v2.5
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).

View File

@ -1,73 +0,0 @@
{
"allOf": [
{
"$ref": "https://json.schemastore.org/package.json"
},
{
"$ref": "keycloakifyPackageJsonSchema"
}
],
"$ref": "#/definitions/keycloakifyPackageJsonSchema",
"definitions": {
"keycloakifyPackageJsonSchema": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"homepage": {
"type": "string"
},
"keycloakify": {
"type": "object",
"properties": {
"extraPages": {
"type": "array",
"items": {
"type": "string"
}
},
"extraThemeProperties": {
"type": "array",
"items": {
"type": "string"
}
},
"areAppAndKeycloakServerSharingSameDomain": {
"type": "boolean"
},
"artifactId": {
"type": "string"
},
"groupId": {
"type": "string"
},
"bundler": {
"type": "string",
"enum": ["mvn", "keycloakify", "none"]
},
"keycloakVersionDefaultAssets": {
"type": "string"
},
"reactAppBuildDirPath": {
"type": "string"
},
"keycloakifyBuildDirPath": {
"type": "string"
},
"themeName": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": ["name", "version"],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}

View File

@ -1,39 +1,26 @@
{
"name": "keycloakify",
"version": "8.4.1",
"description": "Create Keycloak themes using React",
"version": "11.8.23",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
"url": "git://github.com/keycloakify/keycloakify.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
"prepare": "tsx scripts/generate-i18n-messages.ts",
"build": "tsx scripts/build/main.ts",
"storybook": "tsx scripts/start-storybook.ts",
"link-in-starter": "tsx scripts/link-in-starter.ts",
"test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write",
"format:check": "yarn _format --list-different",
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "yarn link-in-app keycloakify-starter",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
"link-in-app": "tsx scripts/link-in-app.ts",
"build-storybook": "tsx scripts/build-storybook.ts",
"dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
},
"bin": {
"copy-keycloak-resources-to-public": "dist/bin/copy-keycloak-resources-to-public.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js",
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
"keycloakify": "dist/bin/keycloakify/index.js"
"keycloakify": "dist/bin/main.js"
},
"lint-staged": {
"*.{ts,tsx,json,md}": [
@ -48,83 +35,91 @@
"author": "u/garronej",
"license": "MIT",
"files": [
"src/",
"dist/",
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/tsconfig.tsbuildinfo"
"!dist/bin/",
"dist/bin/**/*.d.ts",
"dist/bin/main.js",
"dist/bin/*.index.js",
"dist/bin/*.node",
"dist/bin/shared/constants.js",
"dist/bin/shared/constants.js.map",
"dist/bin/shared/customHandler.js",
"dist/bin/shared/customHandler.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts"
],
"keywords": [
"bluehats",
"keycloak",
"react",
"theme",
"FreeMarker",
"ftl",
"login",
"register"
"register",
"account",
"bluehats"
],
"homepage": "https://www.keycloakify.dev",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"dependencies": {
"tsafe": "^1.8.5"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@emotion/react": "^11.10.6",
"@babel/core": "^7.24.5",
"@babel/generator": "^7.24.5",
"@babel/parser": "^7.24.5",
"@babel/preset-env": "7.24.8",
"@babel/types": "^7.24.5",
"@emotion/react": "^11.11.4",
"@octokit/rest": "^20.1.1",
"@storybook/addon-a11y": "^6.5.16",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"@types/babel__generator": "^7.6.4",
"@types/dompurify": "^2.0.0",
"@types/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^18.15.3",
"@types/properties-parser": "^0.3.3",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/yauzl": "^2.10.0",
"@types/yazl": "^2.4.2",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"@types/yauzl": "^2.10.3",
"@vercel/ncc": "^0.38.1",
"babel-loader": "9.1.3",
"chalk": "^4.1.2",
"cheerio": "1.0.0-rc.12",
"chokidar-cli": "^3.0.0",
"cli-select": "^1.1.2",
"dompurify": "^3.1.6",
"eslint-plugin-storybook": "^0.6.7",
"evt": "^2.5.8",
"html-entities": "^2.5.2",
"husky": "^4.3.8",
"isomorphic-dompurify": "^2.15.0",
"lint-staged": "^11.0.0",
"powerhooks": "^0.26.7",
"prettier": "^2.3.0",
"magic-string": "^0.30.7",
"make-fetch-happen": "^11.0.3",
"powerhooks": "^1.0.19",
"prettier": "^3.2.5",
"properties-parser": "^0.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.3",
"tss-react": "^4.8.2",
"typescript": "^4.9.1-beta",
"vitest": "^0.29.8",
"zod-to-json-schema": "^3.20.4"
},
"dependencies": {
"@babel/generator": "^7.22.9",
"@babel/parser": "^7.22.7",
"@babel/types": "^7.22.5",
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.18",
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
"tsafe": "^1.6.0",
"run-exclusive": "^2.2.19",
"storybook-dark-mode": "^1.1.2",
"termost": "^v0.12.1",
"tsc-alias": "^1.8.10",
"tss-react": "^4.9.10",
"tsx": "^4.15.5",
"typescript": "^4.9.4",
"vite": "^5.2.11",
"vitest": "^1.6.0",
"webpack": "5.93.0",
"webpack-cli": "5.1.4",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
"zod": "^3.17.10"
}
}

View File

@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["main", "landingpage"],
"baseBranches": ["main"],
"extends": ["config:base"],
"dependencyDashboard": false,
"bumpVersion": "patch",

View File

@ -0,0 +1,4 @@
import { run } from "./shared/run";
run("yarn build");
run("npx build-storybook");

View File

@ -0,0 +1,79 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
import { accountMultiPageSupportedLanguages } from "../generate-i18n-messages";
import * as fsPr from "fs/promises";
export async function createAccountV1Dir() {
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersionId: "FOR_ACCOUNT_MULTI_PAGE"
});
const destDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"dist",
"res",
"account-v1"
);
await fsPr.rm(destDirPath, { recursive: true, force: true });
transformCodebase({
srcDirPath: pathJoin(extractedDirPath, "base", "account"),
destDirPath
});
transformCodebase({
srcDirPath: pathJoin(extractedDirPath, "keycloak", "account", "resources"),
destDirPath: pathJoin(destDirPath, "resources")
});
transformCodebase({
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
destDirPath: pathJoin(
destDirPath,
"resources",
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
)
});
fs.writeFileSync(
pathJoin(destDirPath, "theme.properties"),
Buffer.from(
[
"accountResourceProvider=account-v1",
"",
`locales=${accountMultiPageSupportedLanguages.join(",")}`,
"",
"styles=" +
[
"css/account.css",
"img/icon-sidebar-active.png",
"img/logo.png",
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(
fileBasename =>
`${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}`
)
].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
"kcButtonClass=btn",
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
"kcButtonPrimaryClass=btn-primary",
"kcButtonDefaultClass=btn-default",
"# classes defining size of the button",
"kcButtonLargeClass=btn-lg",
""
].join("\n"),
"utf8"
)
);
}

View File

@ -0,0 +1,73 @@
import { join as pathJoin } from "path";
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants";
import { assert, type Equals } from "tsafe/assert";
import * as fsPr from "fs/promises";
export async function createPublicKeycloakifyDevResourcesDir() {
await Promise.all(
(["login", "account"] as const).map(async themeType => {
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersionId: (() => {
switch (themeType) {
case "login":
return "FOR_LOGIN_THEME";
case "account":
return "FOR_ACCOUNT_MULTI_PAGE";
}
assert<Equals<typeof themeType, never>>();
})()
});
const destDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"dist",
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
themeType
);
await fsPr.rm(destDirPath, { recursive: true, force: true });
base_resources: {
const srcDirPath = pathJoin(
extractedDirPath,
"base",
themeType,
"resources"
);
if (!(await existsAsync(srcDirPath))) {
break base_resources;
}
transformCodebase({
srcDirPath,
destDirPath
});
}
transformCodebase({
srcDirPath: pathJoin(
extractedDirPath,
"keycloak",
themeType,
"resources"
),
destDirPath
});
transformCodebase({
srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"),
destDirPath: pathJoin(
destDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON
)
});
})
);
}

View File

@ -0,0 +1,39 @@
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
import { cacheDirPath } from "../shared/cacheDirPath";
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
import * as fs from "fs/promises";
import {
KEYCLOAKIFY_LOGGING_VERSION,
KEYCLOAKIFY_LOGIN_JAR_BASENAME
} from "../../src/bin/shared/constants";
import { join as pathJoin } from "path";
export async function downloadKeycloakifyLogging(params: { distDirPath: string }) {
const { distDirPath } = params;
const jarFilePath = pathJoin(
distDirPath,
"src",
"bin",
"start-keycloak",
KEYCLOAKIFY_LOGIN_JAR_BASENAME
);
if (await existsAsync(jarFilePath)) {
return;
}
const { archiveFilePath } = await downloadAndExtractArchive({
cacheDirPath,
fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: getThisCodebaseRootDirPath()
}),
url: `https://github.com/keycloakify/keycloakify-logging/releases/download/${KEYCLOAKIFY_LOGGING_VERSION}/keycloakify-logging-${KEYCLOAKIFY_LOGGING_VERSION}.jar`,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
await fs.cp(archiveFilePath, jarFilePath);
}

188
scripts/build/main.ts Normal file
View File

@ -0,0 +1,188 @@
import * as fs from "fs";
import { join } from "path";
import { assert } from "tsafe/assert";
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
import { createPublicKeycloakifyDevResourcesDir } from "./createPublicKeycloakifyDevResourcesDir";
import { createAccountV1Dir } from "./createAccountV1Dir";
import chalk from "chalk";
import { run } from "../shared/run";
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
(async () => {
console.log(chalk.cyan("Building Keycloakify..."));
const startTime = Date.now();
if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
fs.renameSync(
join("dist", "bin", "main.original.js"),
join("dist", "bin", "main.js")
);
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
fs.rmSync(join("dist", "bin", fileBasename));
}
});
}
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
if (
!fs
.readFileSync(join("dist", "bin", "main.js"))
.toString("utf8")
.includes("__nccwpck_require__")
) {
fs.cpSync(
join("dist", "bin", "main.js"),
join("dist", "bin", "main.original.js")
);
}
run(
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
);
transformCodebase({
srcDirPath: join("dist", "ncc_out"),
destDirPath: join("dist", "bin"),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (fileRelativePath === "index.js") {
return {
newFileName: "main.js",
modifiedSourceCode: sourceCode
};
}
return { modifiedSourceCode: sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
{
let hasBeenPatched = false;
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) {
return;
}
const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage(
join("dist", "bin", fileBasename)
);
if (hasBeenPatched_i) {
hasBeenPatched = true;
}
});
assert(hasBeenPatched);
}
fs.chmodSync(
join("dist", "bin", "main.js"),
fs.statSync(join("dist", "bin", "main.js")).mode |
fs.constants.S_IXUSR |
fs.constants.S_IXGRP |
fs.constants.S_IXOTH
);
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
vendorFrontendDependencies({ distDirPath: join(process.cwd(), "dist") });
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
fs.renameSync(
join("dist", "vite-plugin", "index.original.js"),
join("dist", "vite-plugin", "index.js")
);
}
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
if (
!fs
.readFileSync(join("dist", "vite-plugin", "index.js"))
.toString("utf8")
.includes("__nccwpck_require__")
) {
fs.cpSync(
join("dist", "vite-plugin", "index.js"),
join("dist", "vite-plugin", "index.original.js")
);
}
run(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join(
"dist",
"ncc_out"
)}`
);
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => {
assert(!fileBasename.endsWith(".index.js"));
assert(!fileBasename.endsWith(".node"));
});
transformCodebase({
srcDirPath: join("dist", "ncc_out"),
destDirPath: join("dist", "vite-plugin"),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
assert(fileRelativePath === "index.js");
return { modifiedSourceCode: sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { recursive: true });
{
const dirBasename = "src";
const destDirPath = join("dist", dirBasename);
fs.rmSync(destDirPath, { recursive: true, force: true });
fs.cpSync(dirBasename, destDirPath, { recursive: true });
}
transformCodebase({
srcDirPath: join("stories"),
destDirPath: join("dist", "stories"),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (!fileRelativePath.endsWith(".stories.tsx")) {
return undefined;
}
return { modifiedSourceCode: sourceCode };
}
});
await createPublicKeycloakifyDevResourcesDir();
await createAccountV1Dir();
await downloadKeycloakifyLogging({
distDirPath: join(process.cwd(), "dist")
});
console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
);
})();
function patchDeprecatedBufferApiUsage(filePath: string) {
const before = fs.readFileSync(filePath).toString("utf8");
const after = before.replace(
`var buffer = new Buffer(toRead);`,
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
);
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
const hasBeenPatched = after !== before;
return { hasBeenPatched };
}

View File

@ -0,0 +1,97 @@
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { run } from "../shared/run";
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
export function vendorFrontendDependencies(params: { distDirPath: string }) {
const { distDirPath } = params;
const vendorDirPath = pathJoin(distDirPath, "tools", "vendor");
const cacheDirPath = pathJoin(cacheDirPath_base, "vendorFrontendDependencies");
const extraBundleFileBasenames = new Set<string>();
fs.readdirSync(vendorDirPath)
.filter(fileBasename => fileBasename.endsWith(".js"))
.map(fileBasename => pathJoin(vendorDirPath, fileBasename))
.forEach(filePath => {
{
const mapFilePath = `${filePath}.map`;
if (fs.existsSync(mapFilePath)) {
fs.unlinkSync(mapFilePath);
}
}
if (!fs.existsSync(cacheDirPath)) {
fs.mkdirSync(cacheDirPath, { recursive: true });
}
const webpackConfigJsFilePath = pathJoin(cacheDirPath, "webpack.config.js");
const webpackOutputDirPath = pathJoin(cacheDirPath, "webpack_output");
const webpackOutputFilePath = pathJoin(webpackOutputDirPath, "index.js");
fs.writeFileSync(
webpackConfigJsFilePath,
Buffer.from(
[
``,
`module.exports = {`,
` mode: 'production',`,
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
` output: {`,
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
` filename: '${pathBasename(webpackOutputFilePath)}',`,
` libraryTarget: 'module',`,
` },`,
` target: "web",`,
` module: {`,
` rules: [`,
` {`,
` test: /\.js$/,`,
` use: {`,
` loader: 'babel-loader',`,
` options: {`,
` presets: ['@babel/preset-env'],`,
` }`,
` }`,
` }`,
` ]`,
` },`,
` experiments: {`,
` outputModule: true`,
` }`,
`};`
].join("\n")
)
);
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
cwd: pathDirname(webpackConfigJsFilePath)
});
fs.readdirSync(webpackOutputDirPath)
.filter(fileBasename => !fileBasename.endsWith(".txt"))
.map(fileBasename => pathJoin(webpackOutputDirPath, fileBasename))
.forEach(bundleFilePath => {
assert(bundleFilePath.endsWith(".js"));
if (pathBasename(bundleFilePath) === "index.js") {
fs.renameSync(webpackOutputFilePath, filePath);
} else {
const bundleFileBasename = pathBasename(bundleFilePath);
assert(!extraBundleFileBasenames.has(bundleFileBasename));
extraBundleFileBasenames.add(bundleFileBasename);
fs.renameSync(
bundleFilePath,
pathJoin(pathDirname(filePath), bundleFileBasename)
);
}
});
fs.rmSync(webpackOutputDirPath, { recursive: true });
});
}

View File

@ -0,0 +1,45 @@
import { CONTAINER_NAME } from "../src/bin/shared/constants";
import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer";
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
import { cacheDirPath } from "./shared/cacheDirPath";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
import { join as pathJoin } from "path";
import chalk from "chalk";
(async () => {
const keycloakMajorVersionNumber = SemVer.parse(
child_process
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
.toString("utf8")
.trim()
.split(":")[1]
).major;
const parsedRealmJson = await dumpContainerConfig({
buildContext: {
cacheDirPath
},
keycloakMajorVersionNumber,
realmName: "myrealm"
});
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
"realmConfig",
"defaultConfig",
`realm-kc-${keycloakMajorVersionNumber}.json`
);
await writeRealmJsonFile({
parsedRealmJson,
realmJsonFilePath,
keycloakMajorVersionNumber
});
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
})();

View File

@ -1,93 +1,200 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import {
join as pathJoin,
relative as pathRelative,
dirname as pathDirname,
sep as pathSep
} from "path";
import { assert, type Equals } from "tsafe/assert";
import { same } from "evt/tools/inDepth";
import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getLogger } from "../src/bin/tools/logger";
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { deepAssign } from "../src/tools/deepAssign";
import { THEME_TYPES } from "../src/bin/shared/constants";
import { transformCodebase } from "../src/bin/tools/transformCodebase";
import propertiesParser from "properties-parser";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
if (require.main === module) {
generateI18nMessages();
}
//@ts-ignore
const propertiesParser = require("properties-parser");
async function generateI18nMessages() {
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
const isSilent = true;
const accountI18nDirPath = pathJoin(
thisCodebaseRootDirPath,
"src",
"account",
"i18n"
);
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "21.0.1";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
await downloadBuiltinKeycloakTheme({
"projectDirPath": getProjectRoot(),
keycloakVersion,
"destDirPath": tmpDirPath
});
if (fs.existsSync(accountI18nDirPath)) {
fs.rmSync(accountI18nDirPath, { recursive: true });
}
type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl({
"dirPath": baseThemeDirPath,
"returnedPathsType": "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re);
if (match === null) {
return;
}
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersionId: (() => {
switch (themeType) {
case "login":
return "FOR_LOGIN_THEME";
case "account":
return "FOR_ACCOUNT_MULTI_PAGE";
}
assert<Equals<typeof themeType, never>>();
})()
});
}
fs.rmSync(tmpDirPath, { recursive: true, force: true });
{
const baseThemeDirPath = pathJoin(extractedDirPath, "base");
const re = new RegExp(
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
);
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
crawl({
dirPath: baseThemeDirPath,
returnedPathsType: "relative to dirPath"
}).forEach(filePath => {
const match = filePath.match(re);
if (themeType !== "login" && themeType !== "account") {
return;
if (match === null) {
return;
}
const [, themeType_here, language] = match;
if (themeType_here !== themeType) {
return;
}
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
Object.fromEntries(
Object.entries(
propertiesParser.parse(
fs
.readFileSync(pathJoin(baseThemeDirPath, filePath))
.toString("utf8")
) as Record<string, string>
)
.map(([key, value]) => [key, value.replace(/''/g, "'")])
.map(([key, value]) => [
key === "locale_pt_BR" ? "locale_pt-BR" : key,
value
])
.map(([key, value]) => [
key,
key === "termsText" ? "" : value
])
);
});
}
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
const recordForThemeType = record[themeType];
const languages = Object.keys(recordForPageType);
const languages = Object.keys(recordForThemeType);
const keycloakifyExtraMessages = (() => {
switch (themeType) {
case "login":
return keycloakifyExtraMessages_login;
case "account":
return keycloakifyExtraMessages_account;
}
assert(false);
})();
/* Migration helper
console.log({ themeType });
{
const all = new Set<string>();
languages.forEach(languages => all.add(languages));
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
currentlySupportedLanguages.forEach(languages => all.add(languages));
all.forEach(language => {
console.log([
`"${language}": `,
`isInLanguages: ${languages.includes(language)}`,
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
].join(" "))
});
}
*/
assert(
same(languages, Object.keys(keycloakifyExtraMessages), {
takeIntoAccountArraysOrdering: false
})
);
deepAssign({
target: recordForThemeType,
source: keycloakifyExtraMessages
});
const messagesDirPath = pathJoin(
thisCodebaseRootDirPath,
"src",
themeType,
"i18n",
"messages_defaultSet"
);
if (!fs.existsSync(messagesDirPath)) {
fs.mkdirSync(messagesDirPath, { recursive: true });
}
fs.writeFileSync(
pathJoin(messagesDirPath, "types.ts"),
Buffer.from(
[
``,
`export const languageTags = ${JSON.stringify(languages, null, 2)} as const;`,
``,
`export type LanguageTag = typeof languageTags[number];`,
``,
`export type MessageKey = keyof typeof import("./en")["default"];`,
``
].join("\n"),
"utf8"
)
);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
""
`//This code was automatically generated by running ${pathRelative(
thisCodebaseRootDirPath,
__filename
)}`,
"//PLEASE DO NOT EDIT MANUALLY"
].join("\n");
languages.forEach(language => {
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
const filePath = pathJoin(messagesDirPath, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.mkdirSync(pathDirname(filePath), { recursive: true });
fs.writeFileSync(
filePath,
Buffer.from(
[
generatedFileHeader,
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
`const messages= ${JSON.stringify(
recordForThemeType[language],
null,
2
)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
@ -96,30 +203,551 @@ async function main() {
)
);
logger.log(`${filePath} wrote`);
//console.log(`${filePath} wrote`);
});
fs.writeFileSync(
pathJoin(baseMessagesDirPath, "index.ts"),
pathJoin(messagesDirPath, "index.ts"),
Buffer.from(
[
generatedFileHeader,
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
`import * as en from "./en";`,
"",
"export async function fetchMessages_defaultSet(currentLanguageTag: string) {",
" const { default: messages_defaultSet } = await (() => {",
" switch (currentLanguageTag) {",
...languages.map(language => ` case "${language}": return import("./${language}");`),
` case "en": return en;`,
...languages
.filter(language => language !== "en")
.map(
language =>
` case "${language}": return import("./${language}");`
),
' default: return { "default": {} };',
" }",
" })();",
" return messages;",
" return messages_defaultSet;",
"}"
].join("\n"),
"utf8"
)
);
}
transformCodebase({
srcDirPath: pathJoin(thisCodebaseRootDirPath, "src", "login", "i18n"),
destDirPath: accountI18nDirPath,
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (fileRelativePath.startsWith("messages_defaultSet")) {
return undefined;
}
return { modifiedSourceCode: sourceCode };
}
});
}
if (require.main === module) {
main();
}
const keycloakifyExtraMessages_login: Record<
| "en"
| "ar"
| "ca"
| "cs"
| "da"
| "de"
| "el"
| "es"
| "fa"
| "fi"
| "fr"
| "hu"
| "it"
| "ja"
| "lt"
| "lv"
| "nl"
| "no"
| "pl"
| "pt"
| "pt-BR"
| "ru"
| "sk"
| "sv"
| "th"
| "tr"
| "uk"
| "ka"
| "zh-CN"
| "zh-TW",
Record<
| "shouldBeEqual"
| "shouldBeDifferent"
| "shouldMatchPattern"
| "mustBeAnInteger"
| "notAValidOption"
| "selectAnOption"
| "remove"
| "addValue"
| "languages",
string
>
> = {
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
selectAnOption: "Select an option",
remove: "Remove",
addValue: "Add value",
languages: "Languages"
},
/* spell-checker: disable */
ar: {
shouldBeEqual: "{0} يجب أن يكون مساويًا لـ {1}",
shouldBeDifferent: "{0} يجب أن يكون مختلفًا عن {1}",
shouldMatchPattern: "`/يجب أن يطابق النمط: `/{0}/",
mustBeAnInteger: "يجب أن يكون عددًا صحيحًا",
notAValidOption: "ليس خيارًا صالحًا",
selectAnOption: "اختر خيارًا",
remove: "إزالة",
addValue: "أضف قيمة",
languages: "اللغات"
},
ca: {
shouldBeEqual: "{0} hauria de ser igual a {1}",
shouldBeDifferent: "{0} hauria de ser diferent de {1}",
shouldMatchPattern: "El patró hauria de coincidir: `/{0}/`",
mustBeAnInteger: "Ha de ser un enter",
notAValidOption: "No és una opció vàlida",
selectAnOption: "Selecciona una opció",
remove: "Elimina",
addValue: "Afegeix valor",
languages: "Idiomes"
},
cs: {
shouldBeEqual: "{0} by měl být roven {1}",
shouldBeDifferent: "{0} by měl být odlišný od {1}",
shouldMatchPattern: "Vzor by měl odpovídat: `/{0}/`",
mustBeAnInteger: "Musí být celé číslo",
notAValidOption: "Není platná možnost",
selectAnOption: "Vyberte možnost",
remove: "Odstranit",
addValue: "Přidat hodnotu",
languages: "Jazyky"
},
da: {
shouldBeEqual: "{0} bør være lig med {1}",
shouldBeDifferent: "{0} bør være forskellig fra {1}",
shouldMatchPattern: "Mønsteret bør matche: `/{0}/`",
mustBeAnInteger: "Skal være et heltal",
notAValidOption: "Ikke en gyldig mulighed",
selectAnOption: "Vælg en mulighed",
remove: "Fjern",
addValue: "Tilføj værdi",
languages: "Sprog"
},
de: {
shouldBeEqual: "{0} sollte gleich {1} sein",
shouldBeDifferent: "{0} sollte sich von {1} unterscheiden",
shouldMatchPattern: "Muster sollte übereinstimmen: `/{0}/`",
mustBeAnInteger: "Muss eine ganze Zahl sein",
notAValidOption: "Keine gültige Option",
selectAnOption: "Wählen Sie eine Option",
remove: "Entfernen",
addValue: "Wert hinzufügen",
languages: "Sprachen"
},
el: {
shouldBeEqual: "Το {0} πρέπει να είναι ίσο με {1}",
shouldBeDifferent: "Το {0} πρέπει να διαφέρει από το {1}",
shouldMatchPattern: "Το πρότυπο πρέπει να ταιριάζει: `/{0}/`",
mustBeAnInteger: "Πρέπει να είναι ακέραιος",
notAValidOption: "Δεν είναι μια έγκυρη επιλογή",
selectAnOption: "Επιλέξτε μια επιλογή",
remove: "Αφαίρεση",
addValue: "Προσθήκη τιμής",
languages: "Γλώσσες"
},
es: {
shouldBeEqual: "{0} debería ser igual a {1}",
shouldBeDifferent: "{0} debería ser diferente a {1}",
shouldMatchPattern: "El patrón debería coincidir: `/{0}/`",
mustBeAnInteger: "Debe ser un número entero",
notAValidOption: "No es una opción válida",
selectAnOption: "Selecciona una opción",
remove: "Eliminar",
addValue: "Añadir valor",
languages: "Idiomas"
},
fa: {
shouldBeEqual: "{0} باید برابر باشد با {1}",
shouldBeDifferent: "{0} باید متفاوت باشد از {1}",
shouldMatchPattern: "الگو باید مطابقت داشته باشد: `/{0}/`",
mustBeAnInteger: "باید یک عدد صحیح باشد",
notAValidOption: "یک گزینه معتبر نیست",
selectAnOption: "یک گزینه انتخاب کنید",
remove: "حذف",
addValue: "افزودن مقدار",
languages: "زبان‌ها"
},
fi: {
shouldBeEqual: "{0} pitäisi olla yhtä suuri kuin {1}",
shouldBeDifferent: "{0} pitäisi olla erilainen kuin {1}",
shouldMatchPattern: "Mallin tulisi vastata: `/{0}/`",
mustBeAnInteger: "On oltava kokonaisluku",
notAValidOption: "Ei ole kelvollinen vaihtoehto",
selectAnOption: "Valitse vaihtoehto",
remove: "Poista",
addValue: "Lisää arvo",
languages: "Kielet"
},
fr: {
shouldBeEqual: "{0} devrait être égal à {1}",
shouldBeDifferent: "{0} devrait être différent de {1}",
shouldMatchPattern: "Le motif devrait correspondre: `/{0}/`",
mustBeAnInteger: "Doit être un entier",
notAValidOption: "Pas une option valide",
selectAnOption: "Sélectionnez une option",
remove: "Supprimer",
addValue: "Ajouter une valeur",
languages: "Langues"
},
hu: {
shouldBeEqual: "{0} egyenlő kell legyen {1}-vel",
shouldBeDifferent: "{0} különbözőnek kell lennie, mint {1}",
shouldMatchPattern: "A mintának egyeznie kell: `/{0}/`",
mustBeAnInteger: "Egész számnak kell lennie",
notAValidOption: "Nem érvényes opció",
selectAnOption: "Válasszon egy lehetőséget",
remove: "Eltávolítás",
addValue: "Érték hozzáadása",
languages: "Nyelvek"
},
it: {
shouldBeEqual: "{0} dovrebbe essere uguale a {1}",
shouldBeDifferent: "{0} dovrebbe essere diverso da {1}",
shouldMatchPattern: "Il modello dovrebbe corrispondere: `/{0}/`",
mustBeAnInteger: "Deve essere un numero intero",
notAValidOption: "Non è un'opzione valida",
selectAnOption: "Seleziona un'opzione",
remove: "Rimuovi",
addValue: "Aggiungi valore",
languages: "Lingue"
},
ja: {
shouldBeEqual: "{0} は {1} と等しい必要があります",
shouldBeDifferent: "{0} は {1} と異なる必要があります",
shouldMatchPattern: "パターンは一致する必要があります: `/{0}/`",
mustBeAnInteger: "整数である必要があります",
notAValidOption: "有効なオプションではありません",
selectAnOption: "オプションを選択",
remove: "削除",
addValue: "値を追加",
languages: "言語"
},
lt: {
shouldBeEqual: "{0} turėtų būti lygus {1}",
shouldBeDifferent: "{0} turėtų skirtis nuo {1}",
shouldMatchPattern: "Šablonas turėtų atitikti: `/{0}/`",
mustBeAnInteger: "Turi būti sveikasis skaičius",
notAValidOption: "Netinkama parinktis",
selectAnOption: "Pasirinkite parinktį",
remove: "Pašalinti",
addValue: "Pridėti reikšmę",
languages: "Kalbos"
},
lv: {
shouldBeEqual: "{0} jābūt vienādam ar {1}",
shouldBeDifferent: "{0} jābūt atšķirīgam no {1}",
shouldMatchPattern: "Mustrim jāsakrīt: `/{0}/`",
mustBeAnInteger: "Jābūt veselam skaitlim",
notAValidOption: "Nav derīga opcija",
selectAnOption: "Izvēlieties opciju",
remove: "Noņemt",
addValue: "Pievienot vērtību",
languages: "Valodas"
},
nl: {
shouldBeEqual: "{0} moet gelijk zijn aan {1}",
shouldBeDifferent: "{0} moet verschillen van {1}",
shouldMatchPattern: "Patroon moet overeenkomen: `/{0}/`",
mustBeAnInteger: "Moet een geheel getal zijn",
notAValidOption: "Geen geldige optie",
selectAnOption: "Selecteer een optie",
remove: "Verwijderen",
addValue: "Waarde toevoegen",
languages: "Talen"
},
no: {
shouldBeEqual: "{0} skal være lik {1}",
shouldBeDifferent: "{0} skal være forskjellig fra {1}",
shouldMatchPattern: "Mønsteret skal matche: `/{0}/`",
mustBeAnInteger: "Må være et heltall",
notAValidOption: "Ikke et gyldig alternativ",
selectAnOption: "Velg et alternativ",
remove: "Fjern",
addValue: "Legg til verdi",
languages: "Språk"
},
pl: {
shouldBeEqual: "{0} powinno być równe {1}",
shouldBeDifferent: "{0} powinno być różne od {1}",
shouldMatchPattern: "Wzór pow inien pasować: `/{0}/`",
mustBeAnInteger: "Musi być liczbą całkowitą",
notAValidOption: "Nieprawidłowa opcja",
selectAnOption: "Wybierz opcję",
remove: "Usuń",
addValue: "Dodaj wartość",
languages: "Języki"
},
pt: {
shouldBeEqual: "{0} deve ser igual a {1}",
shouldBeDifferent: "{0} deve ser diferente de {1}",
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
mustBeAnInteger: "Deve ser um número inteiro",
notAValidOption: "Não é uma opção válida",
selectAnOption: "Selecione uma opção",
remove: "Remover",
addValue: "Adicionar valor",
languages: "Idiomas"
},
"pt-BR": {
shouldBeEqual: "{0} deve ser igual a {1}",
shouldBeDifferent: "{0} deve ser diferente de {1}",
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
mustBeAnInteger: "Deve ser um número inteiro",
notAValidOption: "Não é uma opção válida",
selectAnOption: "Selecione uma opção",
remove: "Remover",
addValue: "Adicionar valor",
languages: "Idiomas"
},
ru: {
shouldBeEqual: "{0} должно быть равно {1}",
shouldBeDifferent: "{0} должно отличаться от {1}",
shouldMatchPattern: "Шаблон должен соответствовать: `/{0}/`",
mustBeAnInteger: "Должно быть целым числом",
notAValidOption: "Недопустимый вариант",
selectAnOption: "Выберите вариант",
remove: "Удалить",
addValue: "Добавить значение",
languages: "Языки"
},
sk: {
shouldBeEqual: "{0} by mal byť rovnaký ako {1}",
shouldBeDifferent: "{0} by mal byť odlišný od {1}",
shouldMatchPattern: "Vzor by mal zodpovedať: `/{0}/`",
mustBeAnInteger: "Musí byť celé číslo",
notAValidOption: "Nie je platná možnosť",
selectAnOption: "Vyberte možnosť",
remove: "Odstrániť",
addValue: "Pridať hodnotu",
languages: "Jazyky"
},
sv: {
shouldBeEqual: "{0} bör vara lika med {1}",
shouldBeDifferent: "{0} bör vara annorlunda än {1}",
shouldMatchPattern: "Mönstret bör matcha: `/{0}/`",
mustBeAnInteger: "Måste vara ett heltal",
notAValidOption: "Inte ett giltigt alternativ",
selectAnOption: "Välj ett alternativ",
remove: "Ta bort",
addValue: "Lägg till värde",
languages: "Språk"
},
th: {
shouldBeEqual: "{0} ควรเท่ากับ {1}",
shouldBeDifferent: "{0} ควรแตกต่างจาก {1}",
shouldMatchPattern: "รูปแบบควรตรงกับ: `/{0}/`",
mustBeAnInteger: "ต้องเป็นจำนวนเต็ม",
notAValidOption: "ไม่ใช่ตัวเลือกที่ถูกต้อง",
selectAnOption: "เลือกตัวเลือก",
remove: "ลบ",
addValue: "เพิ่มค่า",
languages: "ภาษา"
},
tr: {
shouldBeEqual: "{0} {1} eşit olmalıdır",
shouldBeDifferent: "{0} {1} farklı olmalıdır",
shouldMatchPattern: "Desen eşleşmelidir: `/{0}/`",
mustBeAnInteger: "Tam sayı olmalıdır",
notAValidOption: "Geçerli bir seçenek değil",
selectAnOption: "Bir seçenek seçin",
remove: "Kaldır",
addValue: "Değer ekle",
languages: "Diller"
},
uk: {
shouldBeEqual: "{0} повинно бути рівним {1}",
shouldBeDifferent: "{0} повинно відрізнятися від {1}",
shouldMatchPattern: "Шаблон повинен відповідати: `/{0}/`",
mustBeAnInteger: "Повинно бути цілим числом",
notAValidOption: "Не є дійсною опцією",
selectAnOption: "Виберіть опцію",
remove: "Видалити",
addValue: "Додати значення",
languages: "Мови"
},
ka: {
shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის",
shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის",
shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`",
mustBeAnInteger: "უნდა იყოს მთელი რიცხვი",
notAValidOption: "არასწორი ვარიანტი",
selectAnOption: "აირჩიეთ ვარიანტი",
remove: "წაშალეთ",
addValue: "დაამატეთ მნიშვნელობა",
languages: "ენები"
},
"zh-CN": {
shouldBeEqual: "{0} 应该等于 {1}",
shouldBeDifferent: "{0} 应该不同于 {1}",
shouldMatchPattern: "模式应匹配: `/{0}/`",
mustBeAnInteger: "必须是整数",
notAValidOption: "不是有效选项",
selectAnOption: "选择一个选项",
remove: "移除",
addValue: "添加值",
languages: "语言"
},
"zh-TW": {
shouldBeEqual: "{0} 應該等於 {1}",
shouldBeDifferent: "{0} 應該不同於 {1}",
shouldMatchPattern: "模式應匹配: `/{0}/`",
mustBeAnInteger: "必須是整數",
notAValidOption: "不是有效選項",
selectAnOption: "選擇一個選項",
remove: "移除",
addValue: "添加值",
languages: "語言"
}
/* spell-checker: enable */
};
export const accountMultiPageSupportedLanguages = [
"en",
"ar",
"ca",
"cs",
"da",
"de",
"es",
"fi",
"fr",
"hu",
"it",
"ja",
"lt",
"lv",
"nl",
"no",
"pl",
"pt-BR",
"ru",
"sk",
"sv",
"tr",
"zh-CN"
] as const;
const keycloakifyExtraMessages_account: Record<
(typeof accountMultiPageSupportedLanguages)[number],
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
> = {
en: {
newPasswordSameAsOld: "New password must be different from the old one",
passwordConfirmNotMatch: "Password confirmation does not match"
},
/* spell-checker: disable */
ar: {
newPasswordSameAsOld: "يجب أن تكون كلمة المرور الجديدة مختلفة عن القديمة",
passwordConfirmNotMatch: "تأكيد كلمة المرور لا يتطابق"
},
ca: {
newPasswordSameAsOld: "La nova contrasenya ha de ser diferent de l'anterior",
passwordConfirmNotMatch: "La confirmació de la contrasenya no coincideix"
},
cs: {
newPasswordSameAsOld: "Nové heslo musí být odlišné od starého",
passwordConfirmNotMatch: "Potvrzení hesla se neshoduje"
},
da: {
newPasswordSameAsOld: "Det nye kodeord skal være forskelligt fra det gamle",
passwordConfirmNotMatch: "Adgangskodebekræftelse matcher ikke"
},
de: {
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
},
es: {
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
},
fi: {
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
},
fr: {
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
},
hu: {
newPasswordSameAsOld: "Az új jelszónak különböznie kell az előzőtől",
passwordConfirmNotMatch: "A jelszó megerősítése nem egyezik"
},
it: {
newPasswordSameAsOld:
"La nuova password deve essere diversa da quella precedente",
passwordConfirmNotMatch: "La conferma della password non corrisponde"
},
ja: {
newPasswordSameAsOld: "新しいパスワードは古いパスワードと異なる必要があります",
passwordConfirmNotMatch: "パスワード確認が一致しません"
},
lt: {
newPasswordSameAsOld: "Naujas slaptažodis turi skirtis nuo seno",
passwordConfirmNotMatch: "Slaptažodžio patvirtinimas neatitinka"
},
lv: {
newPasswordSameAsOld: "Jaunajam parolam jābūt atšķirīgam no vecā",
passwordConfirmNotMatch: "Paroles apstiprināšana neatbilst"
},
nl: {
newPasswordSameAsOld: "Het nieuwe wachtwoord moet verschillend zijn van het oude",
passwordConfirmNotMatch: "Wachtwoordbevestiging komt niet overeen"
},
no: {
newPasswordSameAsOld: "Det nye passordet må være forskjellig fra det gamle",
passwordConfirmNotMatch: "Passordbekreftelsen stemmer ikke"
},
pl: {
newPasswordSameAsOld: "Nowe hasło musi być inne niż stare",
passwordConfirmNotMatch: "Potwierdzenie hasła nie pasuje"
},
"pt-BR": {
newPasswordSameAsOld: "A nova senha deve ser diferente da antiga",
passwordConfirmNotMatch: "A confirmação da senha não corresponde"
},
ru: {
newPasswordSameAsOld: "Новый пароль должен отличаться от старого",
passwordConfirmNotMatch: "Подтверждение пароля не совпадает"
},
sk: {
newPasswordSameAsOld: "Nové heslo musí byť odlišné od starého",
passwordConfirmNotMatch: "Potvrdenie hesla sa nezhoduje"
},
sv: {
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
},
tr: {
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
},
"zh-CN": {
newPasswordSameAsOld: "新密码必须与旧密码不同",
passwordConfirmNotMatch: "密码确认不匹配"
}
/* spell-checker: enable */
};

View File

@ -1,14 +0,0 @@
import fs from "fs";
import path from "path";
import zodToJsonSchema from "zod-to-json-schema";
import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
const jsonSchemaName = "keycloakifyPackageJsonSchema";
const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
const baseProperties = {
// merges package.json schema with keycloakify properties
"allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
};
fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));

View File

@ -1,79 +1,104 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import * as os from "os";
const singletonDependencies: string[] = ["react", "@types/react"];
const rootDirPath = getProjectRoot();
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
const rootDirPath = getThisCodebaseRootDirPath();
const commonThirdPartyDeps = [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync(
pathJoin(rootDirPath, "dist", "package.json"),
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
{
let modifiedPackageJsonContent = fs
.readFileSync(pathJoin(rootDirPath, "package.json"))
.toString("utf8");
return {
...packageJsonParsed,
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
"exports": !("exports" in packageJsonParsed)
? undefined
: Object.fromEntries(
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
])
)
};
})(),
null,
2
),
"utf8"
)
);
modifiedPackageJsonContent = (() => {
const o = JSON.parse(modifiedPackageJsonContent);
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
delete o.files;
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
return JSON.stringify(o, null, 2);
})();
return [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
})();
modifiedPackageJsonContent = modifiedPackageJsonContent
.replace(/"dist\//g, '"')
.replace(/"\.\/dist\//g, '"./')
.replace(/"!dist\//g, '"!')
.replace(/"!\.\/dist\//g, '"!./');
modifiedPackageJsonContent = JSON.stringify(
{
...JSON.parse(modifiedPackageJsonContent),
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
},
null,
4
);
fs.writeFileSync(
pathJoin(rootDirPath, "dist", "package.json"),
Buffer.from(modifiedPackageJsonContent, "utf8")
);
}
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
if (targetModuleName === undefined) {
const packageJsonFilePath = pathJoin(cwd, "package.json");
const packageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
delete packageJson["packageManager"];
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(packageJson, null, 2))
);
}
const cmd = [
"yarn",
"link",
...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])
].join(" ");
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
env: {
...process.env,
"HOME": yarnGlobalDirPath
...(os.platform() === "win32"
? {
USERPROFILE: yarnGlobalDirPath,
LOCALAPPDATA: yarnGlobalDirPath
}
: { HOME: yarnGlobalDirPath })
}
});
};
@ -89,7 +114,9 @@ const testAppPaths = (() => {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
console.warn(
`Skipping ${testAppName} since it cant be found here: ${testAppPath}`
);
return undefined;
})
@ -101,7 +128,54 @@ if (testAppPaths.length === 0) {
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
testAppPaths.forEach(testAppPath => {
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
scripts?: Record<string, string>;
};
let hasPostInstallOrPrepareScript = false;
if (parsedPackageJson.scripts !== undefined) {
for (const scriptName of ["postinstall", "prepare"]) {
if (parsedPackageJson.scripts[scriptName] === undefined) {
continue;
}
hasPostInstallOrPrepareScript = true;
delete parsedPackageJson.scripts[scriptName];
}
}
if (hasPostInstallOrPrepareScript) {
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
);
}
const restorePackageJson = () => {
if (!hasPostInstallOrPrepareScript) {
return;
}
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
};
try {
execSync("yarn install", { cwd: testAppPath });
} catch (error) {
restorePackageJson();
throw error;
}
restorePackageJson();
});
console.log("=== Linking common dependencies ===");
@ -114,30 +188,54 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
...[
rootDirPath,
"node_modules",
...(commonThirdPartyDep.startsWith("@")
? commonThirdPartyDep.split("/")
: [commonThirdPartyDep])
]
);
execYarnLink({ "cwd": localInstallPath });
execYarnLink({ cwd: localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
cwd: testAppPath,
targetModuleName: commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
execYarnLink({ cwd: pathJoin(rootDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
cwd: testAppPath,
targetModuleName: JSON.parse(
fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8")
)["name"]
})
);
testAppPaths.forEach(testAppPath => {
const { scripts = {} } = JSON.parse(
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
) as {
scripts?: Record<string, string>;
};
for (const scriptName of ["postinstall", "prepare"]) {
if (scripts[scriptName] === undefined) {
continue;
}
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
}
});
export {};

View File

@ -0,0 +1,88 @@
import * as fs from "fs";
import { join as pathJoin, sep as pathSep } from "path";
import { run } from "./shared/run";
import cliSelect from "cli-select";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import chalk from "chalk";
import { removeNodeModules } from "./tools/removeNodeModules";
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
(async () => {
const parentDirPath = pathJoin(getThisCodebaseRootDirPath(), "..");
const { starterName } = await (async () => {
const starterNames = fs
.readdirSync(parentDirPath)
.filter(
basename =>
basename.includes("starter") &&
basename.includes("keycloakify") &&
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
);
if (starterNames.length === 0) {
console.log(
chalk.red(
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
)
);
process.exit(-1);
}
const starterName = await (async () => {
if (starterNames.length === 1) {
return starterNames[0];
}
console.log(chalk.cyan(`\nSelect a starter to link in:`));
const { value } = await cliSelect<string>({
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
}).catch(() => {
process.exit(-1);
});
return value.split(pathSep)[1];
})();
return { starterName };
})();
const startTime = Date.now();
console.log(chalk.cyan(`\n\nLinking in ..${pathSep}${starterName}...`));
removeNodeModules({
nodeModulesDirPath: pathJoin(getThisCodebaseRootDirPath(), "node_modules")
});
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), "dist"), {
recursive: true,
force: true
});
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), ".yarn_home"), {
recursive: true,
force: true
});
run("yarn install");
run("yarn build");
const starterDirPath = pathJoin(parentDirPath, starterName);
removeNodeModules({
nodeModulesDirPath: pathJoin(starterDirPath, "node_modules")
});
run("yarn install", { cwd: starterDirPath });
run(`npx tsx ${pathJoin("scripts", "link-in-app.ts")} ${starterName}`);
const durationSeconds = Math.round((Date.now() - startTime) / 1000);
await new Promise(resolve => setTimeout(resolve, 1000));
startRebuildOnSrcChange();
console.log(chalk.green(`\n\nLinked in ${starterName} in ${durationSeconds}s`));
})();

View File

@ -0,0 +1,9 @@
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
export const cacheDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"node_modules",
".cache",
"scripts"
);

View File

@ -0,0 +1,357 @@
import { relative as pathRelative } from "path";
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
import { join as pathJoin } from "path";
import { assert, type Equals } from "tsafe/assert";
import { cacheDirPath } from "./cacheDirPath";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
const KEYCLOAK_VERSION = {
FOR_LOGIN_THEME: "25.0.4",
FOR_ACCOUNT_MULTI_PAGE: "21.1.2"
} as const;
export async function downloadKeycloakDefaultTheme(params: {
keycloakVersionId: keyof typeof KEYCLOAK_VERSION;
}) {
const { keycloakVersionId } = params;
const keycloakVersion = KEYCLOAK_VERSION[keycloakVersionId];
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath,
fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: getThisCodebaseRootDirPath()
}),
uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles",
onArchiveFile: async params => {
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
if (fileRelativePath.startsWith("..")) {
return;
}
const { readFile, writeFile } = params;
if (
!fileRelativePath.startsWith("base") &&
!fileRelativePath.startsWith("keycloak")
) {
return;
}
switch (keycloakVersion) {
case KEYCLOAK_VERSION.FOR_LOGIN_THEME:
if (
!fileRelativePath.startsWith(pathJoin("base", "login")) &&
!fileRelativePath.startsWith(pathJoin("keycloak", "login")) &&
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
) {
return;
}
if (fileRelativePath.endsWith(".ftl")) {
return;
}
break;
case KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE:
if (
!fileRelativePath.startsWith(pathJoin("base", "account")) &&
!fileRelativePath.startsWith(pathJoin("keycloak", "account")) &&
!fileRelativePath.startsWith(pathJoin("keycloak", "common"))
) {
return;
}
break;
default:
assert<Equals<typeof keycloakVersion, never>>(false);
}
last_account_v1_transformations: {
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE) {
break last_account_v1_transformations;
}
skip_web_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "web_modules")
)
) {
break skip_web_modules;
}
return;
}
skip_lib: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "lib")
)
) {
break skip_lib;
}
return;
}
skip_node_modules: {
const nodeModulesRelativeDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
)
]);
}
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
nodeModulesRelativeDirPath.length + 1
);
if (
kcNodeModulesKeepFilePaths_lastAccountV1.has(
fileRelativeToNodeModulesPath
)
) {
break skip_node_modules;
}
return;
}
patch_account_css: {
if (
fileRelativePath !==
pathJoin("keycloak", "account", "resources", "css", "account.css")
) {
break patch_account_css;
}
await writeFile({
fileRelativePath,
modifiedData: Buffer.from(
(await readFile())
.toString("utf8")
.replace("top: -34px;", "top: -34px !important;"),
"utf8"
)
});
return;
}
}
skip_unused_resources: {
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_LOGIN_THEME) {
break skip_unused_resources;
}
skip_node_modules: {
const nodeModulesRelativeDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths === undefined) {
kcNodeModulesKeepFilePaths = new Set([
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"fontawesome-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.ttf"
),
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
pathJoin("jquery", "dist", "jquery.min.js"),
pathJoin("rfc4648", "lib", "rfc4648.js")
]);
}
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
nodeModulesRelativeDirPath.length + 1
);
if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) {
break skip_node_modules;
}
return;
}
skip_vendor: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "vendor")
)
) {
break skip_vendor;
}
return;
}
skip_rollup_config: {
if (
fileRelativePath !==
pathJoin("keycloak", "common", "resources", "rollup.config.js")
) {
break skip_rollup_config;
}
return;
}
skip_package_json: {
if (
fileRelativePath !==
pathJoin("keycloak", "common", "resources", "package.json")
) {
break skip_package_json;
}
return;
}
}
await writeFile({ fileRelativePath });
}
});
return { extractedDirPath };
}

8
scripts/shared/run.ts Normal file
View File

@ -0,0 +1,8 @@
import * as child_process from "child_process";
import chalk from "chalk";
export function run(command: string, options?: { cwd: string }) {
console.log(chalk.grey(`$ ${command}`));
child_process.execSync(command, { stdio: "inherit", ...options });
}

View File

@ -0,0 +1,40 @@
import * as child_process from "child_process";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import chokidar from "chokidar";
import * as runExclusive from "run-exclusive";
import { Deferred } from "evt/tools/Deferred";
import chalk from "chalk";
export function startRebuildOnSrcChange() {
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
const runYarnBuild = runExclusive.build(async () => {
console.log(chalk.green("Running `yarn build`"));
const dCompleted = new Deferred<void>();
const child = child_process.spawn("yarn", ["build"], { shell: true });
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", () => dCompleted.resolve());
await dCompleted.pr;
console.log("\n\n");
});
console.log(chalk.green("Watching for changes in src/"));
chokidar
.watch(["src", "stories"], { ignoreInitial: true })
.on("all", async (event, path) => {
console.log(chalk.bold(`${event}: ${path}`));
await waitForDebounce();
runYarnBuild();
});
}

View File

@ -0,0 +1,21 @@
import * as child_process from "child_process";
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
import { run } from "./shared/run";
(async () => {
run("yarn build");
{
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
shell: true
});
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", process.exit.bind(process));
}
startRebuildOnSrcChange();
})();

View File

@ -1,29 +0,0 @@
import { execSync } from "child_process";
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
import path from "path";
const testDir = "keycloakify_starter_test";
if (existsSync(path.join(process.cwd(), testDir))) {
rmSync(path.join(process.cwd(), testDir), { recursive: true });
}
// Build and link package
execSync("yarn build");
const pkgJSON = JSON.parse(readFileSync(path.join(process.cwd(), "package.json")).toString("utf8"));
pkgJSON.main = "./index.js";
pkgJSON.types = "./index.d.ts";
pkgJSON.scripts.prepare = undefined;
writeFileSync(path.join(process.cwd(), "dist", "package.json"), JSON.stringify(pkgJSON));
// Wrapped in a try/catch because unlink errors if the package isn't linked
try {
execSync("yarn unlink");
} catch {}
execSync("yarn link", { "cwd": path.join(process.cwd(), "dist") });
// Clone latest keycloakify-starter and link to keycloakify output
execSync(`git clone https://github.com/keycloakify/keycloakify-starter.git ${testDir}`);
execSync("yarn install", { "cwd": path.join(process.cwd(), testDir) });
execSync("yarn link keycloakify", { "cwd": path.join(process.cwd(), testDir) });
//Ensure keycloak theme can be built
execSync("yarn build-keycloak-theme", { "cwd": path.join(process.cwd(), testDir) });

View File

@ -0,0 +1,27 @@
import * as fs from "fs";
import { crawl } from "../../src/bin/tools/crawl";
export function removeNodeModules(params: { nodeModulesDirPath: string }) {
const { nodeModulesDirPath } = params;
try {
fs.rmSync(nodeModulesDirPath, { recursive: true, force: true });
} catch {
// NOTE: This is a workaround for windows
// we can't remove locked executables.
crawl({
dirPath: nodeModulesDirPath,
returnedPathsType: "absolute"
}).forEach(filePath => {
try {
fs.rmSync(filePath, { force: true });
} catch (error) {
if (filePath.endsWith(".exe")) {
return;
}
throw error;
}
});
}
}

23
src/PUBLIC_URL.ts Normal file
View File

@ -0,0 +1,23 @@
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
import { assert } from "tsafe/assert";
/**
* This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects.
* This works both in your main app and in your Keycloak theme.
*/
export const PUBLIC_URL = (() => {
const kcContext: { "x-keycloakify": { resourcesPath: string } } | undefined = (
window as any
).kcContext;
if (kcContext === undefined || process.env.NODE_ENV === "development") {
assert(
process.env.PUBLIC_URL !== undefined,
`If you use keycloakify/PUBLIC_URL you should be in Webpack and thus process.env.PUBLIC_URL should be defined`
);
return process.env.PUBLIC_URL;
}
return `${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}`;
})();

View File

@ -0,0 +1,41 @@
import { lazy, Suspense } from "react";
import { assert, type Equals } from "tsafe/assert";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "keycloakify/account/KcContext";
import { I18n } from "keycloakify/account/i18n";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
const Log = lazy(() => import("keycloakify/account/pages/Log"));
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
export default function DefaultPage(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "password.ftl":
return <Password kcContext={kcContext} {...rest} />;
case "sessions.ftl":
return <Sessions kcContext={kcContext} {...rest} />;
case "account.ftl":
return <Account kcContext={kcContext} {...rest} />;
case "totp.ftl":
return <Totp kcContext={kcContext} {...rest} />;
case "applications.ftl":
return <Applications kcContext={kcContext} {...rest} />;
case "log.ftl":
return <Log kcContext={kcContext} {...rest} />;
case "federatedIdentity.ftl":
return <FederatedIdentity kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}
</Suspense>
);
}

View File

@ -1,26 +0,0 @@
import { lazy, Suspense } from "react";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { I18n } from "keycloakify/account/i18n";
import type { KcContext } from "./kcContext";
import { assert, type Equals } from "tsafe/assert";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "password.ftl":
return <Password kcContext={kcContext} {...rest} />;
case "account.ftl":
return <Account kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}
</Suspense>
);
}

View File

@ -0,0 +1,310 @@
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
export type ExtendKcContext<
KcContextExtension extends { properties?: Record<string, string | undefined> },
KcContextExtensionPerPage extends Record<string, Record<string, unknown>>
> = ValueOf<{
[PageId in keyof KcContextExtensionPerPage | KcContext["pageId"]]: Extract<
KcContext,
{ pageId: PageId }
> extends never
? KcContext.Common &
KcContextExtension & {
pageId: PageId;
} & KcContextExtensionPerPage[PageId]
: Extract<KcContext, { pageId: PageId }> &
KcContextExtension &
KcContextExtensionPerPage[PageId];
}>;
export type KcContext =
| KcContext.Password
| KcContext.Account
| KcContext.Sessions
| KcContext.Totp
| KcContext.Applications
| KcContext.Log
| KcContext.FederatedIdentity;
export declare namespace KcContext {
export type Common = {
themeVersion: string;
keycloakifyVersion: string;
themeType: "account";
themeName: string;
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
url: {
accountUrl: string;
passwordUrl: string;
totpUrl: string;
socialUrl: string;
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
logoutUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
referrerURI?: string;
getLogoutUrl: () => string;
};
features: {
passwordUpdateSupported: boolean;
identityFederation: boolean;
log: boolean;
authorization: boolean;
};
realm: {
internationalizationEnabled: boolean;
userManagedAccessAllowed: boolean;
};
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
referrer?: {
url: string; // The url of the App
name: string; // Client id
};
messagesPerField: {
/**
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
*
* @param fieldName to check for
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(
fieldName: string,
text: T
) => T | undefined;
/**
* Check if exists error message for given fields
*
* @param fields
* @return boolean
*/
existsError: (fieldName: string) => boolean;
/**
* Get message for given field.
*
* @param fieldName
* @return message text or empty string
*/
get: (fieldName: string) => string;
/**
* Check if message for given field exists
*
* @param field
* @return boolean
*/
exists: (fieldName: string) => boolean;
};
account: {
email?: string;
firstName: string;
lastName?: string;
username?: string;
};
properties: {};
"x-keycloakify": {
messages: Record<string, string>;
};
};
export type Password = Common & {
pageId: "password.ftl";
password: {
passwordSet: boolean;
};
stateChecker: string;
};
export type Account = Common & {
pageId: "account.ftl";
url: {
accountUrl: string;
};
realm: {
registrationEmailAsUsername: boolean;
editUsernameAllowed: boolean;
};
stateChecker: string;
};
export type Sessions = Common & {
pageId: "sessions.ftl";
sessions: {
sessions: {
expires: string;
clients: string[];
ipAddress: string;
started: string;
lastAccess: string;
id: string;
}[];
};
stateChecker: string;
};
export type Totp = Common & {
pageId: "totp.ftl";
totp: {
enabled: boolean;
totpSecretEncoded: string;
qrUrl: string;
policy: {
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
getAlgorithmKey: () => string;
} & (
| {
type: "totp";
period: number;
}
| {
type: "hotp";
initialCounter: number;
}
);
supportedApplications: string[];
totpSecretQrCode: string;
manualUrl: string;
totpSecret: string;
otpCredentials: { id: string; userLabel: string }[];
};
mode?: "qr" | "manual" | undefined | null;
isAppInitiatedAction: boolean;
stateChecker: string;
};
export type Applications = Common & {
pageId: "applications.ftl";
features: {
log: boolean;
identityFederation: boolean;
authorization: boolean;
passwordUpdateSupported: boolean;
};
stateChecker: string;
applications: {
applications: {
realmRolesAvailable: {
name: string;
description: string;
compositesStream?: Record<string, unknown>;
clientRole?: boolean;
composite?: boolean;
id?: string;
containerId?: string;
attributes?: Record<string, unknown>;
}[];
resourceRolesAvailable: Record<
string,
{
roleName: string;
roleDescription?: string;
clientName: string;
clientId: string;
}[]
>;
additionalGrants: string[];
clientScopesGranted: string[];
effectiveUrl?: string;
client: {
alwaysDisplayInConsole: boolean;
attributes: Record<string, unknown>;
authenticationFlowBindingOverrides: Record<string, unknown>;
baseUrl?: string;
bearerOnly: boolean;
clientAuthenticatorType: string;
clientId: string;
consentRequired: boolean;
consentScreenText: string;
description: string;
directAccessGrantsEnabled: boolean;
displayOnConsentScreen: boolean;
dynamicScope: boolean;
enabled: boolean;
frontchannelLogout: boolean;
fullScopeAllowed: boolean;
id: string;
implicitFlowEnabled: boolean;
includeInTokenScope: boolean;
managementUrl: string;
name?: string;
nodeReRegistrationTimeout: string;
notBefore: string;
protocol: string;
protocolMappersStream: Record<string, unknown>;
publicClient: boolean;
realm: Record<string, unknown>;
realmScopeMappingsStream: Record<string, unknown>;
redirectUris: string[];
registeredNodes: Record<string, unknown>;
rolesStream: Record<string, unknown>;
rootUrl?: string;
scopeMappingsStream: Record<string, unknown>;
secret: string;
serviceAccountsEnabled: boolean;
standardFlowEnabled: boolean;
surrogateAuthRequired: boolean;
webOrigins: string[];
};
}[];
};
};
export type Log = Common & {
pageId: "log.ftl";
log: {
events: {
date: string | number | Date;
event: string;
ipAddress: string;
client: string;
details: { value: string; key: string }[];
}[];
};
};
export type FederatedIdentity = Common & {
pageId: "federatedIdentity.ftl";
stateChecker: string;
federatedIdentity: {
identities: {
providerId: string;
displayName: string;
userName: string;
connected: boolean;
}[];
removeLinkPossible: boolean;
};
};
}
{
type Got = KcContext["pageId"];
type Expected = AccountThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -0,0 +1,69 @@
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
import type { AccountThemePageId } from "keycloakify/bin/shared/constants";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { exclude } from "tsafe/exclude";
export function createGetKcContextMock<
KcContextExtension extends { properties?: Record<string, string | undefined> },
KcContextExtensionPerPage extends Record<`${string}.ftl`, Record<string, unknown>>
>(params: {
kcContextExtension: KcContextExtension;
kcContextExtensionPerPage: KcContextExtensionPerPage;
overrides?: DeepPartial<KcContextExtension & KcContextBase.Common>;
overridesPerPage?: {
[PageId in AccountThemePageId | keyof KcContextExtensionPerPage]?: DeepPartial<
Extract<
ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>,
{ pageId: PageId }
>
>;
};
}) {
const {
kcContextExtension,
kcContextExtensionPerPage,
overrides: overrides_global,
overridesPerPage: overridesPerPage_global
} = params;
type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;
function getKcContextMock<
PageId extends AccountThemePageId | keyof KcContextExtensionPerPage
>(params: {
pageId: PageId;
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
}): Extract<KcContext, { pageId: PageId }> {
const { pageId, overrides } = params;
const kcContextMock = structuredCloneButFunctions(
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
...kcContextCommonMock,
pageId
}
);
[
kcContextExtension,
kcContextExtensionPerPage[pageId],
overrides_global,
overridesPerPage_global?.[pageId],
overrides
]
.filter(exclude(undefined))
.forEach(overrides =>
deepAssign({
target: kcContextMock,
source: overrides
})
);
// @ts-expect-error
return kcContextMock;
}
return { getKcContextMock };
}

View File

@ -0,0 +1,2 @@
export type { ExtendKcContext, KcContext } from "./KcContext";
export { createGetKcContextMock } from "./getKcContextMock";

View File

@ -0,0 +1,213 @@
import "keycloakify/tools/Object.fromEntries";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
import { assert, type Equals } from "tsafe/assert";
import type { LanguageTag } from "keycloakify/account/i18n/messages_defaultSet/types";
const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES}/account`;
export const kcContextCommonMock: KcContext.Common = {
themeVersion: "0.0.0",
keycloakifyVersion: "0.0.0",
themeType: "account",
themeName: "my-theme-name",
url: {
resourcesPath,
resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`,
resourceUrl: "#",
accountUrl: "#",
applicationsUrl: "#",
logoutUrl: "#",
getLogoutUrl: () => "#",
logUrl: "#",
passwordUrl: "#",
sessionsUrl: "#",
socialUrl: "#",
totpUrl: "#"
},
realm: {
internationalizationEnabled: true,
userManagedAccessAllowed: true
},
messagesPerField: {
printIfExists: () => {
return undefined;
},
existsError: () => false,
get: key => `Fake error for ${key}`,
exists: () => false
},
locale: {
supported: (
[
/* spell-checker: disable */
["de", "Deutsch"],
["no", "Norsk"],
["ru", "Русский"],
["sv", "Svenska"],
["pt-BR", "Português (Brasil)"],
["lt", "Lietuvių"],
["en", "English"],
["it", "Italiano"],
["fr", "Français"],
["zh-CN", "中文简体"],
["es", "Español"],
["cs", "Čeština"],
["ja", "日本語"],
["sk", "Slovenčina"],
["pl", "Polski"],
["ca", "Català"],
["nl", "Nederlands"],
["tr", "Türkçe"],
["ar", "العربية"],
["da", "Dansk"],
["fi", "Suomi"],
["hu", "Magyar"],
["lv", "Latviešu"]
/* spell-checker: enable */
] as const
).map(([languageTag, label]) => {
{
type Got = typeof languageTag;
type Expected = LanguageTag;
type Missing = Exclude<Expected, Got>;
type Unexpected = Exclude<Got, Expected>;
assert<Equals<Missing, never>>;
assert<Equals<Unexpected, never>>;
}
return {
languageTag,
label,
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
} as const;
}),
currentLanguageTag: "en"
},
features: {
authorization: true,
identityFederation: true,
log: true,
passwordUpdateSupported: true
},
referrer: undefined,
account: {
firstName: "john",
lastName: "doe",
email: "john.doe@code.gouv.fr",
username: "doe_j"
},
properties: {},
"x-keycloakify": {
messages: {}
}
};
export const kcContextMocks: KcContext[] = [
id<KcContext.Password>({
...kcContextCommonMock,
pageId: "password.ftl",
password: {
passwordSet: true
},
stateChecker: "state checker"
}),
id<KcContext.Account>({
...kcContextCommonMock,
pageId: "account.ftl",
url: {
...kcContextCommonMock.url,
referrerURI: "#",
accountUrl: "#"
},
realm: {
...kcContextCommonMock.realm,
registrationEmailAsUsername: true,
editUsernameAllowed: true
},
stateChecker: ""
}),
id<KcContext.Sessions>({
...kcContextCommonMock,
pageId: "sessions.ftl",
sessions: {
sessions: [
{
ipAddress: "127.0.0.1",
started: new Date().toString(),
lastAccess: new Date().toString(),
expires: new Date().toString(),
clients: ["Chrome", "Firefox"],
id: "f8951177-817d-4a70-9c02-86d3c170fe51"
}
]
},
stateChecker: "g6WB1FaYnKotTkiy7ZrlxvFztSqS0U8jvHsOOOb2z4g"
}),
id<KcContext.Totp>({
...kcContextCommonMock,
pageId: "totp.ftl",
totp: {
enabled: true,
totpSecretEncoded: "KVVF G2BY N4YX S6LB IUYT K2LH IFYE 4SBV",
qrUrl: "#",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACM0lEQVR4Xu3OIZJgOQwDUDFd2UxiurLAVnnbHw4YGDKtSiWOn4Gxf81//7r/+q8b4HfLGBZDK9d85NmNR+sB42sXvOYrN5P1DcgYYFTGfOlbzE8gzwy3euweGizw7cfdl34/GRhlkxjKNV+5AebPXPORX1JuB9x8ZfbyyD2y1krWAKsbMq1HnqQDaLfa77p4+MqvzEGSqvSAD/2IHW2yHaigR9tX3m8dDIYGcNf3f+gDpVBZbZU77zyJ6Rlcy+qoTMG887KAPD9hsh6a1Sv3gJUHGHUAxSMzj7zqDDe7Phmt2eG+8UsMxjRGm816MAO+8VMl1R1jGHOrZB/5Zo/WXAPgxixm9Mo96vDGrM1eOto8c4Ax4wF437mifOXlpiPzCnN7Y9l95NnEMxgMY9AAGA8fucH14Y1aVb6N/cqrmyh0BVht7k1e+bU8LK0Cg5vmVq9c5vHIjOfqxDIfeTraNVTwewa4wVe+SW5N+uP1qACeudUZbqGOfA6VZV750Noq2Xx3kpveV44ZelSV1V7KFHzkWyVrrlUwG0Pl9pWnoy3vsQoME6vKI69i5osVgwWzHT7zjmJtMcNUSVn1oYMd7ZodbgowZl45VG0uVuLPUr1yc79uaQBag/mqR34xhlWyHm1prplHboCWdZ4TeZjsK8+dI+jbz1C5hl65mcpgB5dhcj8+dGO+0Ko68+lD37JDD83dpDLzzK+TrQyaVwGj6pUboGV+7+AyN8An/pf84/7rv/4/1l4OCc/1BYMAAAAASUVORK5CYII=",
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
supportedApplications: [
"totpAppFreeOTPName",
"totpAppMicrosoftAuthenticatorName",
"totpAppGoogleName"
],
policy: {
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,
type: "totp",
period: 30,
getAlgorithmKey: () => "SHA1"
}
},
mode: "qr",
isAppInitiatedAction: false,
stateChecker: ""
}),
id<KcContext.Log>({
...kcContextCommonMock,
pageId: "log.ftl",
log: {
events: [
{
date: "2/21/2024, 1:28:39 PM",
event: "login",
ipAddress: "172.17.0.1",
client: "security-admin-console",
details: [{ key: "openid-connect", value: "admin" }]
}
]
}
}),
id<KcContext.FederatedIdentity>({
...kcContextCommonMock,
stateChecker: "",
pageId: "federatedIdentity.ftl",
federatedIdentity: {
identities: [
{
providerId: "keycloak-oidc",
displayName: "keycloak-oidc",
userName: "John",
connected: true
}
],
removeLinkPossible: true
}
})
];

View File

@ -1,32 +1,39 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { type TemplateProps } from "keycloakify/account/TemplateProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "./kcContext";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import { useSetClassName } from "keycloakify/tools/useSetClassName";
import { useInitialize } from "keycloakify/account/Template.useInitialize";
import type { TemplateProps } from "keycloakify/account/TemplateProps";
import type { I18n } from "./i18n";
import { assert } from "keycloakify/tools/assert";
import type { KcContext } from "./KcContext";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext;
const { url, features, realm, message, referrer } = kcContext;
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
"styles": [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
useEffect(() => {
document.title = msgStr("accountManagementTitle");
}, []);
useSetClassName({
qualifiedName: "html",
className: kcClsx("kcHtmlClass")
});
if (!isReady) {
useSetClassName({
qualifiedName: "body",
className: clsx("admin-console", "user", kcClsx("kcBodyClass"))
});
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
if (!isReadyToRender) {
return null;
}
@ -42,20 +49,16 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div className="navbar-collapse navbar-collapse-1">
<div className="container">
<ul className="nav navbar-nav navbar-utility">
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
{enabledLanguages.length > 1 && (
<li>
<div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
{currentLanguage.label}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
{enabledLanguages.map(({ languageTag, label, href }) => (
<li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
<a href={href}>{label}</a>
</li>
))}
</ul>
@ -121,7 +124,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className="pficon pficon-ok"></span>}
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
<span className="kc-feedback-text">{message.summary}</span>
<span
className="kc-feedback-text"
dangerouslySetInnerHTML={{
__html: kcSanitize(message.summary)
}}
/>
</div>
)}

View File

@ -0,0 +1,35 @@
import { assert } from "keycloakify/tools/assert";
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import type { KcContext } from "keycloakify/account/KcContext";
export type KcContextLike = {
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
};
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
assert<KcContext extends KcContextLike ? true : false>();
export function useInitialize(params: {
kcContext: KcContextLike;
doUseDefaultCss: boolean;
}) {
const { kcContext, doUseDefaultCss } = params;
const { url } = kcContext;
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
componentOrHookName: "Template",
hrefs: !doUseDefaultCss
? []
: [
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
`${url.resourcesPath}/css/account.css`
]
});
return { isReadyToRender: areAllStyleSheetsLoaded };
}

View File

@ -1,14 +1,14 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
import type { ClassKey } from "keycloakify/account/lib/kcClsx";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
export type TemplateProps<KcContext, I18n> = {
kcContext: KcContext;
i18n: I18nExtended;
i18n: I18n;
doUseDefaultCss: boolean;
active: string;
classes?: Partial<Record<ClassKey, string>>;
children: ReactNode;
active: string;
};
export type ClassKey = "kcHtmlClass" | "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
export type { ClassKey };

View File

@ -1,233 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import { useEffect, useState, useRef } from "react";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContext } from "../kcContext/KcContext";
import { Markdown } from "keycloakify/tools/Markdown";
export const fallbackLanguageTag = "en";
export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
};
};
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type GenericI18n<MessageKey extends string> = {
/**
* e.g: "en", "fr", "zh-CN"
*
* The current language
*/
currentLanguageTag: string;
/**
* To call when the user switch language.
* This will cause the page to be reloaded,
* on next load currentLanguageTag === newLanguageTag
*/
changeLocale: (newLanguageTag: string) => never;
/**
* e.g. "en" => "English", "fr" => "Français", ...
*
* Used to render a select that enable user to switch language.
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
* */
labelBySupportedLanguageTag: Record<string, string>;
/**
* Examples assuming currentLanguageTag === "en"
*
* msg("access-denied") === <span>Access denied</span>
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
*/
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
/**
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
*/
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
*/
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*/
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
};
export type I18n = GenericI18n<MessageKey>;
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
const { kcContext } = params;
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
const refHasStartedFetching = useRef(false);
useEffect(() => {
if (refHasStartedFetching.current) {
return;
}
refHasStartedFetching.current = true;
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
assert(false, "never");
},
"labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
})();
}, []);
return i18n ?? null;
}
return { useI18n };
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
messages: Record<MessageKey, string>;
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
const { fallbackMessages, messages } = params;
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderMarkdown } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
if (messageOrUndefined === undefined) {
return undefined;
}
const message = messageOrUndefined;
const messageWithArgsInjectedIfAny = (() => {
const startIndex = message
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
// No {0} in message (no arguments expected)
return message;
}
let messageWithArgsInjected = message;
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return messageWithArgsInjected;
})();
return doRenderMarkdown ? (
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
{messageWithArgsInjectedIfAny}
</Markdown>
) : (
messageWithArgsInjectedIfAny
);
}
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
const { key, args, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
doRenderMarkdown
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
return {
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
};
}
const keycloakifyExtraMessages = {
"en": {
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option",
"newPasswordSameAsOld": "New password must be different from the old one",
"passwordConfirmNotMatch": "Password confirmation does not match"
},
"fr": {
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être égal à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entier",
"notAValidOption": "N'est pas une option valide",
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter",
"newPasswordSameAsOld": "Le nouveau mot de passe doit être différent de l'ancien",
"passwordConfirmNotMatch": "La confirmation du mot de passe ne correspond pas"
/* spell-checker: enable */
}
};

View File

@ -1 +0,0 @@
export type { I18n } from "./i18n";

View File

@ -1,10 +1,3 @@
import Fallback from "keycloakify/account/Fallback";
export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export type { AccountThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";
export type { ExtendKcContext } from "keycloakify/account/KcContext";
export type { ClassKey } from "keycloakify/account/TemplateProps";
export { i18nBuilder, type MessageKey_defaultSet } from "keycloakify/account/i18n";

View File

@ -1,126 +0,0 @@
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
themeVersion: string;
keycloakifyVersion: string;
themeType: "account";
themeName: string;
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
url: {
accountUrl: string;
passwordUrl: string;
totpUrl: string;
socialUrl: string;
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
referrerURI?: string;
getLogoutUrl: () => string;
};
features: {
passwordUpdateSupported: boolean;
identityFederation: boolean;
log: boolean;
authorization: boolean;
};
realm: {
internationalizationEnabled: boolean;
userManagedAccessAllowed: boolean;
};
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
referrer?: {
url: string; // The url of the App
name: string; // Client id
};
messagesPerField: {
/**
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
*
* @param fieldName to check for
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
/**
* Check if exists error message for given fields
*
* @param fields
* @return boolean
*/
existsError: (fieldName: string) => boolean;
/**
* Get message for given field.
*
* @param fieldName
* @return message text or empty string
*/
get: (fieldName: string) => string;
/**
* Check if message for given field exists
*
* @param field
* @return boolean
*/
exists: (fieldName: string) => boolean;
};
account: {
email?: string;
firstName: string;
lastName?: string;
username?: string;
};
};
export type Password = Common & {
pageId: "password.ftl";
password: {
passwordSet: boolean;
};
stateChecker: string;
};
export type Account = Common & {
pageId: "account.ftl";
url: {
accountUrl: string;
};
realm: {
registrationEmailAsUsername: boolean;
editUsernameAllowed: boolean;
};
stateChecker: string;
};
}
{
type Got = KcContext["pageId"];
type Expected = AccountThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -1,102 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}) {
const { mockData } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
if (mockDataPick !== undefined) {
deepAssign({
"target": out,
"source": mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
"target": out,
"source": storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "account") {
return { "kcContext": undefined as any };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
return { "kcContext": realKcContext as any };
}
return { getKcContext };
}

View File

@ -1,21 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const { getKcContext } = createGetKcContext({
mockData
});
const { kcContext } = getKcContext({ mockPageId });
return { kcContext };
}

View File

@ -1,11 +0,0 @@
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
}

View File

@ -1 +0,0 @@
export type { KcContext } from "./KcContext";

View File

@ -1,175 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
const PUBLIC_URL = (typeof process !== "object" ? undefined : process.env?.["PUBLIC_URL"]) || "/";
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"themeName": "my-theme-name",
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
"getLogoutUrl": () => "#",
"logUrl": "#",
"passwordUrl": "#",
"sessionsUrl": "#",
"socialUrl": "#",
"totpUrl": "#"
},
"realm": {
"internationalizationEnabled": true,
"userManagedAccessAllowed": true
},
"messagesPerField": {
"printIfExists": () => {
return undefined;
},
"existsError": () => false,
"get": key => `Fake error for ${key}`,
"exists": () => false
},
"locale": {
"supported": [
/* spell-checker: disable */
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr"
}
/* spell-checker: enable */
],
"currentLanguageTag": "en"
},
"features": {
"authorization": true,
"identityFederation": true,
"log": true,
"passwordUpdateSupported": true
},
"referrer": undefined,
"account": {
"firstName": "john",
"lastName": "doe",
"email": "john.doe@code.gouv.fr",
"username": "doe_j"
}
};
export const kcContextMocks: KcContext[] = [
id<KcContext.Password>({
...kcContextCommonMock,
"pageId": "password.ftl",
"password": {
"passwordSet": true
},
"stateChecker": "state checker"
}),
id<KcContext.Account>({
...kcContextCommonMock,
"pageId": "account.ftl",
"url": {
...kcContextCommonMock.url,
"referrerURI": "#",
"accountUrl": "#"
},
"realm": {
...kcContextCommonMock.realm,
"registrationEmailAsUsername": true,
"editUsernameAllowed": true
},
"stateChecker": ""
})
];

37
src/account/lib/kcClsx.ts Normal file
View File

@ -0,0 +1,37 @@
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
export type ClassKey =
| "kcHtmlClass"
| "kcBodyClass"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonLargeClass"
| "kcButtonDefaultClass"
| "kcContentWrapperClass"
| "kcFormClass"
| "kcFormGroupClass"
| "kcInputWrapperClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass";
export const { getKcClsx } = createGetKcClsx<ClassKey>({
defaultClasses: {
kcHtmlClass: undefined,
kcBodyClass: undefined,
kcButtonClass: "btn",
kcContentWrapperClass: "row",
kcButtonPrimaryClass: "btn-primary",
kcButtonLargeClass: "btn-lg",
kcButtonDefaultClass: "btn-default",
kcFormClass: "form-horizontal",
kcFormGroupClass: "form-group",
kcInputWrapperClass: "col-xs-12 col-sm-12 col-md-12 col-lg-12",
kcLabelClass: "control-label",
kcInputClass: "form-control",
kcInputErrorMessageClass:
"pf-c-form__helper-text pf-m-error required kc-feedback-text"
}
});
export type KcClsx = ReturnType<typeof getKcClsx>["kcClsx"];

View File

@ -1,13 +0,0 @@
import { createUseClassName } from "keycloakify/lib/useGetClassName";
import type { ClassKey } from "keycloakify/account/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcHtmlClass": undefined,
"kcBodyClass": undefined,
"kcButtonClass": "btn",
"kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg",
"kcButtonDefaultClass": "btn-default"
}
});

View File

@ -1,18 +1,20 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcContext, i18n, doUseDefaultCss, Template } = props;
const { getClassName } = useGetClassName({
const classes = {
...props.classes,
kcBodyClass: clsx(props.classes?.kcBodyClass, "user")
};
const { kcClsx } = getKcClsx({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
}
classes
});
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
@ -102,11 +104,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
name="submitAction"
value="Save"
>
@ -114,11 +112,7 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
</button>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass")
)}
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
name="submitAction"
value="Cancel"
>

View File

@ -0,0 +1,136 @@
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
const {
url,
applications: { applications },
stateChecker
} = kcContext;
const { msg, advancedMsg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="applications">
<div className="row">
<div className="col-md-10">
<h2>{msg("applicationsHtmlTitle")}</h2>
</div>
<form action={url.applicationsUrl} method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<input type="hidden" id="referrer" name="referrer" value={stateChecker} />
<table className="table table-striped table-bordered">
<thead>
<tr>
<td>{msg("application")}</td>
<td>{msg("availableRoles")}</td>
<td>{msg("grantedPermissions")}</td>
<td>{msg("additionalGrants")}</td>
<td>{msg("action")}</td>
</tr>
</thead>
<tbody>
{applications.map(application => (
<tr key={application.client.clientId}>
<td>
{application.effectiveUrl && (
<a href={application.effectiveUrl}>
{(application.client.name && advancedMsg(application.client.name)) || application.client.clientId}
</a>
)}
{!application.effectiveUrl &&
((application.client.name && advancedMsg(application.client.name)) || application.client.clientId)}
</td>
<td>
{!isArrayWithEmptyObject(application.realmRolesAvailable) &&
application.realmRolesAvailable.map((role, index) => (
<span key={role.name}>
{role.description ? advancedMsg(role.description) : advancedMsg(role.name)}
{index < application.realmRolesAvailable.length - 1 && ", "}
</span>
))}
{application.resourceRolesAvailable &&
Object.keys(application.resourceRolesAvailable).map(resource => (
<span key={resource}>
{!isArrayWithEmptyObject(application.realmRolesAvailable) && ", "}
{application.resourceRolesAvailable[resource].map(clientRole => (
<span key={clientRole.roleName}>
{clientRole.roleDescription
? advancedMsg(clientRole.roleDescription)
: advancedMsg(clientRole.roleName)}{" "}
{msg("inResource")}{" "}
<strong>
{clientRole.clientName ? advancedMsg(clientRole.clientName) : clientRole.clientId}
</strong>
{clientRole !==
application.resourceRolesAvailable[resource][
application.resourceRolesAvailable[resource].length - 1
] && ", "}
</span>
))}
</span>
))}
</td>
<td>
{application.client.consentRequired ? (
application.clientScopesGranted.map(claim => (
<span key={claim}>
{advancedMsg(claim)}
{claim !== application.clientScopesGranted[application.clientScopesGranted.length - 1] && ", "}
</span>
))
) : (
<strong>{msg("fullAccess")}</strong>
)}
</td>
<td>
{application.additionalGrants.map(grant => (
<span key={grant}>
{advancedMsg(grant)}
{grant !== application.additionalGrants[application.additionalGrants.length - 1] && ", "}
</span>
))}
</td>
<td>
{(application.client.consentRequired && application.clientScopesGranted.length > 0) ||
application.additionalGrants.length > 0 ? (
<button
type="submit"
className={kcClsx("kcButtonPrimaryClass", "kcButtonClass")}
id={`revoke-${application.client.clientId}`}
name="clientId"
value={application.client.id}
>
{msg("revoke")}
</button>
) : null}
</td>
</tr>
))}
</tbody>
</table>
</form>
</div>
</Template>
);
}
function isArrayWithEmptyObject(variable: any): boolean {
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
}

View File

@ -0,0 +1,58 @@
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, federatedIdentity, stateChecker } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="social">
<div className="main-layout social">
<div className="row">
<div className="col-md-10">
<h2>{msg("federatedIdentitiesHtmlTitle")}</h2>
</div>
</div>
<div id="federated-identities">
{federatedIdentity.identities.map(identity => (
<div key={identity.providerId} className="row margin-bottom">
<div className="col-sm-2 col-md-2">
<label htmlFor={identity.providerId} className="control-label">
{identity.displayName}
</label>
</div>
<div className="col-sm-5 col-md-5">
<input disabled className="form-control" value={identity.userName} />
</div>
<div className="col-sm-5 col-md-5">
{identity.connected ? (
federatedIdentity.removeLinkPossible && (
<form action={url.socialUrl} method="post" className="form-inline">
<input type="hidden" name="stateChecker" value={stateChecker} />
<input type="hidden" name="action" value="remove" />
<input type="hidden" name="providerId" value={identity.providerId} />
<button id={`remove-link-${identity.providerId}`} className="btn btn-default">
{msg("doRemove")}
</button>
</form>
)
) : (
<form action={url.socialUrl} method="post" className="form-inline">
<input type="hidden" name="stateChecker" value={stateChecker} />
<input type="hidden" name="action" value="add" />
<input type="hidden" name="providerId" value={identity.providerId} />
<button id={`add-link-${identity.providerId}`} className="btn btn-default">
{msg("doAdd")}
</button>
</form>
)}
</div>
</div>
))}
</div>
</div>
</Template>
);
}

70
src/account/pages/Log.tsx Normal file
View File

@ -0,0 +1,70 @@
import type { Key } from "react";
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
const { log } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
<div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10">
<h2>{msg("accountLogHtmlTitle")}</h2>
</div>
<table className="table table-striped table-bordered">
<thead>
<tr>
<td>{msg("date")}</td>
<td>{msg("event")}</td>
<td>{msg("ip")}</td>
<td>{msg("client")}</td>
<td>{msg("details")}</td>
</tr>
</thead>
<tbody>
{log.events.map(
(
event: {
date: string | number | Date;
event: string;
ipAddress: string;
client: any;
details: any[];
},
index: Key | null | undefined
) => (
<tr key={index}>
<td>{event.date ? new Date(event.date).toLocaleString() : ""}</td>
<td>{event.event}</td>
<td>{event.ipAddress}</td>
<td>{event.client || ""}</td>
<td>
{event.details.map((detail, detailIndex) => (
<span key={detailIndex}>
{`${detail.key} = ${detail.value}`}
{detailIndex < event.details.length - 1 && ", "}
</span>
))}
</td>
</tr>
)
)}
</tbody>
</table>
</div>
</Template>
);
}

View File

@ -1,11 +1,11 @@
import type { LazyExoticComponent } from "react";
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { JSX } from "keycloakify/tools/JSX";
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
export type PageProps<KcContext, I18nExtended extends I18n> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18nExtended;
export type PageProps<NarrowedKcContext, I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: NarrowedKcContext;
i18n: I18n;
doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>;
};

View File

@ -1,19 +1,21 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcContext, i18n, doUseDefaultCss, Template } = props;
const { getClassName } = useGetClassName({
const classes = {
...props.classes,
kcBodyClass: clsx(props.classes?.kcBodyClass, "password")
};
const { kcClsx } = getKcClsx({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
}
classes
});
const { url, password, account, stateChecker } = kcContext;
@ -57,18 +59,18 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
{...{
kcContext: {
...kcContext,
"message": (() => {
message: (() => {
if (newPasswordError !== "") {
return {
"type": "error",
"summary": newPasswordError
type: "error",
summary: newPasswordError
};
}
if (newPasswordConfirmError !== "") {
return {
"type": "error",
"summary": newPasswordConfirmError
type: "error",
summary: newPasswordConfirmError
};
}
@ -98,7 +100,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none" }}
style={{ display: "none" }}
/>
{password.passwordSet && (
@ -192,11 +194,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
<button
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
name="submitAction"
value="Save"
>

View File

@ -0,0 +1,64 @@
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
const { url, stateChecker, sessions } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
<div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10">
<h2>{msg("sessionsHtmlTitle")}</h2>
</div>
</div>
<table className="table table-striped table-bordered">
<thead>
<tr>
<th>{msg("ip")}</th>
<th>{msg("started")}</th>
<th>{msg("lastAccess")}</th>
<th>{msg("expires")}</th>
<th>{msg("clients")}</th>
</tr>
</thead>
<tbody role="rowgroup">
{sessions.sessions.map((session, index: number) => (
<tr key={index}>
<td>{session.ipAddress}</td>
<td>{session?.started}</td>
<td>{session?.lastAccess}</td>
<td>{session?.expires}</td>
<td>
{session.clients.map((client: string, clientIndex: number) => (
<div key={clientIndex}>
{client}
<br />
</div>
))}
</td>
</tr>
))}
</tbody>
</table>
<form action={url.sessionsUrl} method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<button id="logout-all-sessions" type="submit" className={kcClsx("kcButtonDefaultClass", "kcButtonClass")}>
{msg("doLogOutAllSessions")}
</button>
</form>
</Template>
);
}

226
src/account/pages/Totp.tsx Normal file
View File

@ -0,0 +1,226 @@
import { clsx } from "keycloakify/tools/clsx";
import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
const { msg, msgStr, advancedMsg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
<>
<div className="row">
<div className="col-md-10">
<h2>{msg("authenticatorTitle")}</h2>
</div>
{totp.otpCredentials.length === 0 && (
<div className="subtitle col-md-2">
<span className="required">*</span>
{msg("requiredFields")}
</div>
)}
</div>
{totp.enabled && (
<table className="table table-bordered table-striped">
<thead>
{totp.otpCredentials.length > 1 ? (
<tr>
<th colSpan={4}>{msg("configureAuthenticators")}</th>
</tr>
) : (
<tr>
<th colSpan={3}>{msg("configureAuthenticators")}</th>
</tr>
)}
</thead>
<tbody>
{totp.otpCredentials.map((credential, index) => (
<tr key={index}>
<td className="provider">{msg("mobile")}</td>
{totp.otpCredentials.length > 1 && <td className="provider">{credential.id}</td>}
<td className="provider">{credential.userLabel || ""}</td>
<td className="action">
<form action={url.totpUrl} method="post" className="form-inline">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<input type="hidden" id="submitAction" name="submitAction" value="Delete" />
<input type="hidden" id="credentialId" name="credentialId" value={credential.id} />
<button id={`remove-mobile-${index}`} className="btn btn-default">
<i className="pficon pficon-delete"></i>
</button>
</form>
</td>
</tr>
))}
</tbody>
</table>
)}
{!totp.enabled && (
<div>
<hr />
<ol id="kc-totp-settings">
<li>
<p>{msg("totpStep1")}</p>
<ul id="kc-totp-supported-apps">{totp.supportedApplications?.map(app => <li key={app}>{advancedMsg(app)}</li>)}</ul>
</li>
{mode && mode == "manual" ? (
<>
<li>
<p>{msg("totpManualStep2")}</p>
<p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
</p>
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("totpScanBarcode")}
</a>
</p>
</li>
<li>
<p>{msg("totpManualStep3")}</p>
<ul>
<li id="kc-totp-type">
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li>
<li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("totpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("totpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</li>
</>
) : (
<li>
<p>{msg("totpStep2")}</p>
<p>
<img
id="kc-totp-secret-qr-code"
src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
alt="Figure: Barcode"
/>
</p>
<p>
<a href={totp.manualUrl} id="mode-manual">
{msg("totpUnableToScan")}
</a>
</p>
</li>
)}
<li>
<p>{msg("totpStep3")}</p>
<p>{msg("totpStep3DeviceName")}</p>
</li>
</ol>
<hr />
<form action={url.totpUrl} className={kcClsx("kcFormClass")} id="kc-totp-settings-form" method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<div className={kcClsx("kcFormGroupClass")}>
<div className="col-sm-2 col-md-2">
<label htmlFor="totp" className="control-label">
{msg("authenticatorCode")}
</label>
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input
type="text"
id="totp"
name="totp"
autoComplete="off"
className={kcClsx("kcInputClass")}
aria-invalid={messagesPerField.existsError("totp")}
/>
{messagesPerField.existsError("totp") && (
<span
id="input-error-otp-code"
className={kcClsx("kcInputErrorMessageClass")}
aria-live="polite"
dangerouslySetInnerHTML={{
__html: kcSanitize(messagesPerField.get("totp"))
}}
/>
)}
</div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div>
<div className={kcClsx("kcFormGroupClass")}>
<div className="col-sm-2 col-md-2">
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
{msg("totpDeviceName")}
</label>
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div>
<div className="col-sm-10 col-md-10">
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={kcClsx("kcInputClass")}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span
id="input-error-otp-label"
className={kcClsx("kcInputErrorMessageClass")}
aria-live="polite"
dangerouslySetInnerHTML={{
__html: kcSanitize(messagesPerField.get("userLabel"))
}}
/>
)}
</div>
</div>
<div id="kc-form-buttons" className={clsx(kcClsx("kcFormGroupClass"), "text-right")}>
<div className={kcClsx("kcInputWrapperClass")}>
<input
type="submit"
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
id="saveTOTPBtn"
value={msgStr("doSave")}
/>
<button
type="submit"
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
id="cancelTOTPBtn"
name="submitAction"
value="Cancel"
>
{msg("doCancel")}
</button>
</div>
</div>
</form>
</div>
)}
</>
</Template>
);
}

165
src/bin/add-story.ts Normal file
View File

@ -0,0 +1,165 @@
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import {
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId,
type AccountThemePageId,
THEME_TYPES
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "add-story",
buildContext
});
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
const values = THEME_TYPES.filter(themeType => {
switch (themeType) {
case "account":
return buildContext.implementedThemeTypes.account.isImplemented;
case "login":
return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
}
assert<Equals<typeof themeType, never>>(false);
});
assert(values.length > 0, "No theme is implemented in this project");
if (values.length === 1) {
return values[0];
}
const { value } = await cliSelect({
values
}).catch(() => {
process.exit(-1);
});
return value;
})();
if (
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page")
) {
console.log(
`${chalk.red("✗")} Sorry, there is no Storybook support for Single-Page Account themes.`
);
process.exit(0);
return;
}
if (themeType === "admin") {
console.log(
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
);
process.exit(0);
return;
}
console.log(`${themeType}`);
console.log(chalk.cyan("Select the page you want to create a Storybook for:"));
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
values: (() => {
switch (themeType) {
case "login":
return [...LOGIN_THEME_PAGE_IDS];
case "account":
return [...ACCOUNT_THEME_PAGE_IDS];
}
assert<Equals<typeof themeType, never>>(false);
})()
}).catch(() => {
process.exit(-1);
});
console.log(`${pageId}`);
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
/ftl$/,
"stories.tsx"
);
const targetFilePath = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"pages",
componentBasename
);
if (fs.existsSync(targetFilePath)) {
console.log(`${pathRelative(process.cwd(), targetFilePath)} already exists`);
process.exit(-1);
}
let sourceCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
"stories",
themeType,
"pages",
componentBasename
)
)
.toString("utf8")
.replace('import React from "react";\n', "")
.replace(/from "[./]+dist\//, 'from "keycloakify/');
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetFilePath,
sourceCode: sourceCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { recursive: true });
}
}
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
console.log(
[
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
)} copy pasted from the Keycloakify source code into your project`,
`You can start storybook with ${chalk.bold("npm run storybook")}`
].join("\n")
);
}

View File

@ -1,49 +1,96 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { themeTypes } from "./keycloakify/generateFtl";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import type { BuildContext } from "./shared/buildContext";
import { transformCodebase } from "./tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
(async () => {
const projectDirPath = process.cwd();
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2),
"projectDirPath": process.cwd()
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
if (hasBeenHandled) {
return;
}
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
projectDirPath,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeType": themeType,
"themeDirPath": keycloakDirInPublicDir,
"usedResources": undefined
});
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`);
})();
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -1,97 +0,0 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as child_process from "child_process";
import * as fs from "fs";
export async function downloadBuiltinKeycloakTheme(params: { projectDirPath: string; keycloakVersion: string; destDirPath: string }) {
const { projectDirPath, keycloakVersion, destDirPath } = params;
await downloadAndUnzip({
"doUseCache": true,
projectDirPath,
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
"preCacheTransform": {
"actionCacheId": "npm install and build",
"action": async ({ destDirPath }) => {
install_common_node_modules: {
const commonResourcesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources");
if (!fs.existsSync(commonResourcesDirPath)) {
break install_common_node_modules;
}
if (!fs.existsSync(pathJoin(commonResourcesDirPath, "package.json"))) {
break install_common_node_modules;
}
if (fs.existsSync(pathJoin(commonResourcesDirPath, "node_modules"))) {
break install_common_node_modules;
}
child_process.execSync("npm install --omit=dev", {
"cwd": commonResourcesDirPath,
"stdio": "ignore"
});
}
install_and_move_to_common_resources_generated_in_keycloak_v2: {
const accountV2DirSrcDirPath = pathJoin(destDirPath, "keycloak.v2", "account", "src");
if (!fs.existsSync(accountV2DirSrcDirPath)) {
break install_and_move_to_common_resources_generated_in_keycloak_v2;
}
child_process.execSync("npm install", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
const packageJsonFilePath = pathJoin(accountV2DirSrcDirPath, "package.json");
const packageJsonRaw = fs.readFileSync(packageJsonFilePath);
const parsedPackageJson = JSON.parse(packageJsonRaw.toString("utf8"));
parsedPackageJson.scripts.build = parsedPackageJson.scripts.build
.replace("npm run check-types", "true")
.replace("npm run babel", "true");
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
child_process.execSync("npm run build", { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
}
}
}
});
}
async function main() {
const buildOptions = readBuildOptions({
"projectDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({
"projectDirPath": process.cwd(),
keycloakVersion,
destDirPath
});
}
if (require.main === module) {
main();
}

View File

@ -1,65 +0,0 @@
#!/usr/bin/env node
import { getProjectRoot } from "./tools/getProjectRoot";
import cliSelect from "cli-select";
import {
loginThemePageIds,
accountThemePageIds,
type LoginThemePageId,
type AccountThemePageId,
themeTypes,
type ThemeType
} from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize";
import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getSrcDirPath";
(async () => {
console.log("Select a theme type");
const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes]
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
console.log("Select a page you would like to eject");
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
"values": (() => {
switch (themeType) {
case "login":
return [...loginThemePageIds];
case "account":
return [...accountThemePageIds];
}
assert<Equals<typeof themeType, never>>(false);
})()
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
const { themeSrcDirPath } = getThemeSrcDirPath({ "projectDirPath": process.cwd() });
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
if (existsSync(targetFilePath)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
process.exit(-1);
}
await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();

332
src/bin/eject-page.ts Normal file
View File

@ -0,0 +1,332 @@
#!/usr/bin/env node
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import {
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId,
type AccountThemePageId,
THEME_TYPES
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "eject-page",
buildContext
});
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
const values = THEME_TYPES.filter(themeType => {
switch (themeType) {
case "account":
return buildContext.implementedThemeTypes.account.isImplemented;
case "login":
return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
}
assert<Equals<typeof themeType, never>>(false);
});
assert(values.length > 0, "No theme is implemented in this project");
if (values.length === 1) {
return values[0];
}
const { value } = await cliSelect({
values
}).catch(() => {
process.exit(-1);
});
return value;
})();
if (themeType === "admin") {
console.log("Use `npx keycloakify own` command instead, see documentation");
process.exit(-1);
}
if (
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page")
) {
console.log(
chalk.yellow(
[
"You are implementing a Single-Page Account theme.",
"The eject-page command isn't applicable in this context"
].join("\n")
)
);
process.exit(1);
return;
}
console.log(`${themeType}`);
console.log(chalk.cyan("Select the page you want to customize:"));
const templateValue = "Template.tsx (Layout common to every page)";
const userProfileFormFieldsValue =
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
const otherPageValue = "The page you're looking for isn't listed here";
const { value: pageIdOrComponent } = await cliSelect<
| LoginThemePageId
| AccountThemePageId
| typeof templateValue
| typeof userProfileFormFieldsValue
| typeof otherPageValue
>({
values: (() => {
switch (themeType) {
case "login":
return [
templateValue,
userProfileFormFieldsValue,
...LOGIN_THEME_PAGE_IDS,
otherPageValue
];
case "account":
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
}
assert<Equals<typeof themeType, never>>(false);
})()
}).catch(() => {
process.exit(-1);
});
if (pageIdOrComponent === otherPageValue) {
console.log(
[
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
].join(" ")
);
process.exit(0);
}
console.log(`${pageIdOrComponent}`);
const componentBasename = (() => {
if (pageIdOrComponent === templateValue) {
return "Template.tsx";
}
if (pageIdOrComponent === userProfileFormFieldsValue) {
return "UserProfileFormFields.tsx";
}
return capitalize(kebabCaseToCamelCase(pageIdOrComponent)).replace(/ftl$/, "tsx");
})();
const pagesOrDot = (() => {
if (
pageIdOrComponent === templateValue ||
pageIdOrComponent === userProfileFormFieldsValue
) {
return ".";
}
return "pages";
})();
const targetFilePath = pathJoin(
buildContext.themeSrcDirPath,
themeType,
pagesOrDot,
componentBasename
);
if (fs.existsSync(targetFilePath)) {
console.log(
`${pageIdOrComponent} is already ejected, ${pathRelative(
process.cwd(),
targetFilePath
)} already exists`
);
process.exit(-1);
}
let componentCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
themeType,
pagesOrDot,
componentBasename
)
)
.toString("utf8");
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
componentCode = await runPrettier({
filePath: targetFilePath,
sourceCode: componentCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { recursive: true });
}
}
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
console.log(
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
)} copy pasted from the Keycloakify source code into your project`
);
edit_KcPage: {
if (
pageIdOrComponent !== templateValue &&
pageIdOrComponent !== userProfileFormFieldsValue
) {
break edit_KcPage;
}
const kcAppTsxPath = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"KcPage.tsx"
);
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
const modifiedKcAppTsxCode = (() => {
switch (pageIdOrComponent) {
case templateValue:
return kcAppTsxCode.replace(
`keycloakify/${themeType}/Template`,
"./Template"
);
case userProfileFormFieldsValue:
return kcAppTsxCode.replace(
`keycloakify/login/UserProfileFormFields`,
"./UserProfileFormFields"
);
}
assert<Equals<typeof pageIdOrComponent, never>>(false);
})();
if (kcAppTsxCode === modifiedKcAppTsxCode) {
console.log(
chalk.red(
"Unable to automatically update KcPage.tsx, please update it manually"
)
);
return;
}
fs.writeFileSync(kcAppTsxPath, Buffer.from(modifiedKcAppTsxCode, "utf8"));
console.log(
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), kcAppTsxPath))
)} Updated`
);
return;
}
const userProfileFormFieldComponentName = "UserProfileFormFields";
const componentName = componentBasename.replace(/.tsx$/, "");
console.log(
[
``,
`You now need to update your page router:`,
``,
`${chalk.bold(
pathJoin(
".",
pathRelative(process.cwd(), buildContext.themeSrcDirPath),
themeType,
"KcPage.tsx"
)
)}:`,
chalk.grey("```"),
`// ...`,
``,
chalk.green(
`+const ${componentName} = lazy(() => import("./pages/${componentName}"));`
),
...[
``,
` export default function KcPage(props: { kcContext: KcContext; }) {`,
``,
` // ...`,
``,
` return (`,
` <Suspense>`,
` {(() => {`,
` switch (kcContext.pageId) {`,
` // ...`,
`+ case "${pageIdOrComponent}": return (`,
`+ <${componentName}`,
`+ {...{ kcContext, i18n, classes }}`,
`+ Template={Template}`,
`+ doUseDefaultCss={true}`,
...(!componentCode.includes(userProfileFormFieldComponentName)
? []
: [
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`,
`+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}`
]),
`+ />`,
`+ );`,
` default: return <Fallback /* .. */ />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
` }`
].map(line => {
if (line.startsWith("+")) {
return chalk.green(line);
}
if (line.startsWith("-")) {
return chalk.red(line);
}
return chalk.grey(line);
}),
chalk.grey("```")
].join("\n")
);
}

View File

@ -1,47 +0,0 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./keycloakify/generateFtl";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({ "dirPath": srcDirPath, "returnedPathsType": "relative to dirPath" })
.map(fileRelativePath => {
for (const themeSrcDirBasename of themeSrcDirBasenames) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { "themeSrcDirPath": srcDirPath };
}
console.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme or src/keycloak_theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
process.exit(-1);
}

View File

@ -0,0 +1 @@
export * from "./initialize-account-theme";

View File

@ -0,0 +1,94 @@
import type { BuildContext } from "../shared/buildContext";
import cliSelect from "cli-select";
import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
const { value: accountThemeType } = await cliSelect({
values: ["Single-Page" as const, "Multi-Page" as const]
}).catch(() => {
process.exit(-1);
});
switch (accountThemeType) {
case "Multi-Page":
{
if (
fs.existsSync(accountThemeSrcDirPath) &&
fs.readdirSync(accountThemeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
accountThemeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"multi-page-boilerplate"
),
accountThemeSrcDirPath,
{ recursive: true }
);
}
break;
case "Single-Page":
{
const { initializeSpa } = await import("../shared/initializeSpa");
await initializeSpa({
themeType: "account",
buildContext
});
}
break;
}
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
await updateKcGenCommand({
buildContext: {
...buildContext,
implementedThemeTypes: {
...buildContext.implementedThemeTypes,
account: {
isImplemented: true,
type: accountThemeType
}
}
}
});
}

View File

@ -0,0 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { ExtendKcContext } from "keycloakify/account";
import type { KcEnvName, ThemeName } from "../kc.gen";
export type KcContextExtension = {
themeName: ThemeName;
properties: Record<KcEnvName, string> & {};
};
export type KcContextExtensionPerPage = {};
export type KcContext = ExtendKcContext<KcContextExtension, KcContextExtensionPerPage>;

View File

@ -0,0 +1,25 @@
import { Suspense } from "react";
import type { ClassKey } from "keycloakify/account";
import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
import DefaultPage from "keycloakify/account/DefaultPage";
import Template from "keycloakify/account/Template";
export default function KcPage(props: { kcContext: KcContext }) {
const { kcContext } = props;
const { i18n } = useI18n({ kcContext });
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
default:
return <DefaultPage kcContext={kcContext} i18n={i18n} classes={classes} Template={Template} doUseDefaultCss={true} />;
}
})()}
</Suspense>
);
}
const classes = {} satisfies { [key in ClassKey]?: string };

View File

@ -0,0 +1,38 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import type { KcContext } from "./KcContext";
import { createGetKcContextMock } from "keycloakify/account/KcContext";
import type { KcContextExtension, KcContextExtensionPerPage } from "./KcContext";
import KcPage from "./KcPage";
import { themeNames, kcEnvDefaults } from "../kc.gen";
const kcContextExtension: KcContextExtension = {
themeName: themeNames[0],
properties: {
...kcEnvDefaults
}
};
const kcContextExtensionPerPage: KcContextExtensionPerPage = {};
export const { getKcContextMock } = createGetKcContextMock({
kcContextExtension,
kcContextExtensionPerPage,
overrides: {},
overridesPerPage: {}
});
export function createKcPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
const { pageId } = params;
function KcPageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
const { kcContext: overrides } = props;
const kcContextMock = getKcContextMock({
pageId,
overrides
});
return <KcPage kcContext={kcContextMock} />;
}
return { KcPageStory };
}

View File

@ -0,0 +1,10 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { i18nBuilder } from "keycloakify/account";
import type { ThemeName } from "../kc.gen";
/** @see: https://docs.keycloakify.dev/features/i18n */
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
type I18n = typeof ofTypeI18n;
export { useI18n, type I18n };

View File

@ -0,0 +1,101 @@
import { join as pathJoin } from "path";
import { assert, type Equals, is } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs";
import chalk from "chalk";
import { z } from "zod";
import { id } from "tsafe/id";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
projectDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function updateAccountThemeImplementationInConfig(params: {
buildContext: BuildContextLike;
accountThemeType: "Single-Page" | "Multi-Page";
}) {
const { buildContext, accountThemeType } = params;
switch (buildContext.bundler) {
case "vite":
{
const viteConfigPath = pathJoin(
buildContext.projectDirPath,
"vite.config.ts"
);
if (!fs.existsSync(viteConfigPath)) {
console.log(
chalk.bold(
`You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite config`
)
);
break;
}
const viteConfigContent = fs
.readFileSync(viteConfigPath)
.toString("utf8");
const modifiedViteConfigContent = viteConfigContent.replace(
/accountThemeImplementation\s*:\s*"none"/,
`accountThemeImplementation: "${accountThemeType}"`
);
if (modifiedViteConfigContent === viteConfigContent) {
console.log(
chalk.bold(
`You must manually set the accountThemeImplementation to "${accountThemeType}" in your vite.config.ts`
)
);
break;
}
fs.writeFileSync(viteConfigPath, modifiedViteConfigContent);
}
break;
case "webpack":
{
const parsedPackageJson = (() => {
type ParsedPackageJson = {
keycloakify: Record<string, unknown>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
keycloakify: z.record(z.unknown())
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
parsedPackageJson.keycloakify.accountThemeImplementation =
accountThemeType;
fs.writeFileSync(
buildContext.packageJsonFilePath,
Buffer.from(JSON.stringify(parsedPackageJson, undefined, 4), "utf8")
);
}
break;
}
}

View File

@ -0,0 +1,39 @@
import type { BuildContext } from "./shared/buildContext";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { initializeSpa } from "./shared/initializeSpa";
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
import { command as updateKcGenCommand } from "./update-kc-gen";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "initialize-admin-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
await initializeSpa({
themeType: "admin",
buildContext
});
await updateKcGenCommand({
buildContext: {
...buildContext,
implementedThemeTypes: {
...buildContext.implementedThemeTypes,
admin: {
isImplemented: true
}
}
}
});
}

View File

@ -1,62 +1,156 @@
#!/usr/bin/env node
import type { BuildContext } from "./shared/buildContext";
import cliSelect from "cli-select";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getSrcDirPath";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
import { npmInstall } from "./tools/npmInstall";
import * as child_process from "child_process";
import { z } from "zod";
import chalk from "chalk";
export async function main() {
const projectDirPath = process.cwd();
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { isSilent } = readBuildOptions({
projectDirPath,
"processArgv": process.argv.slice(2)
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
const logger = getLogger({ isSilent });
if (hasBeenHandled) {
return;
}
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) {
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
if (
fs.existsSync(emailThemeSrcDirPath) &&
fs.readdirSync(emailThemeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
emailThemeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
const { keycloakVersion } = await promptKeycloakVersion();
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
projectDirPath,
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath
const { value: emailThemeType } = await cliSelect({
values: [
"native (FreeMarker)" as const,
"Another email templating solution" as const
]
}).catch(() => {
process.exit(-1);
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": emailThemeSrcDirPath
});
if (emailThemeType === "Another email templating solution") {
console.log(
[
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
"https://docs.keycloakify.dev/theme-types/email-theme"
].join("\n")
);
{
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
process.exit(0);
}
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string | undefined>;
dependencies?: Record<string, string | undefined>;
devDependencies?: Record<string, string | undefined>;
};
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
if (require.main === module) {
main();
const zTargetType = z.object({
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
addSyncExtensionsToPostinstallScript({
parsedPackageJson,
buildContext
});
const moduleName = `@keycloakify/email-native`;
const [version] = ((): string[] => {
const cmdOutput = child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim();
const versions = JSON.parse(cmdOutput) as string | string[];
// NOTE: Bug in some older npm versions
if (typeof versions === "string") {
return [versions];
}
return versions;
})()
.reverse()
.filter(version => !version.includes("-"));
assert(version !== undefined);
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
if (parsedPackageJson.devDependencies !== undefined) {
delete parsedPackageJson.devDependencies[moduleName];
}
{
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: buildContext.packageJsonFilePath
});
}
fs.writeFileSync(
buildContext.packageJsonFilePath,
Buffer.from(sourceCode, "utf8")
);
}
await npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});
console.log(chalk.green("Email theme initialized."));
}

View File

@ -1,138 +0,0 @@
import { assert } from "tsafe/assert";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
import { typeGuard } from "tsafe/typeGuard";
import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import { join as pathJoin, sep as pathSep } from "path";
import parseArgv from "minimist";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = {
isSilent: boolean;
themeVersion: string;
themeName: string;
extraThemeNames: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
bundler: Bundler;
keycloakVersionDefaultAssets: string;
/** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
/** If your app is hosted under a subpath, it's the case in CRA if you have "homepage": "https://example.com/my-app" in your package.json
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
};
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params;
const { isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv);
return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false
};
})();
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets, extraThemeNames = [] } = keycloakify ?? {};
const themeName =
keycloakify.themeName ??
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
extraThemeNames,
"bundler": (() => {
const { KEYCLOAKIFY_BUNDLER } = process.env;
assert(
typeGuard<Bundler | undefined>(KEYCLOAKIFY_BUNDLER, [undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
);
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
extraThemeProperties,
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
"reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build");
}
if (pathSep === "\\") {
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
}
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, reactAppBuildDirPath);
}
return reactAppBuildDirPath;
})(),
"keycloakifyBuildDirPath": (() => {
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build_keycloak");
}
if (pathSep === "\\") {
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
}
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
}
return keycloakifyBuildDirPath;
})(),
"urlPathname": (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (url === undefined) {
return undefined;
}
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
}

View File

@ -0,0 +1,265 @@
import { assert, type Equals } from "tsafe/assert";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
import { join as pathJoin, dirname as pathDirname } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext";
import * as fs from "fs/promises";
import {
generatePom,
BuildContextLike as BuildContextLike_generatePom
} from "./generatePom";
import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside";
import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync";
import { existsAsync } from "../../tools/fs.existsAsync";
export type BuildContextLike = BuildContextLike_generatePom & {
keycloakifyBuildDirPath: string;
themeNames: string[];
artifactId: string;
themeVersion: string;
cacheDirPath: string;
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJar(params: {
jarFileBasename: string;
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
resourcesDirPath: string;
doesImplementAccountV1Theme: boolean;
buildContext: BuildContextLike;
}): Promise<void> {
const {
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
doesImplementAccountV1Theme,
buildContext
} = params;
const keycloakifyBuildCacheDirPath = pathJoin(
buildContext.cacheDirPath,
"maven",
jarFileBasename.replace(".jar", "")
);
const tmpResourcesDirPath = pathJoin(
keycloakifyBuildCacheDirPath,
"src",
"main",
"resources"
);
rmSync(tmpResourcesDirPath, { recursive: true, force: true });
transformCodebase({
srcDirPath: resourcesDirPath,
destDirPath: tmpResourcesDirPath,
transformSourceCode:
!doesImplementAccountV1Theme || keycloakAccountV1Version !== null
? undefined
: (params: {
fileRelativePath: string;
sourceCode: Buffer;
}): { modifiedSourceCode: Buffer } | undefined => {
const { fileRelativePath, sourceCode } = params;
if (
isInside({
dirPath: pathJoin("theme", "account-v1"),
filePath: fileRelativePath
})
) {
return undefined;
}
for (const themeName of buildContext.themeNames) {
if (
fileRelativePath ===
pathJoin("theme", themeName, "account", "theme.properties")
) {
const modifiedSourceCode = Buffer.from(
sourceCode
.toString("utf8")
.replace(`parent=account-v1`, "parent=keycloak"),
"utf8"
);
assert(
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
);
return { modifiedSourceCode };
}
}
return { modifiedSourceCode: sourceCode };
}
});
{
const filePath = pathJoin(
tmpResourcesDirPath,
"META-INF",
"keycloak-themes.json"
);
await fs.mkdir(pathDirname(filePath));
await fs.writeFile(
filePath,
Buffer.from(
JSON.stringify(
{
themes: await (async () => {
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
const themeNames = (await fs.readdir(dirPath)).sort(
(a, b) => {
const indexA = buildContext.themeNames.indexOf(a);
const indexB = buildContext.themeNames.indexOf(b);
const orderA = indexA === -1 ? Infinity : indexA;
const orderB = indexB === -1 ? Infinity : indexB;
return orderA - orderB;
}
);
return Promise.all(
themeNames.map(async themeName => {
const types = await fs.readdir(
pathJoin(dirPath, themeName)
);
return {
name: themeName,
types
};
})
);
})()
},
null,
2
),
"utf8"
)
);
}
route_legacy_pages: {
if (!buildContext.implementedThemeTypes.login.isImplemented) {
break route_legacy_pages;
}
await Promise.all(
(["register.ftl", "login-update-profile.ftl"] as const)
.map(pageId =>
buildContext.themeNames.map(async themeName => {
const ftlFilePath = pathJoin(
tmpResourcesDirPath,
"theme",
themeName,
"login",
pageId
);
// NOTE: https://github.com/keycloakify/keycloakify/issues/665
if (!(await existsAsync(ftlFilePath))) {
return;
}
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
const ftlFileBasename = (() => {
switch (pageId) {
case "register.ftl":
return "register-user-profile.ftl";
case "login-update-profile.ftl":
return "update-user-profile.ftl";
}
assert<Equals<typeof pageId, never>>(false);
})();
const modifiedFtlFileContent = ftlFileContent.replace(
`"ftlTemplateFileName": "${pageId}"`,
`"ftlTemplateFileName": "${ftlFileBasename}"`
);
assert(modifiedFtlFileContent !== ftlFileContent);
await fs.writeFile(
pathJoin(pathDirname(ftlFilePath), ftlFileBasename),
Buffer.from(modifiedFtlFileContent, "utf8")
);
})
)
.flat()
);
}
{
const { pomFileCode } = generatePom({
buildContext,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
});
await fs.writeFile(
pathJoin(keycloakifyBuildCacheDirPath, "pom.xml"),
Buffer.from(pomFileCode, "utf8")
);
}
{
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
await new Promise<void>((resolve, reject) =>
child_process.exec(
mvnBuildCmd,
{ cwd: keycloakifyBuildCacheDirPath },
error => {
if (error !== null) {
console.error(
[
`Build jar failed: ${JSON.stringify(
{
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
},
null,
2
)}`,
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
].join("\n")
);
reject(error);
return;
}
resolve();
}
)
);
}
await fs.rename(
pathJoin(
keycloakifyBuildCacheDirPath,
"target",
`${buildContext.artifactId}-${buildContext.themeVersion}.jar`
),
pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename)
);
}

View File

@ -0,0 +1,68 @@
import { assert } from "tsafe/assert";
import {
keycloakAccountV1Versions,
keycloakThemeAdditionalInfoExtensionVersions
} from "./extensionVersions";
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext";
export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string;
keycloakifyBuildDirPath: string;
implementedThemeTypes: BuildContext["implementedThemeTypes"];
jarTargets: BuildContext["jarTargets"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: {
resourcesDirPath: string;
buildContext: BuildContextLike;
}): Promise<void> {
const { resourcesDirPath, buildContext } = params;
const doesImplementAccountV1Theme =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Multi-Page";
await Promise.all(
keycloakAccountV1Versions
.map(keycloakAccountV1Version =>
keycloakThemeAdditionalInfoExtensionVersions.map(
keycloakThemeAdditionalInfoExtensionVersion => {
const keycloakVersionRange = getKeycloakVersionRangeForJar({
doesImplementAccountV1Theme,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
});
if (keycloakVersionRange === undefined) {
return undefined;
}
const jarTarget = buildContext.jarTargets.find(
jarTarget =>
jarTarget.keycloakVersionRange === keycloakVersionRange
);
if (jarTarget === undefined) {
return undefined;
}
const { jarFileBasename } = jarTarget;
return buildJar({
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
doesImplementAccountV1Theme,
buildContext
});
}
)
)
.flat()
);
}

View File

@ -0,0 +1,17 @@
// NOTE: v0.5 is a dummy version.
export const keycloakAccountV1Versions = [null, "0.3", "0.4", "0.6"] as const;
/**
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1
* https://github.com/p2-inc/keycloak-account-v1
*/
export type KeycloakAccountV1Version = (typeof keycloakAccountV1Versions)[number];
export const keycloakThemeAdditionalInfoExtensionVersions = [null, "1.1.5"] as const;
/**
* https://central.sonatype.com/artifact/dev.jcputney/keycloak-theme-additional-info-extension
* https://github.com/jcputney/keycloak-theme-additional-info-extension
* */
export type KeycloakThemeAdditionalInfoExtensionVersion =
(typeof keycloakThemeAdditionalInfoExtensionVersions)[number];

View File

@ -0,0 +1,94 @@
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
export type BuildContextLike = {
groupId: string;
artifactId: string;
themeVersion: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function generatePom(params: {
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
buildContext: BuildContextLike;
}) {
const {
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
buildContext
} = params;
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${buildContext.groupId}</groupId>`,
` <artifactId>${buildContext.artifactId}</artifactId>`,
` <version>${buildContext.themeVersion}</version>`,
` <name>${buildContext.artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
...(keycloakAccountV1Version !== null ||
keycloakThemeAdditionalInfoExtensionVersion !== null
? [
` <build>`,
` <plugins>`,
` <plugin>`,
` <groupId>org.apache.maven.plugins</groupId>`,
` <artifactId>maven-shade-plugin</artifactId>`,
` <version>3.5.1</version>`,
` <executions>`,
` <execution>`,
` <phase>package</phase>`,
` <goals>`,
` <goal>shade</goal>`,
` </goals>`,
` </execution>`,
` </executions>`,
` </plugin>`,
` </plugins>`,
` </build>`,
` <dependencies>`,
...(keycloakAccountV1Version !== null
? [
` <dependency>`,
` <groupId>io.phasetwo.keycloak</groupId>`,
` <artifactId>keycloak-account-v1</artifactId>`,
` <version>${keycloakAccountV1Version}</version>`,
` </dependency>`
]
: []),
...(keycloakThemeAdditionalInfoExtensionVersion !== null
? [
` <dependency>`,
` <groupId>dev.jcputney</groupId>`,
` <artifactId>keycloak-theme-additional-info-extension</artifactId>`,
` <version>${keycloakThemeAdditionalInfoExtensionVersion}</version>`,
` </dependency>`
]
: []),
` </dependencies>`
]
: []),
`</project>`
].join("\n");
return { pomFileCode };
})();
return { pomFileCode };
}

View File

@ -0,0 +1,96 @@
import { assert, type Equals } from "tsafe/assert";
import type {
KeycloakAccountV1Version,
KeycloakThemeAdditionalInfoExtensionVersion
} from "./extensionVersions";
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
export function getKeycloakVersionRangeForJar(params: {
doesImplementAccountV1Theme: boolean;
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
}): KeycloakVersionRange | undefined {
const {
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
doesImplementAccountV1Theme
} = params;
if (doesImplementAccountV1Theme) {
const keycloakVersionRange = (() => {
switch (keycloakAccountV1Version) {
case null:
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return "21-and-below" as const;
case "1.1.5":
return undefined;
}
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
case "0.3":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "23" as const;
}
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
case "0.4":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "24" as const;
}
assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false);
case "0.6":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return "26-and-above" as const;
case "1.1.5":
return "25" as const;
}
}
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountV1Theme | undefined
>
>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (keycloakAccountV1Version !== null) {
return undefined;
}
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return "all-other-versions";
case "1.1.5":
return "22-to-25";
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(
false
);
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountV1Theme | undefined
>
>();
return keycloakVersionRange;
}
}

View File

@ -0,0 +1 @@
export * from "./buildJars";

View File

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

View File

@ -1,688 +0,0 @@
<script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= {
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
<#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#recover>
</#attempt>
"printIfExists": function (fieldName, text) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"existsError": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"get": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.get in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.exists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistMessageForUsernameOrPassword = "">
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
}
};
<#if account??>
out["url"]["getLogoutUrl"] = function () {
<#attempt>
return "${url.getLogoutUrl()}";
<#recover>
</#attempt>
};
</#if>
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["pageId"] = "${pageId}";
return out;
})()
<#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = "">
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
</#attempt>
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
</#if>
<#local keys = "">
<#attempt>
<#local keys = object?keys>
<#recover>
<#return "ABORT: We can't list keys on this object">
</#attempt>
<#local out_seq = []>
<#list keys as key>
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
<#continue>
</#if>
<#if
(
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
are_same_path(path, ["url"])
) || (
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
["secretData", "value"]?seq_contains(key) &&
are_same_path(path, [ "totp", "otpCredentials", "*" ])
) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"]) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
key == "identityProviderBrokerCtx" &&
are_same_path(path, []) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
are_same_path(path, ["realm"])
) || (
"error.ftl" == pageId &&
are_same_path(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || (
"applications.ftl" == pageId &&
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
) || (
"applications.ftl" == pageId &&
"masterAdminClient" == key
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#continue>
</#if>
<#recover>
</#attempt>
</#if>
<#attempt>
<#if !object[key]??>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
<#continue>
</#attempt>
<#local propertyValue = "">
<#attempt>
<#local propertyValue = object[key]>
<#recover>
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
<#continue>
</#attempt>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
</#list>
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
</#if>
<#local isMethod = "">
<#attempt>
<#local isMethod = object?is_method>
<#recover>
<#return "ABORT: Can't test if it'sa method.">
</#attempt>
<#if isMethod>
<#if are_same_path(path, ["auth", "showUsername"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showUsername()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showResetCredentials"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
</#attempt>
</#if>
<#return "ABORT: It's a method">
</#if>
<#local isBoolean = "">
<#attempt>
<#local isBoolean = object?is_boolean>
<#recover>
<#return "ABORT: Can't test if it's a boolean">
</#attempt>
<#if isBoolean>
<#return object?c>
</#if>
<#local isEnumerable = "">
<#attempt>
<#local isEnumerable = object?is_enumerable>
<#recover>
<#return "ABORT: Can't test if it's an enumerable">
</#attempt>
<#if isEnumerable>
<#local out_seq = []>
<#local i = 0>
<#list object as array_item>
<#if !array_item??>
<#local out_seq += ["null,"]>
<#continue>
</#if>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += [rec_out + ","]>
</#list>
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
</#if>
<#local isDate = "">
<#attempt>
<#local isDate = object?is_date_like>
<#recover>
<#return "ABORT: Can't test if it's a date">
</#attempt>
<#if isDate>
<#return '"' + object?datetime?iso_utc + '"'>
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>
</#attempt>
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#if path?size != searchedPath?size>
<#return false>
</#if>
<#local i=0>
<#list path as property>
<#local searchedProperty=searchedPath[i]>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
<#if searchedProperty?is_string && !property?is_string>
<#return false>
</#if>
<#if searchedProperty?is_number && !property?is_number>
<#return false>
</#if>
<#if searchedProperty?string != property?string>
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
</script>

View File

@ -1,36 +1,48 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as cheerio from "cheerio";
import {
replaceImportsInJsCode,
BuildContextLike as BuildContextLike_replaceImportsInJsCode
} from "../replacers/replaceImportsInJsCode";
import {
replaceImportsInCssCode,
BuildContextLike as BuildContextLike_replaceImportsInCssCode
} from "../replacers/replaceImportsInCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import {
type ThemeType,
WELL_KNOWN_DIRECTORY_BASE_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
} from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export const themeTypes = ["login", "account"] as const;
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
BuildContextLike_replaceImportsInCssCode & {
urlPathname: string | undefined;
themeVersion: string;
kcContextExclusionsFtlCode: string | undefined;
};
export type ThemeType = (typeof themeTypes)[number];
export type BuildOptionsLike = {
themeName: string;
themeVersion: string;
urlPathname: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
buildContext: BuildContextLike;
keycloakifyVersion: string;
themeType: ThemeType;
fieldNames: string[];
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const {
themeName,
indexHtmlCode,
buildContext,
keycloakifyVersion,
themeType,
fieldNames
} = params;
const $ = cheerio.load(indexHtmlCode);
@ -40,7 +52,10 @@ export function generateFtlFilesCodeFactory(params: {
assert(jsCode !== null);
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ jsCode });
const { fixedJsCode } = replaceImportsInJsCode({
jsCode,
buildContext
});
$(element).text(fixedJsCode);
});
@ -50,9 +65,10 @@ export function generateFtlFilesCodeFactory(params: {
assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({
const { fixedCssCode } = replaceImportsInCssCode({
cssCode,
buildOptions
cssFileRelativeDirPath: undefined,
buildContext
});
$(element).text(fixedCssCode);
@ -61,7 +77,8 @@ export function generateFtlFilesCodeFactory(params: {
(
[
["link", "href"],
["script", "src"]
["script", "src"],
["script", "data-src"]
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
@ -73,55 +90,47 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
href.replace(
new RegExp(
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
),
`\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/`
)
);
})
);
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildOptions
}).cssCodeToPrependInHead,
"</style>",
""
].join("\n")
);
}
}
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)
.replace("KEYCLOAKIFY_THEME_NAME_cXxKd3xEer", buildOptions.themeName),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>',
" </#list>",
"</#if>"
].join("\n")
};
const kcContextDeclarationTemplateFtl = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"keycloakify",
"generateFtl",
"kcContextDeclarationTemplate.ftl"
)
)
.toString("utf8")
.replace("{{themeType}}", themeType)
.replace("{{themeName}}", themeName)
.replace("{{keycloakifyVersion}}", keycloakifyVersion)
.replace("{{themeVersion}}", buildContext.themeVersion)
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
.replace("{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}}", KEYCLOAKIFY_SPA_DEV_SERVER_PORT)
.replace(
"{{userDefinedExclusions}}",
buildContext.kcContextExclusionsFtlCode ?? ""
);
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]
].join("\n")
`<script>\n${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}\n</script>`
);
// Remove part of the document marked as ignored.
@ -130,7 +139,9 @@ export function generateFtlFilesCodeFactory(params: {
startTags.each((...[, startTag]) => {
const $startTag = $(startTag);
const $endTag = $startTag.nextAll('meta[name="keycloakify-ignore-end"]').first();
const $endTag = $startTag
.nextAll('meta[name="keycloakify-ignore-end"]')
.first();
if ($endTag.length) {
let currentNode = $startTag.next();
@ -157,9 +168,14 @@ export function generateFtlFilesCodeFactory(params: {
let ftlCode = $.html();
Object.entries({
...replaceValueBySearchValue,
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
kcContextDeclarationTemplateFtl,
"{{pageId}}": pageId,
"{{ftlTemplateFileName}}": pageId
}).map(
([searchValue, replaceValue]) =>
(ftlCode = ftlCode.replace(searchValue, replaceValue))
);
return { ftlCode };
}

View File

@ -1,2 +1 @@
export * from "./generateFtl";
export * from "./pageId";

View File

@ -0,0 +1,724 @@
<#assign xKeycloakify={
"messages": {},
"pageId": "{{pageId}}",
"ftlTemplateFileName": "{{ftlTemplateFileName}}",
"themeType": "{{themeType}}",
"themeName": "{{themeName}}",
"keycloakifyVersion": "{{keycloakifyVersion}}",
"themeVersion": "{{themeVersion}}",
"resourcesPath": ""
}>
<#if url?? && url?is_hash && url.resourcesPath?? && url.resourcesPath?is_string>
<#assign xKeycloakify = xKeycloakify + { "resourcesPath": url.resourcesPath }>
</#if>
<#if resourceUrl?? && resourceUrl?is_string>
<#assign xKeycloakify = xKeycloakify + { "resourcesPath": resourceUrl }>
</#if>
const kcContext = ${toJsDeclarationString(.data_model, [])?no_esc};
kcContext.keycloakifyVersion = "${xKeycloakify.keycloakifyVersion}";
kcContext.themeVersion = "${xKeycloakify.themeVersion}";
kcContext.themeType = "${xKeycloakify.themeType}";
kcContext.themeName = "${xKeycloakify.themeName}";
kcContext.pageId = "${xKeycloakify.pageId}";
kcContext.ftlTemplateFileName = "${xKeycloakify.ftlTemplateFileName}";
<@addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages />
kcContext["x-keycloakify"] = {};
kcContext["x-keycloakify"].resourcesPath = "${xKeycloakify.resourcesPath}";
{
var messages = {};
<#list xKeycloakify.messages as key, resolvedMsg>
messages["${key}"] = decodeHtmlEntities("${resolvedMsg?js_string}");
</#list>
kcContext["x-keycloakify"].messages = messages;
}
if(
kcContext.url instanceof Object &&
typeof kcContext.url.resourcesPath === "string"
){
kcContext.url.resourcesCommonPath = kcContext.url.resourcesPath + "/{{RESOURCES_COMMON}}";
}
if( kcContext.messagesPerField ){
var existsError_singleFieldName = kcContext.messagesPerField.existsError;
kcContext.messagesPerField.existsError = function (){
for( let i = 0; i < arguments.length; i++ ){
if( existsError_singleFieldName(arguments[i]) ){
return true;
}
}
return false;
};
kcContext.messagesPerField.exists = function (fieldName) {
return kcContext.messagesPerField.get(fieldName) !== "";
};
kcContext.messagesPerField.printIfExists = function (fieldName, text) {
return kcContext.messagesPerField.exists(fieldName) ? text : undefined;
};
kcContext.messagesPerField.getFirstError = function () {
for( let i = 0; i < arguments.length; i++ ){
const fieldName = arguments[i];
if( kcContext.messagesPerField.existsError(fieldName) ){
return kcContext.messagesPerField.get(fieldName);
}
}
};
}
attributes_to_attributesByName: {
if( !kcContext.profile ){
break attributes_to_attributesByName;
}
if( !kcContext.profile.attributes ){
break attributes_to_attributesByName;
}
var attributes = kcContext.profile.attributes;
delete kcContext.profile.attributes;
kcContext.profile.attributesByName = {};
attributes.forEach(function(attribute){
kcContext.profile.attributesByName[attribute.name] = attribute;
});
}
redirect_to_dev_server: {
switch(kcContext.themeType){
case "login":
break redirect_to_dev_server;
case "account":
if( kcContext.pageId !== "index.ftl" ){
break redirect_to_dev_server;
}
break;
case "admin":
break;
default:
break redirect_to_dev_server;
}
const devSeverPort = kcContext.properties.{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}};
if( !devSeverPort ){
break redirect_to_dev_server;
}
const redirectUrl = new URL(window.location.href);
redirectUrl.port = devSeverPort;
delete kcContext.msgJSON;
console.log(kcContext);
redirectUrl.searchParams.set("kcContext", encodeURIComponent(JSON.stringify(kcContext)));
window.location.href = redirectUrl.toString();
}
window.kcContext = kcContext;
<#if xKeycloakify.themeType == "login" >
{
const script = document.createElement("script");
script.type = "importmap";
script.textContent = JSON.stringify({
imports: {
"rfc4648": kcContext.url.resourcesCommonPath + "/node_modules/rfc4648/lib/rfc4648.js"
}
}, null, 2);
document.head.appendChild(script);
}
</#if>
function decodeHtmlEntities(htmlStr){
var element = decodeHtmlEntities.element;
if (!element) {
element = document.createElement("textarea");
decodeHtmlEntities.element = element;
}
element.innerHTML = htmlStr;
return element.value;
}
<#function toJsDeclarationString object path>
<#local isHash = -1>
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
<#return "ABORT: Can't evaluate if " + path?join(".") + " is a hash">
</#attempt>
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
</#if>
<#local keys = -1>
<#attempt>
<#local keys = object?keys>
<#recover>
<#return "ABORT: We can't list keys on object">
</#attempt>
<#local outSeq = []>
<#list keys as key>
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
<#continue>
</#if>
<#if (
areSamePath(path, ["url"]) &&
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key)
) || (
key == "updateProfileCtx" &&
areSamePath(path, [])
) || (
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" &&
areSamePath(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
["secretData", "value"]?seq_contains(key) &&
areSamePath(path, [ "totp", "otpCredentials", "*" ])
) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
areSamePath(path, ["brokerContext"]) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
) || (
key == "identityProviderBrokerCtx" &&
areSamePath(path, []) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
) || (
["masterAdminClient", "delegateForUpdate", "defaultRole", "smtpConfig"]?seq_contains(key) &&
areSamePath(path, ["realm"])
) || (
xKeycloakify.pageId == "error.ftl" &&
areSamePath(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || (
xKeycloakify.pageId == "applications.ftl" &&
(
key == "realm" ||
key == "container"
) &&
isSubpath(path, ["applications", "applications"])
) || (
key == "delegateForUpdate" &&
areSamePath(path, ["user"])
) || (
<#-- Security audit forwarded by Garth (Gmail) -->
key == "saml.signing.private.key" &&
areSamePath(path, ["client", "attributes"])
) || (
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
key == "password" &&
areSamePath(path, ["login"])
) || (
<#-- Remove realmAttributes added by https://github.com/jcputney/keycloak-theme-additional-info-extension for peace of mind. -->
key == "realmAttributes" &&
areSamePath(path, [])
) || (
<#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
key == "attributesByName" &&
areSamePath(path, ["profile"])
) || (
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
(key == "attributes" || key == "attributesByName") &&
areSamePath(path, ["register"])
) || (
areSamePath(path, ["properties"]) &&
(
key?starts_with("kc") ||
key == "locales" ||
key == "import" ||
key == "parent" ||
key == "meta" ||
key == "stylesCommon" ||
key == "styles" ||
key == "accountResourceProvider"
)
) || (
key == "execution" &&
areSamePath(path, [])
) || (
key == "entity" &&
areSamePath(path, ["user"])
) || (
key == "attributes" &&
areSamePath(path, ["realm"])
) || (
xKeycloakify.pageId == "index.ftl" &&
xKeycloakify.themeType == "account" &&
areSamePath(path, ["realm"]) &&
![
"name",
"registrationEmailAsUsername",
"editUsernameAllowed",
"isInternationalizationEnabled",
"identityFederationEnabled",
"userManagedAccessAllowed"
]?seq_contains(key)
) || (
["flowContext", "session", "realm"]?seq_contains(key) &&
areSamePath(path, ["social"])
)
>
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
<#continue>
</#if>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if (
key == "attemptedUsername" &&
areSamePath(path, ["auth"]) &&
[
"register.ftl", "terms.ftl", "info.ftl", "login.ftl",
"login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"
]?seq_contains(xKeycloakify.pageId)
)>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
<#continue>
</#if>
<#recover>
<#local outSeq += ["/*Accessing attemptedUsername throwed an exception */"]>
</#attempt>
</#if>
{{userDefinedExclusions}}
<#attempt>
<#if !object[key]??>
<#continue>
</#if>
<#recover>
<#local outSeq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
<#continue>
</#attempt>
<#local propertyValue = -1>
<#attempt>
<#local propertyValue = object[key]>
<#recover>
<#local outSeq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
<#continue>
</#attempt>
<#local recOut = toJsDeclarationString(propertyValue, path + [ key ])>
<#if recOut?starts_with("ABORT:")>
<#local errorMessage = recOut?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local outSeq += ["/*" + key + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local outSeq += ['"' + key + '": ' + recOut + ","]>
</#list>
<#return (["{"] + outSeq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
</#if>
<#local isMethod = -1>
<#attempt>
<#local isMethod = object?is_method>
<#recover>
<#return "ABORT: Can't test if it'sa method.">
</#attempt>
<#if isMethod>
<#if areSamePath(path, ["auth", "showUsername"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showUsername()">
</#attempt>
</#if>
<#if areSamePath(path, ["auth", "showResetCredentials"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
</#attempt>
</#if>
<#if areSamePath(path, ["auth", "showTryAnotherWayLink"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
</#attempt>
</#if>
<#if areSamePath(path, ["url", "getLogoutUrl"])>
<#local returnValue = -1>
<#attempt>
<#local returnValue = url.getLogoutUrl()>
<#recover>
<#return "ABORT: Couldn't evaluate url.getLogoutUrl()">
</#attempt>
<#return 'function(){ return "' + returnValue + '"; }'>
</#if>
<#if areSamePath(path, ["totp", "policy", "getAlgorithmKey"])>
<#local returnValue = "error">
<#if mode?? && mode = "manual">
<#attempt>
<#local returnValue = totp.policy.getAlgorithmKey()>
<#recover>
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
</#attempt>
</#if>
<#return 'function(){ return "' + returnValue + '"; }'>
</#if>
<#assign fieldNames = [{{fieldNames}}]>
<#if profile?? && profile.attributes??>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#if areSamePath(path, ["messagesPerField", "get"])>
<#local jsFunctionCode = "function (fieldName) { ">
<#list fieldNames as fieldName>
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
<#if xKeycloakify.pageId == "login.ftl" >
<#if fieldName == "username">
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
<#if messagesPerField.exists('username') || messagesPerField.exists('password')>
<#local jsFunctionCode += "return kcContext.message && kcContext.message.summary ? kcContext.message.summary : 'error'; ">
<#else>
<#local jsFunctionCode += "return ''; ">
</#if>
<#local jsFunctionCode += "} ">
<#continue>
</#if>
<#if fieldName == "password">
<#continue>
</#if>
</#if>
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "'){ ">
<#if messagesPerField.exists('${fieldName}')>
<#local jsFunctionCode += 'return decodeHtmlEntities("' + messagesPerField.get('${fieldName}')?js_string + '"); '>
<#else>
<#local jsFunctionCode += "return ''; ">
</#if>
<#local jsFunctionCode += "} ">
</#list>
<#local jsFunctionCode += "}">
<#return jsFunctionCode>
</#if>
<#if areSamePath(path, ["messagesPerField", "existsError"])>
<#local jsFunctionCode = "function (fieldName) { ">
<#list fieldNames as fieldName>
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
<#if xKeycloakify.pageId == "login.ftl" >
<#if fieldName == "username">
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
<#if messagesPerField.existsError('username') || messagesPerField.existsError('password')>
<#local jsFunctionCode += "return true; ">
<#else>
<#local jsFunctionCode += "return false; ">
</#if>
<#local jsFunctionCode += "} ">
<#continue>
</#if>
<#if fieldName == "password">
<#continue>
</#if>
</#if>
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "' ){ ">
<#if messagesPerField.existsError('${fieldName}')>
<#local jsFunctionCode += 'return true; '>
<#else>
<#local jsFunctionCode += "return false; ">
</#if>
<#local jsFunctionCode += "}">
</#list>
<#local jsFunctionCode += "}">
<#return jsFunctionCode>
</#if>
<#if xKeycloakify.themeType == "account" && areSamePath(path, ["realm", "isInternationalizationEnabled"])>
<#attempt>
<#return realm.isInternationalizationEnabled()?c>
<#recover>
<#return "ABORT: Couldn't evaluate realm.isInternationalizationEnabled()">
</#attempt>
</#if>
<#return "ABORT: It's a method">
</#if>
<#local isBoolean = -1>
<#attempt>
<#local isBoolean = object?is_boolean>
<#recover>
<#return "ABORT: Can't test if it's a boolean">
</#attempt>
<#if isBoolean>
<#return object?c>
</#if>
<#local isEnumerable = -1>
<#attempt>
<#local isEnumerable = object?is_enumerable>
<#recover>
<#return "ABORT: Can't test if it's an enumerable">
</#attempt>
<#if isEnumerable>
<#local outSeq = []>
<#local i = 0>
<#list object as array_item>
<#if !array_item??>
<#local outSeq += ["null,"]>
<#continue>
</#if>
<#local recOut = toJsDeclarationString(array_item, path + [ i ])>
<#local i = i + 1>
<#if recOut?starts_with("ABORT:")>
<#local errorMessage = recOut?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local outSeq += ["/*" + i?string + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local outSeq += [recOut + ","]>
</#list>
<#return (["["] + outSeq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
</#if>
<#local isDate = -1>
<#attempt>
<#local isDate = object?is_date_like>
<#recover>
<#return "ABORT: Can't test if it's a date">
</#attempt>
<#if isDate>
<#return '"' + object?datetime?iso_utc + '"'>
</#if>
<#local isNumber = -1>
<#attempt>
<#local isNumber = object?is_number>
<#recover>
<#return "ABORT: Can't test if it's a number">
</#attempt>
<#if isNumber>
<#return object?c>
</#if>
<#local isString = -1>
<#attempt>
<#local isString = object?is_string>
<#recover>
<#return "ABORT: Can't test if it's a string">
</#attempt>
<#if isString>
<@addToXKeycloakifyMessagesIfMessageKey str=object />
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>
</#attempt>
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non number, non enumerable object">
</#function>
<#function isSubpath path searchedPath>
<#if path?size < searchedPath?size>
<#return false>
</#if>
<#local i=0>
<#list path as property>
<#if i == searchedPath?size >
<#continue>
</#if>
<#local searchedProperty=searchedPath[i]>
<#local i+= 1>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
<#if searchedProperty?is_string && !property?is_string>
<#return false>
</#if>
<#if searchedProperty?is_number && !property?is_number>
<#return false>
</#if>
<#if searchedProperty?string != property?string>
<#return false>
</#if>
</#list>
<#return true>
</#function>
<#function areSamePath path searchedPath>
<#return path?size == searchedPath?size && isSubpath(path, searchedPath)>
</#function>
<#macro addToXKeycloakifyMessagesIfMessageKey str>
<#if !msg?? || !msg?is_method>
<#return>
</#if>
<#if (str?length > 200)>
<#return>
</#if>
<#local key=removeBrackets(str)>
<#if key?length==0>
<#return>
</#if>
<#if !(key?matches(r"^[a-zA-Z0-9-_.]*$"))>
<#return>
</#if>
<#local resolvedMsg=msg(key)>
<#if resolvedMsg==key>
<#return>
</#if>
<#local messages=xKeycloakify.messages>
<#local messages = messages + { key: resolvedMsg }>
<#assign xKeycloakify = xKeycloakify + { "messages": messages }>
</#macro>
<#function removeBrackets str>
<#if str?starts_with("${") && str?ends_with("}")>
<#return str[2..(str?length-2)]>
<#else>
<#return str>
</#if>
</#function>
<#macro addNonAutomaticallyGatherableMessagesToXKeycloakifyMessages>
<#if profile?? && profile?is_hash && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if !(
attribute.annotations?? && attribute.annotations?is_hash &&
attribute.annotations.inputOptionLabelsI18nPrefix?? && attribute.annotations.inputOptionLabelsI18nPrefix?is_string
)>
<#continue>
</#if>
<#local prefix=attribute.annotations.inputOptionLabelsI18nPrefix>
<#if !(
attribute.validators?? && attribute.validators?is_hash &&
attribute.validators.options?? && attribute.validators.options?is_hash &&
attribute.validators.options.options?? && attribute.validators.options.options?is_enumerable
)>
<#continue>
</#if>
<#list attribute.validators.options.options as option>
<#if !option?is_string>
<#continue>
</#if>
<@addToXKeycloakifyMessagesIfMessageKey str="${prefix}.${option}" />
</#list>
</#list>
</#if>
<#if xKeycloakify.pageId == "terms.ftl" || termsAcceptanceRequired?? && termsAcceptanceRequired>
<@addToXKeycloakifyMessagesIfMessageKey str="termsText" />
</#if>
<#if requiredActions?? && requiredActions?is_enumerable>
<#list requiredActions as requiredAction>
<#if !requiredAction?is_string>
<#continue>
</#if>
<@addToXKeycloakifyMessagesIfMessageKey str="requiredAction.${requiredAction}" />
</#list>
</#if>
</#macro>

View File

@ -1,33 +0,0 @@
export const loginThemePageIds = [
"login.ftl",
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-oauth2-device-verify-user-code.ftl",
"login-oauth-grant.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl",
"update-email.ftl",
"select-authenticator.ftl",
"saml-post-form.ftl"
] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];

View File

@ -1,84 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = {
themeName: string;
extraThemeNames: string[];
groupId: string;
artifactId: string;
themeVersion: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
implementedThemeTypes
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
}))
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -0,0 +1,442 @@
import { type ThemeType, FALLBACK_LANGUAGE_TAG } from "../../shared/constants";
import { crawl } from "../../tools/crawl";
import { join as pathJoin, dirname as pathDirname } from "path";
import { symToStr } from "tsafe/symToStr";
import * as recast from "recast";
import * as babelParser from "@babel/parser";
import babelGenerate from "@babel/generator";
import * as babelTypes from "@babel/types";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
export type BuildContextLike = {
themeNames: string[];
themeSrcDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateMessageProperties(params: {
buildContext: BuildContextLike;
themeType: Exclude<ThemeType, "admin">;
}): {
languageTags: string[];
writeMessagePropertiesFiles: (params: {
messageDirPath: string;
themeName: string;
}) => void;
} {
const { buildContext, themeType } = params;
const baseMessagesDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
themeType,
"i18n",
"messages_defaultSet"
);
const messages_defaultSet_by_languageTag_defaultSet: {
[languageTag_defaultSet: string]: Record<string, string>;
} = Object.fromEntries(
fs
.readdirSync(baseMessagesDirPath)
.filter(basename => basename !== "index.ts" && basename !== "types.ts")
.map(basename => ({
languageTag: basename.replace(/\.ts$/, ""),
filePath: pathJoin(baseMessagesDirPath, basename)
}))
.map(({ languageTag, filePath }) => {
const lines = fs.readFileSync(filePath).toString("utf8").split(/\r?\n/);
let messagesJson = "{";
let isInDeclaration = false;
for (const line of lines) {
if (!isInDeclaration) {
if (line.startsWith("const messages")) {
isInDeclaration = true;
}
continue;
}
if (line.startsWith("}")) {
messagesJson += "}";
break;
}
messagesJson += line;
}
const messages = JSON.parse(messagesJson) as Record<string, string>;
return [languageTag, messages];
})
);
const { i18nTsFilePath } = (() => {
let files = crawl({
dirPath: pathJoin(buildContext.themeSrcDirPath, themeType),
returnedPathsType: "absolute"
});
files = files.filter(file => {
const regex = /\.(js|ts|tsx)$/;
return regex.test(file);
});
files = files.sort((a, b) => {
const regex = /\.i18n\.(ts|js|tsx)$/;
const aIsI18nFile = regex.test(a);
const bIsI18nFile = regex.test(b);
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
});
files = files.sort((a, b) => a.length - b.length);
files = files.filter(file =>
fs.readFileSync(file).toString("utf8").includes("i18nBuilder")
);
const i18nTsFilePath: string | undefined = files[0];
return { i18nTsFilePath };
})();
const i18nTsRoot = (() => {
if (i18nTsFilePath === undefined) {
return undefined;
}
const root = recastParseTs(i18nTsFilePath);
return root;
})();
const messages_defaultSet_by_languageTag_notInDefaultSet:
| { [languageTag_notInDefaultSet: string]: Record<string, string> }
| undefined = (() => {
if (i18nTsRoot === undefined) {
return undefined;
}
let extraLanguageEntryByLanguageTag: Record<
string,
{ label: string; path: string }
> = {};
recast.visit(i18nTsRoot, {
visitCallExpression: function (path) {
const node = path.node;
// Check if the callee is a MemberExpression with property 'withExtraLanguages'
if (
node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "withExtraLanguages"
) {
const arg = node.arguments[0];
if (arg && arg.type === "ObjectExpression") {
// Iterate over the properties of the object
arg.properties.forEach(prop => {
if (
prop.type === "ObjectProperty" &&
prop.key.type === "Identifier"
) {
const lang = prop.key.name;
const value = prop.value;
if (value.type === "ObjectExpression") {
let label: string | undefined = undefined;
let pathStr: string | undefined = undefined;
// Iterate over the properties of the language object
value.properties.forEach(p => {
if (
p.type === "ObjectProperty" &&
p.key.type === "Identifier"
) {
if (
p.key.name === "label" &&
p.value.type === "StringLiteral"
) {
label = p.value.value;
}
if (
p.key.name === "getMessages" &&
(p.value.type ===
"ArrowFunctionExpression" ||
p.value.type === "FunctionExpression")
) {
// Extract the import path from the function body
const body = p.value.body;
if (
body.type === "CallExpression" &&
body.callee.type === "Import"
) {
const importArg = body.arguments[0];
if (
importArg.type === "StringLiteral"
) {
pathStr = importArg.value;
}
} else if (
body.type === "BlockStatement"
) {
// If the function body is a block (e.g., function with braces {})
// Look for return statement
body.body.forEach(statement => {
if (
statement.type ===
"ReturnStatement" &&
statement.argument &&
statement.argument.type ===
"CallExpression" &&
statement.argument.callee
.type === "Import"
) {
const importArg =
statement.argument
.arguments[0];
if (
importArg.type ===
"StringLiteral"
) {
pathStr = importArg.value;
}
}
});
}
}
}
});
if (label && pathStr) {
extraLanguageEntryByLanguageTag[lang] = {
label,
path: pathStr
};
}
}
}
});
}
return false; // Stop traversing this path
}
this.traverse(path); // Continue traversing other paths
}
});
const messages_defaultSet_by_languageTag_notInDefaultSet = Object.fromEntries(
Object.entries(extraLanguageEntryByLanguageTag).map(
([languageTag, { path: relativePathWithoutExt }]) => [
languageTag,
(() => {
const filePath = getAbsoluteAndInOsFormatPath({
pathIsh: relativePathWithoutExt.endsWith(".ts")
? relativePathWithoutExt
: `${relativePathWithoutExt}.ts`,
cwd: pathDirname(i18nTsFilePath)
});
const root = recastParseTs(filePath);
let declarationCode: string | undefined = "";
recast.visit(root, {
visitVariableDeclarator: function (path) {
const node = path.node;
// Check if the variable name is 'messages'
if (
node.id.type === "Identifier" &&
node.id.name === "messages"
) {
// Ensure there is an initializer
if (node.init) {
// Generate code from the initializer, preserving comments
declarationCode = recast
.print(node.init)
.code.replace(/}.*$/, "}");
}
return false; // Stop traversing this path
}
this.traverse(path); // Continue traversing other paths
}
});
assert(
declarationCode !== undefined,
`${filePath} does not contain a 'messages' variable declaration`
);
let messages: Record<string, string> = {};
try {
eval(`${symToStr({ messages })} = ${declarationCode};`);
} catch {
throw new Error(
`The declaration of 'message' in ${filePath} cannot be statically evaluated: ${declarationCode}`
);
}
return messages;
})()
]
)
);
return messages_defaultSet_by_languageTag_notInDefaultSet;
})();
const messages_defaultSet_by_languageTag = {
...messages_defaultSet_by_languageTag_defaultSet,
...messages_defaultSet_by_languageTag_notInDefaultSet
};
const messages_themeDefined_by_languageTag:
| {
[languageTag: string]:
| Record<string, string | Record<string, string>>
| undefined;
}
| undefined = (() => {
if (i18nTsRoot === undefined) {
return undefined;
}
let firstArgumentCode: string | undefined = undefined;
recast.visit(i18nTsRoot, {
visitCallExpression: function (path) {
const node = path.node;
if (
node.callee.type === "MemberExpression" &&
node.callee.property.type === "Identifier" &&
node.callee.property.name === "withCustomTranslations"
) {
firstArgumentCode = babelGenerate(node.arguments[0] as any).code;
return false;
}
this.traverse(path);
}
});
if (firstArgumentCode === undefined) {
return undefined;
}
let messages_themeDefined_by_languageTag: {
[languageTag: string]: Record<string, string | Record<string, string>>;
} = {};
try {
eval(
`${symToStr({ messages_themeDefined_by_languageTag })} = ${firstArgumentCode}`
);
} catch {
console.warn(
[
"WARNING: The argument of withCustomTranslations can't be statically evaluated!",
"This needs to be fixed refer to the documentation: https://docs.keycloakify.dev/i18n",
firstArgumentCode
].join(" ")
);
return undefined;
}
return messages_themeDefined_by_languageTag;
})();
const languageTags = Object.keys(messages_defaultSet_by_languageTag);
return {
languageTags,
writeMessagePropertiesFiles: ({ messageDirPath, themeName }) => {
for (const languageTag of languageTags) {
const messages = {
...messages_defaultSet_by_languageTag[languageTag]
};
add_theme_defined_messages: {
if (messages_themeDefined_by_languageTag === undefined) {
break add_theme_defined_messages;
}
let messages_themeDefined =
messages_themeDefined_by_languageTag[languageTag];
if (messages_themeDefined === undefined) {
messages_themeDefined =
messages_themeDefined_by_languageTag[FALLBACK_LANGUAGE_TAG];
}
if (messages_themeDefined === undefined) {
messages_themeDefined =
messages_themeDefined_by_languageTag[
Object.keys(messages_themeDefined_by_languageTag)[0]
];
}
if (messages_themeDefined === undefined) {
break add_theme_defined_messages;
}
for (const [key, messageOrMessageByThemeName] of Object.entries(
messages_themeDefined
)) {
const message = (() => {
if (typeof messageOrMessageByThemeName === "string") {
return messageOrMessageByThemeName;
}
const message = messageOrMessageByThemeName[themeName];
assert(message !== undefined);
return message;
})();
messages[key] = message;
}
}
const propertiesFileSource = [
"",
...Object.entries(messages).map(
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
),
""
].join("\n");
fs.mkdirSync(messageDirPath, { recursive: true });
fs.writeFileSync(
pathJoin(messageDirPath, `messages_${languageTag}.properties`),
Buffer.from(propertiesFileSource, "utf8")
);
}
}
};
}
function recastParseTs(filePath: string): recast.types.ASTNode {
return recast.parse(fs.readFileSync(filePath).toString("utf8"), {
parser: {
parse: (code: string) =>
babelParser.parse(code, {
sourceType: "module",
plugins: ["typescript"]
}),
generator: babelGenerate,
types: babelTypes
}
});
}

View File

@ -0,0 +1,756 @@
import type { BuildContext } from "../../shared/buildContext";
import fs from "fs";
import { rmSync } from "../../tools/fs.rmSync";
import { transformCodebase } from "../../tools/transformCodebase";
import {
join as pathJoin,
relative as pathRelative,
dirname as pathDirname,
basename as pathBasename
} from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import {
generateFtlFilesCodeFactory,
type BuildContextLike as BuildContextLike_kcContextExclusionsFtlCode
} from "../generateFtl";
import {
type ThemeType,
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS,
WELL_KNOWN_DIRECTORY_BASE_NAME,
THEME_TYPES,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
} from "../../shared/constants";
import { assert, type Equals } from "tsafe/assert";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import {
generateMessageProperties,
type BuildContextLike as BuildContextLike_generateMessageProperties
} from "./generateMessageProperties";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import propertiesParser from "properties-parser";
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
import { listInstalledModules } from "../../tools/listInstalledModules";
import { isInside } from "../../tools/isInside";
import { id } from "tsafe/id";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & {
themeNames: string[];
extraThemeProperties: string[] | undefined;
projectDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateResources(params: {
buildContext: BuildContextLike;
resourcesDirPath: string;
}): Promise<void> {
const start = Date.now();
const { resourcesDirPath, buildContext } = params;
const [themeName] = buildContext.themeNames;
if (fs.existsSync(resourcesDirPath)) {
rmSync(resourcesDirPath, { recursive: true });
}
const getThemeTypeDirPath = (params: {
themeType: ThemeType | "email";
themeName: string;
}) => {
const { themeType, themeName } = params;
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
};
const writeMessagePropertiesFilesByThemeType: Partial<
Record<
ThemeType | "email",
(params: { messageDirPath: string; themeName: string }) => void
>
> = {};
for (const themeType of [...THEME_TYPES, "email"] as const) {
let isNative: boolean;
{
const v = buildContext.implementedThemeTypes[themeType];
if (!v.isImplemented && !v.isImplemented_native) {
continue;
}
isNative = !v.isImplemented && v.isImplemented_native;
}
const getAccountThemeType = () => {
assert(themeType === "account");
assert(buildContext.implementedThemeTypes.account.isImplemented);
return buildContext.implementedThemeTypes.account.type;
};
const isSpa = (() => {
switch (themeType) {
case "login":
return false;
case "account":
return getAccountThemeType() === "Single-Page";
case "admin":
return true;
case "email":
return false;
}
})();
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
apply_replacers_and_move_to_theme_resources: {
if (isNative) {
break apply_replacers_and_move_to_theme_resources;
}
const destDirPath = pathJoin(
themeTypeDirPath,
"resources",
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
);
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { recursive: true, force: true });
if (
themeType !== "login" &&
buildContext.implementedThemeTypes.login.isImplemented
) {
// NOTE: We prevent doing it twice, it has been done for the login theme.
transformCodebase({
srcDirPath: pathJoin(
getThemeTypeDirPath({
themeName,
themeType: "login"
}),
"resources",
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
),
destDirPath
});
break apply_replacers_and_move_to_theme_resources;
}
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack");
throw new Error(
[
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES} directory shouldn't exist in your build directory.`,
`(${pathRelative(process.cwd(), dirPath)}).\n`,
`Theses assets are only required for local development with Storybook.",
"Please remove this directory as an additional step of your command.\n`,
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
].join(" ")
);
}
}
transformCodebase({
srcDirPath: buildContext.projectBuildDirPath,
destDirPath,
transformSourceCode: ({ filePath, fileRelativePath, sourceCode }) => {
if (filePath.endsWith(".css")) {
const { fixedCssCode } = replaceImportsInCssCode({
cssCode: sourceCode.toString("utf8"),
cssFileRelativeDirPath: pathDirname(fileRelativePath),
buildContext
});
return {
modifiedSourceCode: Buffer.from(fixedCssCode, "utf8")
};
}
if (filePath.endsWith(".js")) {
const { fixedJsCode } = replaceImportsInJsCode({
jsCode: sourceCode.toString("utf8"),
buildContext
});
return {
modifiedSourceCode: Buffer.from(fixedJsCode, "utf8")
};
}
return { modifiedSourceCode: sourceCode };
}
});
}
generate_ftl_files: {
if (isNative) {
break generate_ftl_files;
}
assert(themeType !== "email");
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
themeName,
indexHtmlCode: fs
.readFileSync(
pathJoin(buildContext.projectBuildDirPath, "index.html")
)
.toString("utf8"),
buildContext,
keycloakifyVersion: readThisNpmPackageVersion(),
themeType,
fieldNames: isSpa
? []
: (assert(themeType !== "admin"),
readFieldNameUsage({
themeSrcDirPath: buildContext.themeSrcDirPath,
themeType
}))
});
[
...(() => {
switch (themeType) {
case "login":
return LOGIN_THEME_PAGE_IDS;
case "account":
return getAccountThemeType() === "Single-Page"
? ["index.ftl"]
: ACCOUNT_THEME_PAGE_IDS;
case "admin":
return ["index.ftl"];
}
})(),
...(isSpa
? []
: readExtraPagesNames({
themeType,
themeSrcDirPath: buildContext.themeSrcDirPath
}))
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
);
});
}
copy_native_theme: {
if (!isNative) {
break copy_native_theme;
}
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
transformCodebase({
srcDirPath: dirPath,
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
return undefined;
}
return { modifiedSourceCode: sourceCode };
}
});
}
let languageTags: string[] | undefined = undefined;
i18n_multi_page: {
if (isNative) {
break i18n_multi_page;
}
if (isSpa) {
break i18n_multi_page;
}
assert(themeType !== "admin" && themeType !== "email");
const wrap = generateMessageProperties({
buildContext,
themeType
});
languageTags = wrap.languageTags;
const { writeMessagePropertiesFiles } = wrap;
writeMessagePropertiesFilesByThemeType[themeType] =
writeMessagePropertiesFiles;
}
let isLegacyAccountSpa = false;
// NOTE: Eventually remove this block.
i18n_single_page_account_legacy: {
if (!isSpa) {
break i18n_single_page_account_legacy;
}
if (themeType !== "account") {
break i18n_single_page_account_legacy;
}
const [moduleMeta] = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName === "@keycloakify/keycloak-account-ui"
});
assert(
moduleMeta !== undefined,
`@keycloakify/keycloak-account-ui is supposed to be installed`
);
{
const [majorStr] = moduleMeta.version.split(".");
if (majorStr.length === 6) {
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
// 26.0.7 it would be 260007.
break i18n_single_page_account_legacy;
} else {
// 25.0.4-rc.5 or later
isLegacyAccountSpa = true;
}
}
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
if (!fs.existsSync(messageDirPath_defaults)) {
throw new Error(
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
);
}
isLegacyAccountSpa = true;
const messagesDirPath_dest = pathJoin(
getThemeTypeDirPath({ themeName, themeType: "account" }),
"messages"
);
transformCodebase({
srcDirPath: messageDirPath_defaults,
destDirPath: messagesDirPath_dest
});
apply_theme_changes: {
const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath,
"account",
"messages"
);
if (!fs.existsSync(messagesDirPath_theme)) {
break apply_theme_changes;
}
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
const filePath_src = pathJoin(messagesDirPath_theme, basename);
const filePath_dest = pathJoin(messagesDirPath_dest, basename);
if (!fs.existsSync(filePath_dest)) {
fs.cpSync(filePath_src, filePath_dest);
}
const messages_src = propertiesParser.parse(
fs.readFileSync(filePath_src).toString("utf8")
);
const messages_dest = propertiesParser.parse(
fs.readFileSync(filePath_dest).toString("utf8")
);
const messages = {
...messages_dest,
...messages_src
};
const editor = propertiesParser.createEditor();
Object.entries(messages).forEach(([key, value]) => {
editor.set(key, value);
});
fs.writeFileSync(
filePath_dest,
Buffer.from(editor.toString(), "utf8")
);
});
}
languageTags = fs
.readdirSync(messagesDirPath_dest)
.map(basename =>
basename.replace(/^messages_/, "").replace(/\.properties$/, "")
);
}
i18n_for_spas_and_native: {
if (!isSpa && !isNative) {
break i18n_for_spas_and_native;
}
if (isLegacyAccountSpa) {
break i18n_for_spas_and_native;
}
const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath,
themeType,
isNative ? "messages" : "i18n"
);
if (!fs.existsSync(messagesDirPath_theme)) {
break i18n_for_spas_and_native;
}
const propertiesByLang: Record<
string,
{
base: Buffer;
override: Buffer | undefined;
overrideByThemeName: Record<string, Buffer>;
}
> = {};
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
type ParsedBasename = { lang: string } & (
| {
isOverride: false;
}
| {
isOverride: true;
themeName: string | undefined;
}
);
const parsedBasename = ((): ParsedBasename | undefined => {
const match = basename.match(/^messages_([^.]+)\.properties$/);
if (match === null) {
return undefined;
}
const discriminator = match[1];
const split = discriminator.split("_override");
if (split.length === 1) {
return {
lang: discriminator,
isOverride: false
};
}
assert(split.length === 2);
if (split[1] === "") {
return {
lang: split[0],
isOverride: true,
themeName: undefined
};
}
const match2 = split[1].match(/^_(.+)$/);
assert(match2 !== null);
return {
lang: split[0],
isOverride: true,
themeName: match2[1]
};
})();
if (parsedBasename === undefined) {
return;
}
propertiesByLang[parsedBasename.lang] ??= {
base: createObjectThatThrowsIfAccessed<Buffer>({
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
}),
override: undefined,
overrideByThemeName: {}
};
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
if (parsedBasename.isOverride === false) {
propertiesByLang[parsedBasename.lang].base = buffer;
return;
}
if (parsedBasename.themeName === undefined) {
propertiesByLang[parsedBasename.lang].override = buffer;
return;
}
propertiesByLang[parsedBasename.lang].overrideByThemeName[
parsedBasename.themeName
] = buffer;
});
languageTags = Object.keys(propertiesByLang);
writeMessagePropertiesFilesByThemeType[themeType] = ({
messageDirPath,
themeName
}) => {
if (!fs.existsSync(messageDirPath)) {
fs.mkdirSync(messageDirPath, { recursive: true });
}
Object.entries(propertiesByLang).forEach(
([lang, { base, override, overrideByThemeName }]) => {
const messages = propertiesParser.parse(base.toString("utf8"));
if (override !== undefined) {
const overrideMessages = propertiesParser.parse(
override.toString("utf8")
);
Object.entries(overrideMessages).forEach(
([key, value]) => (messages[key] = value)
);
}
if (themeName in overrideByThemeName) {
const overrideMessages = propertiesParser.parse(
overrideByThemeName[themeName].toString("utf8")
);
Object.entries(overrideMessages).forEach(
([key, value]) => (messages[key] = value)
);
}
const editor = propertiesParser.createEditor();
Object.entries(messages).forEach(([key, value]) => {
editor.set(key, value);
});
fs.writeFileSync(
pathJoin(messageDirPath, `messages_${lang}.properties`),
Buffer.from(editor.toString(), "utf8")
);
}
);
};
}
keycloak_static_resources: {
if (isNative) {
break keycloak_static_resources;
}
if (isSpa) {
break keycloak_static_resources;
}
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES,
themeType
),
destDirPath: pathJoin(themeTypeDirPath, "resources")
});
}
bring_in_account_v1: {
if (isNative) {
break bring_in_account_v1;
}
if (themeType !== "account") {
break bring_in_account_v1;
}
assert(buildContext.implementedThemeTypes.account.isImplemented);
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
break bring_in_account_v1;
}
transformCodebase({
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
destDirPath: getThemeTypeDirPath({
themeName: "account-v1",
themeType: "account"
})
});
}
generate_theme_properties: {
if (isNative) {
break generate_theme_properties;
}
assert(themeType !== "email");
fs.writeFileSync(
pathJoin(themeTypeDirPath, "theme.properties"),
Buffer.from(
[
`parent=${(() => {
switch (themeType) {
case "account":
switch (getAccountThemeType()) {
case "Multi-Page":
return "account-v1";
case "Single-Page":
return "base";
}
case "login":
return "keycloak";
case "admin":
return "base";
}
assert<Equals<typeof themeType, never>>;
})()}`,
...(themeType === "account" &&
getAccountThemeType() === "Single-Page"
? ["deprecatedMode=false"]
: []),
...(buildContext.extraThemeProperties ?? []),
...[
...buildContext.environmentVariables,
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
].map(
({ name, default: defaultValue }) =>
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
),
...(languageTags === undefined
? []
: [`locales=${languageTags.join(",")}`])
].join("\n\n"),
"utf8"
)
);
}
}
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
for (const themeType of [...THEME_TYPES, "email"] as const) {
copy_main_theme_to_theme_variant_theme: {
let isNative: boolean;
{
const v = buildContext.implementedThemeTypes[themeType];
if (!v.isImplemented && !v.isImplemented_native) {
break copy_main_theme_to_theme_variant_theme;
}
isNative = !v.isImplemented && v.isImplemented_native;
}
if (!isNative && themeVariantName === themeName) {
break copy_main_theme_to_theme_variant_theme;
}
transformCodebase({
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
destDirPath: getThemeTypeDirPath({
themeName: themeVariantName,
themeType
}),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
patch_xKeycloakify_themeName: {
if (!fileRelativePath.endsWith(".ftl")) {
break patch_xKeycloakify_themeName;
}
if (
!isNative &&
pathBasename(fileRelativePath) !== fileRelativePath
) {
break patch_xKeycloakify_themeName;
}
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
...id<[string | RegExp, string]>(
isNative
? [
/xKeycloakify\.themeName/g,
`"${themeVariantName}"`
]
: [
`"themeName": "${themeName}"`,
`"themeName": "${themeVariantName}"`
]
)
),
"utf8"
);
if (!isNative) {
assert(
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
);
}
return { modifiedSourceCode };
}
return { modifiedSourceCode: sourceCode };
}
});
}
run_writeMessagePropertiesFiles: {
const writeMessagePropertiesFiles =
writeMessagePropertiesFilesByThemeType[themeType];
if (writeMessagePropertiesFiles === undefined) {
break run_writeMessagePropertiesFiles;
}
writeMessagePropertiesFiles({
messageDirPath: pathJoin(
getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
"messages"
),
themeName: themeVariantName
});
}
}
}
console.log(`Generated resources in ${Date.now() - start}ms`);
}

View File

@ -0,0 +1 @@
export * from "./generateResources";

View File

@ -1,19 +1,28 @@
import { crawl } from "../../tools/crawl";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
import {
type ThemeType,
ACCOUNT_THEME_PAGE_IDS,
LOGIN_THEME_PAGE_IDS
} from "../../shared/constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
export function readExtraPagesNames(params: {
themeSrcDirPath: string;
themeType: ThemeType;
}): string[] {
const { themeSrcDirPath, themeType } = params;
const filePaths = crawl({
"dirPath": pathJoin(themeSrcDirPath, themeType),
"returnedPathsType": "absolute"
dirPath: pathJoin(themeSrcDirPath, themeType),
returnedPathsType: "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath => /kcContext\.[^.]+$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath =>
/[kK]cContext\.[^.]+$/.test(filePath)
);
if (candidateFilePaths.length === 0) {
candidateFilePaths.push(...filePaths);
@ -24,15 +33,17 @@ export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType
for (const candidateFilPath of candidateFilePaths) {
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
extraPages.push(...Array.from(rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g), m => m[1]));
extraPages.push(
...Array.from(rawSourceFile.matchAll(/["']([^.\s]+.ftl)["']:/g), m => m[1])
);
}
return extraPages.reduce(...removeDuplicates<string>()).filter(pageId => {
switch (themeType) {
case "account":
return !id<readonly string[]>(accountThemePageIds).includes(pageId);
return !id<readonly string[]>(ACCOUNT_THEME_PAGE_IDS).includes(pageId);
case "login":
return !id<readonly string[]>(loginThemePageIds).includes(pageId);
return !id<readonly string[]>(LOGIN_THEME_PAGE_IDS).includes(pageId);
}
});
}

View File

@ -0,0 +1,91 @@
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
/** Assumes the theme type exists */
export function readFieldNameUsage(params: {
themeSrcDirPath: string;
themeType: Exclude<ThemeType, "admin">;
}): string[] {
const { themeSrcDirPath, themeType } = params;
// NOTE: We pre-populate with the synthetic user attributes defined in useUserProfileForm (can't be parsed automatically)
const fieldNames = new Set<string>([
"firstName",
"lastName",
"email",
"username",
"password",
"password-confirm"
]);
for (const srcDirPath of [
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
pathJoin(themeSrcDirPath, themeType)
]) {
const filePaths = crawl({
dirPath: srcDirPath,
returnedPathsType: "absolute"
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
if (!rawSourceFile.includes("messagesPerField")) {
continue;
}
for (const functionName of [
"printIfExists",
"existsError",
"get",
"exists",
"getFirstError"
] as const) {
if (!rawSourceFile.includes(functionName)) {
continue;
}
try {
rawSourceFile
.split(functionName)
.filter(part => part.startsWith("("))
.map(part => {
let [p1] = part.split(")");
p1 = p1.slice(1);
return p1;
})
.map(part => {
return part
.split(",")
.map(a => a.trim())
.filter((...[, i]) =>
functionName !== "printIfExists" ? true : i === 0
)
.filter(
a =>
a.startsWith('"') ||
a.startsWith("'") ||
a.startsWith("`")
)
.filter(
a =>
a.endsWith('"') ||
a.endsWith("'") ||
a.endsWith("`")
)
.map(a => a.slice(1).slice(0, -1));
})
.flat()
.forEach(fieldName => fieldNames.add(fieldName));
} catch {}
}
}
}
return Array.from(fieldNames);
}

Some files were not shown because too many files have changed in this diff Show More