Compare commits

...

1271 Commits
v9.7.5 ... main

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

View File

@ -231,6 +231,138 @@
"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

@ -13,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
@ -27,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
@ -70,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
@ -88,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 }}
@ -105,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/
/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.

317
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">
@ -38,22 +38,63 @@
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<br/>
<br/>
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
</p>
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
> NOTE: Keycloakify 10 is still in realase-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
> 📣 **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).
## Sponsor
## Sponsors
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.
Project backers, we trust and recommend their services.
[Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github) provides the following services:
<br/>
- Simplify and secure your Keycloak Identity and Access Management. Keycloak as a Service.
- Custom theme building for your brand using Keycloakify.
<div align="center">
![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">
@ -68,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)):
@ -116,7 +155,24 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<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/giorgoslytos"><img src="https://avatars.githubusercontent.com/u/50946162?v=4?s=100" width="100px;" alt="giorgoslytos"/><br /><sub><b>giorgoslytos</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=giorgoslytos" 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>
@ -125,230 +181,3 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
# Changelog highlights
## 9.5
- Post build hook: You can now apply custom transformation to your theme files. [Learn more](https://docs.keycloakify.dev/build-options#postbuild-hook).
- You can now specify your option in the Keycloakify's Vite plugin instead in the package.json. [See example](https://docs.keycloakify.dev/build-options#themename).
## 9.4
**Vite Support! 🎉**
- [The starter is now a Vite project](https://github.com/keycloakify/keycloakify-starter).
The Webpack based starter is accessible [here](https://github.com/keycloakify/keycloakify-starter-cra).
- CRA (Webpack) remains supported for the forseable future.
- If you have a CRA Keycloakify theme that you wish to migrate to Vite checkout [this migration guide](https://docs.keycloakify.dev/migration-guides/cra-greater-than-vite).
## 9.0
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
### Breaking changes
Very few. Check them out [here](https://docs.keycloakify.dev/migration-guides/v8-greater-than-v9).
## 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. [Check them out](https://docs.keycloakify.dev/migration-guides/v7-greater-than-v8).
## 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.themeVariantNames).
## 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)
## 5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## 5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
## 5.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).
## 4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## 4.8.0
[Email template customization.](#email-template-customization)
## 4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## 4.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.
## 4.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)
## 4.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.
## 4.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.
## 4.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.
## 4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## 3
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).
## 2.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`.
## 2
- 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,39 +1,26 @@
{
"name": "keycloakify",
"version": "9.7.2",
"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 -p src/vite-plugin && 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 src/**/*.java",
"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,84 +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",
"vite": "^5.0.12"
},
"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",
"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",
"magic-string": "^0.30.7"
"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,187 @@
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 { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { getLogger } from "../src/bin/tools/logger";
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.
//@ts-ignore
const propertiesParser = require("properties-parser");
const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "23.0.4";
if (require.main === module) {
generateI18nMessages();
}
async function generateI18nMessages() {
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
const accountI18nDirPath = pathJoin(
thisCodebaseRootDirPath,
"src",
"account",
"i18n"
);
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
fs.mkdirSync(tmpDirPath);
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
"cacheDirPath": pathJoin(thisCodebaseRootDirPath, "node_modules", ".cache", "keycloakify"),
"npmWorkspaceRootDirPath": thisCodebaseRootDirPath
}
});
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(thisCodebaseRootDirPath, "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(thisCodebaseRootDirPath, __filename)}`,
`//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,
@ -96,7 +190,11 @@ async function main() {
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 */"
@ -105,35 +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,
`import * as en from "./en";`,
"",
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
"export async function fetchMessages_defaultSet(currentLanguageTag: string) {",
" const { default: messages_defaultSet } = await (() => {",
" switch (currentLanguageTag) {",
` case "en": return en;`,
...languages
.filter(language => language !== "en")
.map(language => ` case "${language}": return import("./${language}");`),
.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

@ -2,78 +2,103 @@ import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import * as os from "os";
const singletonDependencies: string[] = ["react", "@types/react"];
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
const rootDirPath = getThisCodebaseRootDirPath();
const commonThirdPartyDeps = [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
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;
}
});
}
}

View File

@ -1,12 +1,14 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/constants";
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 thay you can use in Webpack projects.
* 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 = (window as any)[nameOfTheGlobal];
const kcContext: { "x-keycloakify": { resourcesPath: string } } | undefined = (
window as any
).kcContext;
if (kcContext === undefined || process.env.NODE_ENV === "development") {
assert(
@ -17,5 +19,5 @@ export const PUBLIC_URL = (() => {
return process.env.PUBLIC_URL;
}
return `${kcContext.url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}`;
return `${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}`;
})();

View File

@ -1,9 +1,8 @@
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";
import FederatedIdentity from "./pages/FederatedIdentity";
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"));
@ -11,8 +10,9 @@ 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 Fallback(props: PageProps<KcContext, I18n>) {
export default function DefaultPage(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (

View File

@ -1,7 +1,24 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
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";
import { type ThemeType } from "keycloakify/bin/constants";
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
@ -69,7 +86,10 @@ export declare namespace KcContext {
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
printIfExists: <T extends string>(
fieldName: string,
text: T
) => T | undefined;
/**
* Check if exists error message for given fields
*
@ -98,7 +118,10 @@ export declare namespace KcContext {
lastName?: string;
username?: string;
};
properties: Record<string, string | undefined>;
properties: {};
"x-keycloakify": {
messages: Record<string, string>;
};
};
export type Password = Common & {
@ -146,6 +169,7 @@ export declare namespace KcContext {
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
getAlgorithmKey: () => string;
} & (
| {
type: "totp";
@ -164,21 +188,6 @@ export declare namespace KcContext {
};
mode?: "qr" | "manual" | undefined | null;
isAppInitiatedAction: boolean;
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;
};
stateChecker: string;
};

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,34 +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")),
"htmlLangProperty": locale?.currentLanguageTag,
"documentTitle": i18n.msgStr("accountManagementTitle")
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;
}
@ -44,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>
@ -123,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,27 +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"
| "kcContentWrapperClass"
| "kcFormClass"
| "kcFormGroupClass"
| "kcInputWrapperClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass";
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,108 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { isStorybook } from "keycloakify/lib/isStorybook";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
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>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = 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
warn_that_mock_is_enbaled: {
if (isStorybook) {
break warn_that_mock_is_enbaled;
}
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
});
}
if (mockProperties !== undefined) {
deepAssign({
"target": kcContext.properties,
"source": mockProperties
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "account") {
return { "kcContext": undefined as any };
}
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 { nameOfTheGlobal } from "keycloakify/bin/constants";
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)[nameOfTheGlobal];
}

View File

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

View File

@ -1,260 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const resourcesPath = `${BASE_URL}${keycloak_resources}/account/resources`;
export const kcContextCommonMock: KcContext.Common = {
"themeVersion": "0.0.0",
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"themeName": "my-theme-name",
"url": {
resourcesPath,
"resourcesCommonPath": `${resourcesPath}/${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 */
{
"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"
},
"properties": {
"parent": "account-v1",
"kcButtonLargeClass": "btn-lg",
"locales": "ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"kcButtonPrimaryClass": "btn-primary",
"accountResourceProvider": "account-v1",
"styles":
"css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
"kcButtonClass": "btn",
"kcButtonDefaultClass": "btn-default"
}
};
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
}
},
"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
}
})
];

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,20 +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",
"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"
}
});

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

@ -1,17 +1,12 @@
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";
function isArrayWithEmptyObject(variable: any): boolean {
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
}
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { getClassName } = useGetClassName({
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
@ -67,7 +62,6 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
{index < application.realmRolesAvailable.length - 1 && ", "}
</span>
))}
{!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}
{application.resourceRolesAvailable &&
Object.keys(application.resourceRolesAvailable).map(resource => (
<span key={resource}>
@ -118,7 +112,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
application.additionalGrants.length > 0 ? (
<button
type="submit"
className={clsx(getClassName("kcButtonPrimaryClass"), getClassName("kcButtonClass"))}
className={kcClsx("kcButtonPrimaryClass", "kcButtonClass")}
id={`revoke-${application.client.clientId}`}
name="clientId"
value={application.client.id}
@ -136,3 +130,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
</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

@ -1,6 +1,6 @@
import { PageProps } from "keycloakify/account";
import { I18n } from "keycloakify/account/i18n";
import { KcContext } from "keycloakify/account/kcContext";
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;
@ -8,7 +8,7 @@ export default function FederatedIdentity(props: PageProps<Extract<KcContext, {
const { url, federatedIdentity, stateChecker } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity">
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="social">
<div className="main-layout social">
<div className="row">
<div className="col-md-10">

View File

@ -1,13 +1,13 @@
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 { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
import { Key } from "react";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { getClassName } = useGetClassName({
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
@ -18,7 +18,7 @@ export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.f
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
<div className={getClassName("kcContentWrapperClass")}>
<div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10">
<h2>{msg("accountLogHtmlTitle")}</h2>
</div>

View File

@ -1,12 +1,11 @@
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";
import type { KcContext } from "keycloakify/account/kcContext";
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
export type PageProps<NarrowedKcContext, I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: NarowedKcContext;
i18n: I18nExtended;
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

@ -1,13 +1,12 @@
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 Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
@ -17,7 +16,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
<div className={getClassName("kcContentWrapperClass")}>
<div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10">
<h2>{msg("sessionsHtmlTitle")}</h2>
</div>
@ -56,7 +55,7 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
<form action={url.sessionsUrl} method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<button id="logout-all-sessions" type="submit" className={clsx(getClassName("kcButtonDefaultClass"), getClassName("kcButtonClass"))}>
<button id="logout-all-sessions" type="submit" className={kcClsx("kcButtonDefaultClass", "kcButtonClass")}>
{msg("doLogOutAllSessions")}
</button>
</form>

View File

@ -1,27 +1,21 @@
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 { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
import { MessageKey } from "keycloakify/account/i18n/i18n";
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
const { kcClsx } = getKcClsx({
doUseDefaultCss,
classes
});
const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
};
const { msg, msgStr, advancedMsg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
@ -78,11 +72,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
<li>
<p>{msg("totpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.supportedApplications?.map(app => (
<li key={app}>{msg(app as MessageKey)}</li>
))}
</ul>
<ul id="kc-totp-supported-apps">{totp.supportedApplications?.map(app => <li key={app}>{advancedMsg(app)}</li>)}</ul>
</li>
{mode && mode == "manual" ? (
@ -105,7 +95,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
{msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li>
<li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits}
@ -145,9 +135,9 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
</li>
</ol>
<hr />
<form action={url.totpUrl} className={getClassName("kcFormClass")} id="kc-totp-settings-form" method="post">
<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={getClassName("kcFormGroupClass")}>
<div className={kcClsx("kcFormGroupClass")}>
<div className="col-sm-2 col-md-2">
<label htmlFor="totp" className="control-label">
{msg("authenticatorCode")}
@ -160,23 +150,28 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
id="totp"
name="totp"
autoComplete="off"
className={getClassName("kcInputClass")}
className={kcClsx("kcInputClass")}
aria-invalid={messagesPerField.existsError("totp")}
/>
{messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("totp")}
</span>
<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={getClassName("kcFormGroupClass")}>
<div className={kcClsx("kcFormGroupClass")}>
<div className="col-sm-2 col-md-2">
<label htmlFor="userLabel" className={getClassName("kcLabelClass")}>
<label htmlFor="userLabel" className={kcClsx("kcLabelClass")}>
{msg("totpDeviceName")}
</label>
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
@ -187,37 +182,33 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
id="userLabel"
name="userLabel"
autoComplete="off"
className={getClassName("kcInputClass")}
className={kcClsx("kcInputClass")}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={getClassName("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
<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(getClassName("kcFormGroupClass"), "text-right")}>
<div className={getClassName("kcInputWrapperClass")}>
<div id="kc-form-buttons" className={clsx(kcClsx("kcFormGroupClass"), "text-right")}>
<div className={kcClsx("kcInputWrapperClass")}>
<input
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonLargeClass")}
id="saveTOTPBtn"
value={msgStr("doSave")}
/>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass"),
getClassName("kcButtonLargeClass")
)}
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass", "kcButtonLargeClass")}
id="cancelTOTPBtn"
name="submitAction"
value="Cancel"

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,14 +0,0 @@
export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const resolvedViteConfigJsonBasename = "vite.json";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const retrocompatPostfix = "_retrocompat";
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const keycloakifyBuildOptionsForPostPostBuildScriptEnvName = "KEYCLOAKIFY_BUILD_OPTIONS_POST_POST_BUILD_SCRIPT";

View File

@ -1,35 +1,48 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources, type BuildOptionsLike } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { readBuildOptions } from "./keycloakify/buildOptions";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
import { readThisNpmProjectVersion } from "./tools/readThisNpmProjectVersion";
import { assert, type Equals } from "tsafe/assert";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import type { BuildContext } from "./shared/buildContext";
import { transformCodebase } from "./tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
const { processArgv } = params;
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const buildOptions = readBuildOptions({ processArgv });
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
if (hasBeenHandled) {
return;
}
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const { keycloakifyBuildinfoRaw } = generateKeycloakifyBuildinfoRaw({
destDirPath,
"keycloakifyVersion": readThisNpmProjectVersion(),
buildOptions
});
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");
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
@ -38,75 +51,46 @@ export async function copyKeycloakResourcesToPublic(params: { processArgv: strin
return;
}
rmSync(destDirPath, { "force": true, "recursive": true });
rmSync(destDirPath, { force: true, recursive: true });
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"keycloakVersion": (() => {
switch (themeType) {
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
case "account":
return lastKeycloakVersionWithAccountV1;
}
})(),
themeType,
"themeDirPath": destDirPath,
buildOptions
});
}
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This 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(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
}
export function generateKeycloakifyBuildinfoRaw(params: {
destDirPath: string;
keycloakifyVersion: string;
buildOptions: BuildOptionsLike & {
loginThemeResourcesFromKeycloakVersion: string;
};
}) {
const { destDirPath, keycloakifyVersion, buildOptions } = params;
const { cacheDirPath, npmWorkspaceRootDirPath, loginThemeResourcesFromKeycloakVersion, ...rest } = buildOptions;
assert<Equals<typeof rest, {}>>(true);
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion,
"buildOptions": {
loginThemeResourcesFromKeycloakVersion,
"cacheDirPath": pathRelative(destDirPath, cacheDirPath),
"npmWorkspaceRootDirPath": pathRelative(destDirPath, npmWorkspaceRootDirPath)
}
},
null,
2
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
return { keycloakifyBuildinfoRaw };
}
async function main() {
await copyKeycloakResourcesToPublic({
"processArgv": process.argv.slice(2)
});
}
if (require.main === module) {
main();
}

View File

@ -1,290 +0,0 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
import { assert } from "tsafe/assert";
import * as child_process from "child_process";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"specificDirsToExtract": ["", "-community"].map(ext => `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`),
buildOptions,
"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"
});
}
repatriate_common_resources_from_base_login_theme: {
const baseLoginThemeResourceDir = pathJoin(destDirPath, "base", "login", "resources");
if (!fs.existsSync(baseLoginThemeResourceDir)) {
break repatriate_common_resources_from_base_login_theme;
}
transformCodebase({
"srcDirPath": baseLoginThemeResourceDir,
"destDirPath": pathJoin(destDirPath, "keycloak", "login", "resources")
});
}
install_and_move_to_common_resources_generated_in_keycloak_v2: {
if (!fs.readFileSync(pathJoin(destDirPath, "keycloak", "login", "theme.properties")).toString("utf8").includes("web_modules")) {
break 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;
}
const packageManager = fs.existsSync(pathJoin(accountV2DirSrcDirPath, "pnpm-lock.yaml")) ? "pnpm" : "npm";
if (packageManager === "pnpm") {
try {
child_process.execSync(`which pnpm`);
} catch {
console.log(`Installing pnpm globally`);
child_process.execSync(`npm install -g pnpm`);
}
}
child_process.execSync(`${packageManager} 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(`${packageManager} run check-types`, "true")
.replace(`${packageManager} run babel`, "true");
fs.writeFileSync(packageJsonFilePath, Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8"));
child_process.execSync(`${packageManager} run build`, { "cwd": accountV2DirSrcDirPath, "stdio": "ignore" });
fs.writeFileSync(packageJsonFilePath, packageJsonRaw);
fs.rmSync(pathJoin(accountV2DirSrcDirPath, "node_modules"), { "recursive": true });
}
remove_keycloak_v2: {
const keycloakV2DirPath = pathJoin(destDirPath, "keycloak.v2");
if (!fs.existsSync(keycloakV2DirPath)) {
break remove_keycloak_v2;
}
rmSync(keycloakV2DirPath, { "recursive": true });
}
// Note, this is an optimization for reducing the size of the jar
remove_unused_node_modules: {
const nodeModuleDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
if (!fs.existsSync(nodeModuleDirPath)) {
break remove_unused_node_modules;
}
const toDeletePerfixes = [
"angular",
"bootstrap",
"rcue",
"font-awesome",
"ng-file-upload",
pathJoin("patternfly", "dist", "sass"),
pathJoin("patternfly", "dist", "less"),
pathJoin("patternfly", "dist", "js"),
"d3",
pathJoin("jquery", "src"),
"c3",
"core-js",
"eonasdan-bootstrap-datetimepicker",
"moment",
"react",
"patternfly-bootstrap-treeview",
"popper.js",
"tippy.js",
"jquery-match-height",
"google-code-prettify",
"patternfly-bootstrap-combobox",
"focus-trap",
"tabbable",
"scheduler",
"@types",
"datatables.net",
"datatables.net-colreorder",
"tslib",
"prop-types",
"file-selector",
"datatables.net-colreorder-bs",
"object-assign",
"warning",
"js-tokens",
"loose-envify",
"prop-types-extra",
"attr-accept",
"datatables.net-select",
"drmonty-datatables-colvis",
"datatables.net-bs",
pathJoin("@patternfly", "react"),
pathJoin("@patternfly", "patternfly", "docs")
];
transformCodebase({
"srcDirPath": nodeModuleDirPath,
"destDirPath": nodeModuleDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
return undefined;
}
if (fileRelativePath.startsWith(pathJoin("patternfly", "dist", "fonts"))) {
if (
!fileRelativePath.endsWith(".woff2") &&
!fileRelativePath.endsWith(".woff") &&
!fileRelativePath.endsWith(".ttf")
) {
return undefined;
}
}
return { "modifiedSourceCode": sourceCode };
}
});
}
// Just like node_modules
remove_unused_lib: {
const libDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "lib");
if (!fs.existsSync(libDirPath)) {
break remove_unused_lib;
}
const toDeletePerfixes = ["ui-ace", "filesaver", "fileupload", "angular", "ui-ace"];
transformCodebase({
"srcDirPath": libDirPath,
"destDirPath": libDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (fileRelativePath.endsWith(".map")) {
return undefined;
}
if (toDeletePerfixes.find(prefix => fileRelativePath.startsWith(prefix)) !== undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
});
}
last_account_v1_transformations: {
if (lastKeycloakVersionWithAccountV1 !== keycloakVersion) {
break last_account_v1_transformations;
}
{
const accountCssFilePath = pathJoin(destDirPath, "keycloak", "account", "resources", "css", "account.css");
fs.writeFileSync(
accountCssFilePath,
Buffer.from(fs.readFileSync(accountCssFilePath).toString("utf8").replace("top: -34px;", "top: -34px !important;"), "utf8")
);
}
// Note, this is an optimization for reducing the size of the jar,
// For this version we know exactly which resources are used.
{
const nodeModulesDirPath = pathJoin(destDirPath, "keycloak", "common", "resources", "node_modules");
const toKeepPrefixes = [
...["patternfly.min.css", "patternfly-additions.min.css", "patternfly-additions.min.css"].map(fileBasename =>
pathJoin("patternfly", "dist", "css", fileBasename)
),
pathJoin("patternfly", "dist", "fonts")
];
transformCodebase({
"srcDirPath": nodeModulesDirPath,
"destDirPath": nodeModulesDirPath,
"transformSourceCode": ({ sourceCode, fileRelativePath }) => {
if (toKeepPrefixes.find(prefix => fileRelativePath.startsWith(prefix)) === undefined) {
return undefined;
}
return { "modifiedSourceCode": sourceCode };
}
});
}
}
}
}
});
}
async function main() {
const buildOptions = readBuildOptions({
"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({
keycloakVersion,
destDirPath,
buildOptions
});
}
if (require.main === module) {
main();
}

View File

@ -1,203 +0,0 @@
import { createHash } from "crypto";
import { mkdir, writeFile, unlink } from "fs/promises";
import fetch from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { transformCodebase } from "./tools/transformCodebase";
import { unzip, zip } from "./tools/unzip";
import { rm } from "./tools/fs.rm";
import * as child_process from "child_process";
import { existsAsync } from "./tools/fs.existsAsync";
import type { BuildOptions } from "./keycloakify/buildOptions";
import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadAndUnzip(params: {
url: string;
destDirPath: string;
specificDirsToExtract?: string[];
preCacheTransform?: {
actionCacheId: string;
action: (params: { destDirPath: string }) => Promise<void>;
};
buildOptions: BuildOptionsLike;
}) {
const { url, destDirPath, specificDirsToExtract, preCacheTransform, buildOptions } = params;
const { extractDirPath, zipFilePath } = (() => {
const zipFileBasenameWithoutExt = generateFileNameFromURL({
url,
"preCacheTransform":
preCacheTransform === undefined
? undefined
: {
"actionCacheId": preCacheTransform.actionCacheId,
"actionFootprint": preCacheTransform.action.toString()
}
});
const zipFilePath = pathJoin(buildOptions.cacheDirPath, `${zipFileBasenameWithoutExt}.zip`);
const extractDirPath = pathJoin(buildOptions.cacheDirPath, `tmp_unzip_${zipFileBasenameWithoutExt}`);
return { zipFilePath, extractDirPath };
})();
download_zip_and_transform: {
if (await existsAsync(zipFilePath)) {
break download_zip_and_transform;
}
const { response, isFromRemoteCache } = await (async () => {
const proxyFetchOptions = await getProxyFetchOptions({
"npmWorkspaceRootDirPath": buildOptions.npmWorkspaceRootDirPath
});
const response = await fetch(
`https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/${pathBasename(zipFilePath)}`,
proxyFetchOptions
);
if (response.status === 200) {
return {
response,
"isFromRemoteCache": true
};
}
return {
"response": await fetch(url, proxyFetchOptions),
"isFromRemoteCache": false
};
})();
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
* does not support node-fetch 3.x. So we stick around with this band-aid until
* octokit upgrades.
*/
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
if (isFromRemoteCache) {
break download_zip_and_transform;
}
if (specificDirsToExtract === undefined && preCacheTransform === undefined) {
break download_zip_and_transform;
}
await unzip(zipFilePath, extractDirPath, specificDirsToExtract);
try {
await preCacheTransform?.action({
"destDirPath": extractDirPath
});
} catch (error) {
await Promise.all([rm(extractDirPath, { "recursive": true }), unlink(zipFilePath)]);
throw error;
}
await unlink(zipFilePath);
await zip(extractDirPath, zipFilePath);
await rm(extractDirPath, { "recursive": true });
upload_to_remot_cache_if_admin: {
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
if (githubToken === undefined) {
break upload_to_remot_cache_if_admin;
}
console.log("uploading to remote cache");
try {
child_process.execSync(`which putasset`);
} catch {
child_process.execSync(`npm install -g putasset`);
}
try {
child_process.execFileSync("putasset", [
"--owner",
"keycloakify",
"--repo",
"keycloakify",
"--tag",
"v0.0.1",
"--filename",
zipFilePath,
"--token",
githubToken
]);
} catch {
console.log("upload failed, asset probably already exists in remote cache");
}
}
}
await unzip(zipFilePath, extractDirPath);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
await rm(extractDirPath, { "recursive": true });
}
function generateFileNameFromURL(params: {
url: string;
preCacheTransform:
| {
actionCacheId: string;
actionFootprint: string;
}
| undefined;
}): string {
const { preCacheTransform } = params;
// Parse the URL
const url = new URL(params.url);
// Extract pathname and remove leading slashes
let fileName = url.pathname.replace(/^\//, "").replace(/\//g, "_");
// Optionally, add query parameters replacing special characters
if (url.search) {
fileName += url.search.replace(/[&=?]/g, "-");
}
// Replace any characters that are not valid in filenames
fileName = fileName.replace(/[^a-zA-Z0-9-_]/g, "");
// Trim or pad the fileName to a specific length
fileName = fileName.substring(0, 50);
add_pre_cache_transform: {
if (preCacheTransform === undefined) {
break add_pre_cache_transform;
}
// Sanitize actionCacheId the same way as other components
const sanitizedActionCacheId = preCacheTransform.actionCacheId.replace(/[^a-zA-Z0-9-_]/g, "_");
fileName += `_${sanitizedActionCacheId}_${createHash("sha256").update(preCacheTransform.actionFootprint).digest("hex").substring(0, 5)}`;
}
return fileName;
}

View File

@ -1,64 +0,0 @@
#!/usr/bin/env node
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } 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 "./getThemeSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
import { getReactAppRootDirPath } from "./keycloakify/buildOptions/getReactAppRootDirPath";
(async () => {
console.log("Select a theme type");
const { reactAppRootDirPath } = getReactAppRootDirPath({
"processArgv": process.argv.slice(2)
});
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({ reactAppRootDirPath });
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(getThisCodebaseRootDirPath(), "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 "./constants";
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: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
const srcDirPath = pathJoin(reactAppRootDirPath, "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,60 +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 "./getThemeSrcDirPath";
import { rmSync } from "./tools/fs.rmSync";
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 buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2)
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
if (hasBeenHandled) {
return;
}
const { themeSrcDirPath } = getThemeSrcDirPath({
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
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({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
buildOptions
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>;
};
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

@ -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,25 +0,0 @@
import { z } from "zod";
export type UserProvidedBuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
export const zUserProvidedBuildOptions = z.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
});

View File

@ -1,193 +0,0 @@
import { parse as urlParse } from "url";
import { readParsedPackageJson } from "./parsedPackageJson";
import { join as pathJoin } from "path";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { readResolvedViteConfig } from "./resolvedViteConfig";
import * as fs from "fs";
import { getCacheDirPath } from "./getCacheDirPath";
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = {
bundler: "vite" | "webpack";
isSilent: boolean;
themeVersion: string;
themeNames: string[];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
doCreateJar: boolean;
loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
publicDirPath: string;
cacheDirPath: 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;
assetsDirPath: string;
doBuildRetrocompatAccountTheme: boolean;
npmWorkspaceRootDirPath: string;
};
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
const { processArgv } = params;
const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
if (resolvedViteConfig === undefined && fs.existsSync(pathJoin(reactAppRootDirPath, "vite.config.ts"))) {
throw new Error("Keycloakify's Vite plugin output not found");
}
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
const userProvidedBuildOptions = {
...userProvidedBuildOptionsFromPackageJson,
...resolvedViteConfig?.userProvidedBuildOptions
};
const themeNames = (() => {
if (userProvidedBuildOptions.themeName === undefined) {
return [
parsedPackageJson.name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-")
];
}
if (typeof userProvidedBuildOptions.themeName === "string") {
return [userProvidedBuildOptions.themeName];
}
return userProvidedBuildOptions.themeName;
})();
const reactAppBuildDirPath = (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "build");
}
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
})();
const argv = parseArgv(processArgv);
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
return {
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames,
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
userProvidedBuildOptions.groupId ??
(parsedPackageJson.homepage === undefined
? fallbackGroupId
: urlParse(parsedPackageJson.homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"doCreateJar": userProvidedBuildOptions.doCreateJar ?? true,
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
reactAppRootDirPath,
reactAppBuildDirPath,
"keycloakifyBuildDirPath": (() => {
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return pathJoin(
reactAppRootDirPath,
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
);
})(),
"publicDirPath": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
if (process.env.PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.PUBLIC_DIR_PATH,
"cwd": reactAppRootDirPath
});
}
return pathJoin(reactAppRootDirPath, "public");
}
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
})(),
cacheDirPath,
"urlPathname": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
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;
}
return resolvedViteConfig.urlPathname;
})(),
"assetsDirPath": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {
break webpack;
}
return pathJoin(reactAppBuildDirPath, "static");
}
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
})(),
"doBuildRetrocompatAccountTheme": userProvidedBuildOptions.doBuildRetrocompatAccountTheme ?? true,
npmWorkspaceRootDirPath
};
}

View File

@ -1,25 +0,0 @@
import { join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
const cacheDirPath = pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
);
return { cacheDirPath };
}

View File

@ -1,49 +0,0 @@
import * as child_process from "child_process";
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
let cache:
| {
reactAppRootDirPath: string;
npmWorkspaceRootDirPath: string;
}
| undefined = undefined;
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
use_cache: {
if (cache === undefined || cache.reactAppRootDirPath !== reactAppRootDirPath) {
break use_cache;
}
const { npmWorkspaceRootDirPath } = cache;
return { npmWorkspaceRootDirPath };
}
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
try {
child_process.execSync("npm config get", { cwd, "stdio": ["pipe", "pipe", "pipe"] });
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep, "NPM workspace not found");
return callee(depth + 1);
}
throw error;
}
return cwd;
})(0);
cache = {
reactAppRootDirPath,
npmWorkspaceRootDirPath
};
return { npmWorkspaceRootDirPath };
}

View File

@ -1,23 +0,0 @@
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
export function getReactAppRootDirPath(params: { processArgv: string[] }) {
const { processArgv } = params;
const argv = parseArgv(processArgv);
const reactAppRootDirPath = (() => {
const arg = argv["project"] ?? argv["p"];
if (typeof arg !== "string") {
return process.cwd();
}
return getAbsoluteAndInOsFormatPath({
"pathIsh": arg,
"cwd": process.cwd()
});
})();
return { reactAppRootDirPath };
}

View File

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

View File

@ -1,32 +0,0 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { type UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: UserProvidedBuildOptions;
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": zUserProvidedBuildOptions.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ParsedPackageJson;
export function readParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;
}
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
return parsedPackageJson;
}

View File

@ -1,74 +0,0 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
import { UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
userProvidedBuildOptions: UserProvidedBuildOptions;
};
const zResolvedViteConfig = z.object({
"buildDir": z.string(),
"publicDir": z.string(),
"assetsDir": z.string(),
"urlPathname": z.string().optional(),
"userProvidedBuildOptions": zUserProvidedBuildOptions
});
{
type Got = ReturnType<(typeof zResolvedViteConfig)["parse"]>;
type Expected = OptionalIfCanBeUndefined<ResolvedViteConfig>;
assert<Equals<Got, Expected>>();
}
export function readResolvedViteConfig(params: { cacheDirPath: string }): {
resolvedViteConfig: ResolvedViteConfig | undefined;
} {
const { cacheDirPath } = params;
const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
return { "resolvedViteConfig": undefined };
}
const resolvedViteConfig = (() => {
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
throw new Error("Missing Keycloakify Vite plugin output.");
}
let out: ResolvedViteConfig;
try {
out = JSON.parse(fs.readFileSync(resolvedViteConfigJsonFilePath).toString("utf8"));
} catch {
throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
}
try {
const zodParseReturn = zResolvedViteConfig.parse(out);
// So that objectKeys from tsafe return the expected result no matter what.
Object.keys(zodParseReturn)
.filter(key => !(key in out))
.forEach(key => {
delete (out as any)[key];
});
} catch {
throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
}
return out;
})();
return { resolvedViteConfig };
}

View File

@ -1,722 +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}";
try {
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} catch(error) {
}
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, path: " + path?join(".")>
</#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 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(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 &&
is_subpath(path, ["applications", "applications"]) &&
(
key == "realm" ||
key == "container"
)
) || (
are_same_path(path, ["user"]) &&
key == "delegateForUpdate"
) || (
<#-- Security audit forwarded by Garth (Gmail) -->
are_same_path(path, ["client", "attributes"]) &&
key == "saml.signing.private.key"
) || (
<#-- See: https://github.com/keycloakify/keycloakify/issues/534 -->
are_same_path(path, ["login"]) &&
key == "password"
)
>
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if (
["register.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
key == "attemptedUsername" && are_same_path(path, ["auth"])
)>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]>
</#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 is_subpath 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 are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function>
</script>

View File

@ -1,34 +1,48 @@
import cheerio from "cheerio";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
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 type { BuildOptions } from "../buildOptions";
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
import {
type ThemeType,
WELL_KNOWN_DIRECTORY_BASE_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
} from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
themeVersion: string;
urlPathname: string | undefined;
reactAppBuildDirPath: string;
assetsDirPath: string;
};
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
BuildContextLike_replaceImportsInCssCode & {
urlPathname: string | undefined;
themeVersion: string;
kcContextExclusionsFtlCode: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
themeName: string;
indexHtmlCode: string;
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
buildContext: BuildContextLike;
keycloakifyVersion: string;
themeType: ThemeType;
fieldNames: string[];
}) {
const { themeName, cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const {
themeName,
indexHtmlCode,
buildContext,
keycloakifyVersion,
themeType,
fieldNames
} = params;
const $ = cheerio.load(indexHtmlCode);
@ -38,7 +52,10 @@ export function generateFtlFilesCodeFactory(params: {
assert(jsCode !== null);
const { fixedJsCode } = replaceImportsInJsCode({ jsCode, buildOptions });
const { fixedJsCode } = replaceImportsInJsCode({
jsCode,
buildContext
});
$(element).text(fixedJsCode);
});
@ -48,9 +65,10 @@ export function generateFtlFilesCodeFactory(params: {
assert(cssCode !== null);
const { fixedCssCode } = replaceImportsInInlineCssCode({
const { fixedCssCode } = replaceImportsInCssCode({
cssCode,
buildOptions
cssFileRelativeDirPath: undefined,
buildContext
});
$(element).text(fixedCssCode);
@ -59,7 +77,8 @@ export function generateFtlFilesCodeFactory(params: {
(
[
["link", "href"],
["script", "src"]
["script", "src"],
["script", "data-src"]
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
@ -72,58 +91,46 @@ export function generateFtlFilesCodeFactory(params: {
$(element).attr(
attrName,
href.replace(
new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`),
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
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", themeName)
.replace("RESOURCES_COMMON_cLsLsMrtDkpVv", resources_common),
"<!-- 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.${nameOfTheGlobal}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]
].join("\n")
`<script>\n${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}\n</script>`
);
// Remove part of the document marked as ignored.
@ -132,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();
@ -159,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,41 +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",
"sessions.ftl",
"totp.ftl",
"applications.ftl",
"log.ftl",
"federatedIdentity.ftl"
] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];

View File

@ -1,70 +0,0 @@
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./buildOptions";
type BuildOptionsLike = {
groupId: string;
artifactId: string;
themeVersion: string;
keycloakifyBuildDirPath: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generatePom(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = 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>${buildOptions.groupId}</groupId>`,
` <artifactId>${buildOptions.artifactId}</artifactId>`,
` <version>${buildOptions.themeVersion}</version>`,
` <name>${buildOptions.artifactId}</name>`,
` <description />`,
` <packaging>jar</packaging>`,
` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`,
` <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>`,
` <dependency>`,
` <groupId>io.phasetwo.keycloak</groupId>`,
` <artifactId>keycloak-account-v1</artifactId>`,
` <version>0.1</version>`,
` </dependency>`,
` </dependencies>`,
`</project>`
].join("\n");
return { pomFileCode };
})();
return { pomFileCode };
}

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
}
});
}

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