Compare commits

...

440 Commits

Author SHA1 Message Date
f256b74929 Bump version 2023-04-20 20:52:23 +02:00
4f1182a230 Feat sml-post-form.ftl #277 2023-04-20 20:51:46 +02:00
e7c20547f8 Bump version 2023-04-20 20:09:29 +02:00
9ab4c510fe #209 2023-04-20 20:08:47 +02:00
7d78c52064 Bump version 2023-04-20 18:15:18 +02:00
6223d91291 Update post build instructions 2023-04-20 18:15:03 +02:00
840b5e1312 Bump version 2023-04-20 13:18:17 +02:00
e69813f6e3 Fix small logical error 2023-04-20 13:17:57 +02:00
3c0c057e06 Bump version 2023-04-20 13:10:46 +02:00
984d12b3f2 Fix bad types in Account kcContext 2023-04-20 13:10:33 +02:00
61dc54f115 Bump version 2023-04-20 13:05:33 +02:00
34e47cccc1 Enable to redirect back to the application from the account pages 2023-04-20 13:05:01 +02:00
c170345550 Update README 2023-04-20 05:52:04 +02:00
1e40706f72 Update CNAME 2023-04-20 05:44:16 +02:00
ea1a747ebf Add all missing pages to the storybook 2023-04-20 05:41:34 +02:00
a14e967020 Add IdpReviewUserProfile.tsx story 2023-04-20 05:19:38 +02:00
0fff10d2c6 Bump version 2023-04-20 04:18:36 +02:00
7c2123614d Remove margin for canvas container 2023-04-20 04:18:17 +02:00
d149866703 Add story for password.ftl 2023-04-20 04:17:37 +02:00
18039140db Important fix, assets common where broken 2023-04-20 04:17:12 +02:00
4de9599018 Bump version 2023-04-20 03:37:31 +02:00
bb85829d71 Add referrerURI to the base type and make it optional 2023-04-20 03:37:11 +02:00
ff077943ec Fixe syntax error in DockContainer, remove story padding 2023-04-20 02:55:05 +02:00
f057114bcc Add account/password.ftl to storybook 2023-04-20 02:54:24 +02:00
e7bfe7f80d Add account/account.ftl to storybook 2023-04-20 02:52:49 +02:00
18112a97ab Deal with story ordering 2023-04-20 02:41:06 +02:00
8ee6fb58ac Reproduce directory layout #274 2023-04-20 01:55:13 +02:00
08831fc31d Add storybook Error page #274 2023-04-20 01:53:36 +02:00
c5c25394fb Hide addon pannel by default 2023-04-20 01:28:02 +02:00
2f649c9866 Remove forgoten console.log 2023-04-20 00:14:00 +02:00
91c5dd40fa Enable the storybook to default to canevas 2023-04-20 00:13:25 +02:00
e95e688cf0 Add rotating logo for the intro 2023-04-19 23:41:25 +02:00
9845f1de08 Update prettierignore 2023-04-19 23:09:30 +02:00
07032d312d Correctly load up the fonts 2023-04-19 22:29:46 +02:00
ccb5d32763 update yarn.lock 2023-04-19 19:08:56 +02:00
bf83e4b03b docs: update .all-contributorsrc [skip ci] 2023-04-19 19:08:56 +02:00
03b491763f docs: update README.md [skip ci] 2023-04-19 19:08:56 +02:00
3abc9edf0e Rename missnamed components 2023-04-19 19:04:48 +02:00
f9accc51d3 Bump version #303 2023-04-19 17:57:21 +02:00
c3bade81b4 Restore copy assets to public in the keycloakify script for backward compatibility 2023-04-19 17:50:27 +02:00
6edd1f00dd Update prettier ignore 2023-04-19 17:36:18 +02:00
02be899629 Merge pull request #325 from Gravity-Software-srl/main
fix typing of algToKeyUriAlg + totp interface + cleanup build dir
2023-04-19 17:34:24 +02:00
8e043f289a sync with upstream main 2023-04-19 17:23:52 +02:00
30fecf8578 Revert "add build option keepBuildDir"
This reverts commit 86884607ef.
2023-04-19 17:04:46 +02:00
1112da33e3 Fix build 2023-04-19 05:32:19 +02:00
ffa65e871e Fix build 2023-04-19 05:10:25 +02:00
f49c7b465b Release beta 2023-04-19 05:05:21 +02:00
e6f75156ec New script only for copying default assets to public 2023-04-19 05:04:11 +02:00
ebafeb19ad Bump version 2023-04-19 03:21:25 +02:00
5166c719c4 Better scripts 2023-04-19 03:21:04 +02:00
bf92ea8340 Update yarn.lock 2023-04-19 03:20:47 +02:00
cf1e595ba2 Clean up dynamically inserted assets when template is unmounted #274 2023-04-19 03:20:22 +02:00
2bf3296c0f Attempt to fix ci 2023-04-18 04:35:16 +02:00
11513f73b7 Add discord 2023-04-18 04:29:02 +02:00
b6f60c6835 Update prettierignore 2023-04-18 04:16:49 +02:00
e9d276010f Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-18 04:12:07 +02:00
b08c4b0b29 Update yarn.lock 2023-04-18 04:11:53 +02:00
d684807d96 Copy keycloak assets into storybook static #274 2023-04-18 04:04:55 +02:00
9a60ef7c47 Update README.md 2023-04-17 11:13:24 +02:00
cc446059de Moving on with setup of the reference storybook #274 2023-04-17 04:02:34 +02:00
d75b809c13 Bump version 2023-04-17 04:02:33 +02:00
9fc3998cf7 Avoid deprecating getKcContext #274 2023-04-17 04:02:18 +02:00
238baa72cf Bump version 2023-04-17 01:33:01 +02:00
089f0f7a87 More explicit naming 2023-04-17 01:32:06 +02:00
aa9d3d1931 Bump version 2023-04-17 00:46:45 +02:00
2fc6aed4f1 Correct the account password page 2023-04-17 00:46:30 +02:00
c2fdea7886 Bump version 2023-04-17 00:28:34 +02:00
c8f71946d4 We where copying login theme assets into accont theme 2023-04-17 00:27:49 +02:00
d1cc6ed88d Smarter getKcContext typing 2023-04-16 03:00:03 +02:00
f6e6cf3750 Better typing for createGetKcContext 2023-04-16 02:36:15 +02:00
d1c7491704 Create a storybook friendly getKcContext 2023-04-16 02:09:26 +02:00
fd49c2fd23 Add step to build storybook 2023-04-15 22:23:09 +02:00
f7fb2efcdd Setup Storybook v6 (I spent 2hours trying to use v7 instead but it isn't worth it 2023-04-15 22:18:11 +02:00
ff0608c202 Update Contributor list 2023-04-15 01:59:01 +02:00
e63e20eade update dontributor list 2023-04-15 01:56:06 +02:00
335292cf4c Merge pull request #323 from keycloakify/all-contributors/add-asashay
docs: add asashay as a contributor for test, and code
2023-04-15 01:55:22 +02:00
7cb927c8b8 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:55:13 +00:00
802d6b3dad docs: update README.md [skip ci] 2023-04-14 23:55:12 +00:00
c16bf28369 update dontributor list 2023-04-15 01:54:05 +02:00
0de76ae613 Merge pull request #322 from keycloakify/all-contributors/add-marcmrf
docs: add marcmrf as a contributor for test, and code
2023-04-15 01:52:32 +02:00
880396e3a6 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:52:23 +00:00
879fc2812d docs: update README.md [skip ci] 2023-04-14 23:52:22 +00:00
6ac6209bd0 Merge pull request #321 from keycloakify/all-contributors/add-lazToum
docs: add lazToum as a contributor for test, and code
2023-04-15 01:50:15 +02:00
d7fd76c568 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:50:04 +00:00
543e08276f docs: update README.md [skip ci] 2023-04-14 23:50:03 +00:00
2e647b9196 Merge pull request #320 from keycloakify/all-contributors/add-juffe
docs: add juffe as a contributor for test, and code
2023-04-15 01:47:54 +02:00
69cf556582 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:47:46 +00:00
e168ee2ae6 docs: update README.md [skip ci] 2023-04-14 23:47:45 +00:00
274d758ba8 Merge pull request #319 from keycloakify/all-contributors/add-0x-Void
docs: add 0x-Void as a contributor for test, and code
2023-04-15 01:46:03 +02:00
b52e35be7d docs: update .all-contributorsrc [skip ci] 2023-04-14 23:45:51 +00:00
7f1ba8f166 docs: update README.md [skip ci] 2023-04-14 23:45:50 +00:00
72a5b9bac5 Merge pull request #318 from keycloakify/all-contributors/add-aidangilmore
docs: add aidangilmore as a contributor for test, and code
2023-04-15 01:44:54 +02:00
34fb0c2753 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:44:45 +00:00
e5f0885cb0 docs: update README.md [skip ci] 2023-04-14 23:44:44 +00:00
4f93190162 Reorder all contributor 2023-04-15 01:42:35 +02:00
9d1dcd278a Merge pull request #317 from keycloakify/all-contributors/add-revolunet
docs: add revolunet as a contributor for test, and code
2023-04-15 01:37:12 +02:00
45d4bce0e7 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:36:46 +00:00
680a7206d3 docs: update README.md [skip ci] 2023-04-14 23:36:44 +00:00
8a08e9fd64 Add contributors 2023-04-15 01:35:52 +02:00
0080dabe09 Include Cloud IAM in the README 2023-04-15 00:44:48 +02:00
556ce60b27 Bump version 2023-04-13 15:10:27 +02:00
12857e3027 Update Account.tsx #306 2023-04-13 15:09:48 +02:00
10965b82a9 Merge remote-tracking branch 'upstream/main' 2023-04-12 11:45:11 +02:00
86884607ef add build option keepBuildDir
if set to true, will not cleanup build_keycloak directory
2023-04-12 11:44:37 +02:00
1ff0449332 removed "$" typo in LoginConfigTotp.tsx 2023-04-11 15:44:54 +02:00
57b056b388 Bump version 2023-04-07 02:04:17 +02:00
9058e9ac9d Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-07 01:39:46 +02:00
ad3de8bff5 Bump version 2023-04-07 01:39:30 +02:00
476b100b04 Remove warning about last release not working 2023-04-07 01:39:15 +02:00
b2c7c86609 Fix the ftl script 2023-04-07 01:38:45 +02:00
f8a8ec2e4d Bump version 2023-04-06 22:41:52 +02:00
393a5ba125 Merge pull request #304 from keycloakify/fix-broken-jar
Fix-broken-jar
2023-04-06 22:41:20 +02:00
466c2d3eb4 chore: reenable test cleanup 2023-04-06 22:06:42 +02:00
b325b3537f style: fix formatting 2023-04-06 22:06:14 +02:00
e429127313 chore(jar): add jar test 2023-04-06 22:02:45 +02:00
2d05521789 fix(jar): fix empty jar 2023-04-06 21:34:20 +02:00
564ffc2be9 infer type of algToKeyUriAlg from type of kcConfig + fix totp interface
- add more yarn dirs to .gitignore
2023-04-06 17:50:26 +02:00
feaf34c124 Include the theme version in kcContext 2023-04-06 16:38:13 +02:00
c1e0563eba latest release broken 2023-04-06 11:10:44 +02:00
1c66f35337 Merge pull request #302 from keycloakify/json-schema
feat: add keycloakify json schema
2023-04-06 08:19:54 +02:00
4a7dd64982 feat: add keycloakify json schema 2023-04-05 20:04:54 +00:00
a84f984a07 Bump version 2023-04-05 18:14:07 +02:00
1f31a228d7 Merge pull request #301 from keycloakify/lordvlad-patch-1
Update jar.ts
2023-04-05 18:00:06 +02:00
309308db15 Update jar.ts
Yazl requires explicit handling of directories
2023-04-05 17:41:23 +02:00
a02c38ac45 Fix eject-keycloak-page 2023-04-04 02:53:56 +02:00
49e495dbbe Include keycloakfiy version number in kcContext (for debug purpose) 2023-04-04 01:40:55 +02:00
f6c2ccb0d6 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-03 22:21:19 +02:00
dcd30f2cad Simplification on FTL script 2023-04-03 22:13:29 +02:00
e5d540ebd2 Fix CI 2023-04-03 22:03:05 +02:00
1073a610d6 Update CI 2023-04-03 22:03:04 +02:00
034f6f8b0e Bump version 2023-04-03 22:03:04 +02:00
3edb23be97 #297 2023-04-03 22:03:04 +02:00
d308c04465 Fix type error in useDownloadTerms 2023-04-03 22:03:04 +02:00
c5899eba94 Refactor Terms.tsx 2023-04-03 22:03:04 +02:00
97cb20b731 Fix CI 2023-04-03 21:56:48 +02:00
f58a5ad524 Merge branch 'main' of github.com:InseeFrLab/keycloakify
* 'main' of github.com:InseeFrLab/keycloakify:
  Update CI
  Bump version
  #297
  Fix type error in useDownloadTerms
  Refactor Terms.tsx
2023-04-03 20:59:59 +02:00
e00692956c refactor: don't catch top-level promises
the ts compiler will handle it for us
2023-04-03 20:59:34 +02:00
b2c1b41981 Update CI 2023-04-03 20:31:19 +02:00
ffa8440d1b Bump version 2023-04-03 20:19:43 +02:00
32f66b3eaa #297 2023-04-03 20:16:38 +02:00
42b196bd0b Fix type error in useDownloadTerms 2023-04-03 20:10:40 +02:00
68dab45931 Refactor Terms.tsx 2023-04-03 20:10:06 +02:00
af2dbb0389 Merge pull request #296 from keycloakify/introduce-yazl
Introduce-yazl
2023-04-02 23:02:16 +02:00
5abbc7f9a7 style: run prettier 2023-04-02 22:49:12 +02:00
dcfefad17f refactor(jar): introduce yazl for creating jars
* introduce yazl
* remove old zip code
* refactor jar code to make it better testable
* introduce unit test for jar creation
2023-04-02 22:47:42 +02:00
4ece6457fd Bump version 2023-04-02 03:10:38 +02:00
53e38336fb #287 Refacror 2023-04-02 03:10:16 +02:00
0b16df7731 Bump version 2023-04-01 23:00:19 +02:00
900125d92e fmt 2023-04-01 23:00:03 +02:00
6aaaf5a9d3 Merge pull request #289 from keycloakify/some-minor-fixes
Some-minor-fixes
2023-04-01 22:59:31 +02:00
bd2f6d8fee style: move loose test into test suite 2023-04-01 22:52:09 +02:00
baae22657e style: fix formatting 2023-04-01 22:44:13 +02:00
46264c85f4 Add unit test and fix some more use cases 2023-04-01 22:36:54 +02:00
2811eb6024 fix: fix typing 2023-04-01 22:08:00 +02:00
218c1a5a50 refactor: use path.sep to be cross-platform 2023-04-01 22:08:00 +02:00
ab5287a3d4 refactor: type-safe trimIndent 2023-04-01 22:07:59 +02:00
d55c62c073 Bump version 2023-04-01 16:32:10 +02:00
4833c34800 Merge pull request #293 from 0x-Void/add-select-authenticator-page
Add support for the select-authenticator.ftl page
2023-04-01 16:31:45 +02:00
fc70e657f0 Bump version 2023-04-01 14:02:48 +02:00
ee23f629f6 Add themeName option 2023-04-01 14:02:32 +02:00
44402c9571 Bump version 2023-04-01 13:31:56 +02:00
ffefb38161 #40 2023-04-01 13:31:35 +02:00
6d667f653e Bump version 2023-03-31 17:46:01 +02:00
1c75fed727 Merge pull request #290 from keycloakify/fix/unzip
refactor: use yauzl for unzipping
2023-03-31 17:45:13 +02:00
e7837aea88 feat: add select-authenticator page 2023-03-31 17:38:22 +02:00
9c133be779 fix: create cache dir if it doesn't already exist 2023-03-31 09:36:59 -06:00
71eb953fd3 Minor changes 2023-03-31 13:25:48 +02:00
f49ef21fed Merge branch 'main' into fix/unzip 2023-03-31 12:29:10 +02:00
6a6fa04ba0 Merge pull request #287 from keycloakify/vitest-integration
Vitest integration
2023-03-31 12:00:25 +02:00
83b0838c94 Minor fixes to the Vitest setup 2023-03-31 11:56:54 +02:00
4ebc1e671f feat(config): add ability to customize input/output directory 2023-03-30 21:24:11 -06:00
08c7e38587 refactor: use yauzl for unzipping 2023-03-30 22:56:58 +02:00
b863d9feb3 chore: add .devcontainer file 2023-03-30 02:47:54 -06:00
e527f043b0 test: add test for valid jar artifacts 2023-03-30 02:46:44 -06:00
58bb403787 test: refactor existing tests to vitest 2023-03-30 02:46:25 -06:00
e4725c23eb feat: add vitest testing 2023-03-30 02:45:43 -06:00
b0db8caf65 Bump version 2023-03-30 08:01:08 +02:00
3bcc6bdf93 Merge pull request #286 from willwill96/KEYCLOAKIFY-285
fix: pass only strings to trimIndent
2023-03-30 07:38:21 +02:00
eafb75a958 fix: do not swallow errors 2023-03-29 18:48:10 -06:00
31ca0939aa fix: pass only strings to trimIndent 2023-03-29 18:07:43 -06:00
7784fdcd6a Bump version #284 2023-03-29 21:36:27 +02:00
8247eef735 Merge pull request #269 from keycloakify/fix-download-cache
fix(download): fix download cache not behaving as expected
2023-03-29 21:35:44 +02:00
cb6629f301 fix(test): fix test after changes to downloadAndUnzip 2023-03-29 09:59:54 +02:00
3a6fe1b374 fix(cache): fix download caches
* also fix npm config running 4 times in the worst case
* factor out unzip methods
* factor and enhance trimindent
* factor out more utils
* restore windows build, which failed cause generate-i18n-messages did not write any files
2023-03-29 09:54:29 +02:00
0ba2f37004 Merge branch 'main' into fix-download-cache 2023-03-29 09:22:55 +02:00
e052dee753 Bump version 2023-03-28 10:01:15 +02:00
9c2ec32d12 Merge pull request #282 from juffe/add-update-email-page
feat: add update-email.ftl page
2023-03-28 10:00:23 +02:00
1669c38bc9 feat: add update-email.ftl page 2023-03-28 10:26:24 +03:00
c6ce6d1b49 #281: Add location and occupation to user attribute (as a patch until https://github.com/keycloakify/keycloakify/issues/40%23issuecomment-1202102662) 2023-03-28 05:19:35 +02:00
bc242b0aa7 fmt 2023-03-27 21:03:11 +02:00
41b67f6af4 Merge pull request #279 from bralandealmeida/fix/add-url-to-login-reset-password
Fix: add  to login reset password page
2023-03-27 21:01:43 +02:00
bef21e1cb9 fix: add url to login reset password page
fix: add  to login reset password page

fix: add urls to kc context mocks
2023-03-27 15:16:19 -03:00
8c73630f5a Update reamde 2023-03-27 17:47:26 +02:00
724953d5b7 Bump version 2023-03-25 05:11:47 +01:00
a22b231982 Mute max listener warning 2023-03-25 05:11:25 +01:00
910bfe2318 Fix previous release 2023-03-25 05:09:28 +01:00
70a524da46 Bump version 2023-03-25 04:56:28 +01:00
bf6c846fac Use locate theme dir in eject script 2023-03-25 04:56:17 +01:00
b83e4bef3f Bump version 2023-03-25 04:43:40 +01:00
9f7fe0d8f7 Fix error initialization email 2023-03-25 04:42:44 +01:00
741dee57e4 Bump version 2023-03-25 04:28:47 +01:00
fff4dba708 #276: Add build option to select keycloak default assets version 2023-03-25 04:28:10 +01:00
f4f7ab3e49 Make email theme initialization work with theme-only projects 2023-03-25 04:20:10 +01:00
88fe99b1b8 Bump version 2023-03-24 06:25:45 +01:00
92c1486f6a Fix previous release 2023-03-24 06:25:25 +01:00
caea64cef3 Fix build 2023-03-24 06:01:07 +01:00
90783d8ee8 Bump version 2023-03-24 05:51:01 +01:00
be57801e21 Fix email theme path 2023-03-24 05:50:40 +01:00
ff84786b4e Bump version 2023-03-24 05:44:00 +01:00
1e863672cb Find the email theme in src 2023-03-24 05:43:34 +01:00
fb98a9c383 Bump version 2023-03-24 04:14:57 +01:00
05163f22cb Rename InseeFrLab to Keycloakify 2023-03-24 04:14:41 +01:00
160f12d7d3 Rename UserProfileCommon to UserProfileFormFields 2023-03-24 04:12:52 +01:00
49e4e36184 Update README.md 2023-03-22 21:33:41 +01:00
c4f8879cda Bump version 2023-03-22 04:49:30 +01:00
8f54166653 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-22 04:48:08 +01:00
b9f020c447 Merge pull request #272 from willwill96/update-login-types
feat(login context): improve login typings
2023-03-22 04:22:13 +01:00
c357f3eb4d Mention storybook in the changelog 2023-03-22 03:48:21 +01:00
7ebbb0417a feat(login context): improve login typings 2023-03-21 19:47:05 -07:00
6e4b4173b5 Add link to storybook #274 2023-03-22 03:46:30 +01:00
87ebad7efb Bump version 2023-03-22 03:34:59 +01:00
3294aaed3b Pefrorm Keycloak theme download in paralel 2023-03-22 03:34:44 +01:00
0e21f3eab6 Add missing mock 2023-03-22 03:07:33 +01:00
9fcf692cb8 Bump version 2023-03-22 03:03:24 +01:00
da577ea3cc Add stateChecker to password context 2023-03-22 03:02:44 +01:00
6ae1d8938a Fix eslint 2023-03-22 03:00:44 +01:00
3e18a7390c Bump version: Release v7 🚀 2023-03-22 01:55:49 +01:00
5f43f1afc6 Shot a new post build tutorial video 2023-03-22 01:46:05 +01:00
2fc9c03430 Relase candidate 2023-03-21 23:39:40 +01:00
d951a9ba02 Improve post build instructions 2023-03-21 23:21:30 +01:00
93385af675 Release candidate 2023-03-21 19:44:41 +01:00
dd75d0ece7 Account theme specific instructions 2023-03-21 19:44:01 +01:00
dcd37ed916 Relase candidate 2023-03-21 18:32:21 +01:00
2e4d722d7f Return undefined if the context dosen't match the theme 2023-03-21 18:32:00 +01:00
09543400ca Relase candidate 2023-03-21 16:47:58 +01:00
8b101e5043 Fix error in log related to getLogoUrl 2023-03-21 16:47:30 +01:00
b31fff9c2b Release candidate 2023-03-21 15:16:37 +01:00
0c5b100dd9 Update post build instructions 2023-03-21 15:16:23 +01:00
253825a35e fix(download): fix download cache not behaving as expected 2023-03-21 14:48:16 +01:00
8937d19891 Release candidate 2023-03-21 14:21:27 +01:00
0fdd9e75a6 Fix the helper type that enable to extend the KcContext 2023-03-21 14:21:09 +01:00
77da00c2c5 Release candidate 2023-03-21 06:05:30 +01:00
3744080d11 Relase candidate 2023-03-21 05:27:53 +01:00
c9e546a8fd Fix numerous bugs, improve structure 2023-03-21 05:27:31 +01:00
6691992a79 Bump version 2023-03-21 03:02:05 +01:00
1ea0f4c339 Fix usePrepareTemplate 2023-03-21 03:01:49 +01:00
8bfa117be2 Release candidate 2023-03-21 02:36:30 +01:00
b3acecdcea Use children prop for Template 2023-03-21 02:36:13 +01:00
ec479c7e91 Update initialize-email-theme script 2023-03-21 01:50:21 +01:00
fd7760d9ed Update eject-keycloak-page 2023-03-21 01:18:02 +01:00
c9fcec6889 Replace postinstall by prepare 2023-03-20 05:39:34 +01:00
fd901ef2cf Run build only on latest ubuntu and node 16 2023-03-20 05:39:12 +01:00
8afdaa8f0e Relase candidate 2023-03-20 05:29:14 +01:00
254bfccc62 Feature Account theme customization 2023-03-20 05:28:53 +01:00
5b4aeca63c Done implementing the templates and the two first pages 2023-03-20 05:14:25 +01:00
17871daf0c Build account theme 2023-03-20 01:48:03 +01:00
cdd4460968 Rename iitialize-email-theme 2023-03-20 01:30:42 +01:00
fa6a37880b Fix script for account and email 2023-03-20 01:30:16 +01:00
d4e1dabe12 Fix build error 2023-03-20 00:59:53 +01:00
a3fd376b24 Refactor of the i18n download mechanism 2023-03-20 00:58:35 +01:00
aaac1f54e8 Untrack generated messages 2023-03-20 00:03:15 +01:00
41c0329822 Move everything under the login dir 2023-03-19 23:12:45 +01:00
74d48fd7e1 relase candidate 2023-03-19 16:59:57 +01:00
9c3c953129 Fix download i18n script 2023-03-19 16:59:27 +01:00
f5cae18da7 Relase candidate 2023-03-19 16:58:42 +01:00
59d47592d9 Fix other scripts 2023-03-19 16:58:26 +01:00
2b6c991190 Fmt 2023-03-19 16:47:00 +01:00
26020ba8bb Relase candidate 2023-03-19 16:37:48 +01:00
b573bc20b5 Fix 2023-03-19 16:37:35 +01:00
210dbfa265 Bump version 2023-03-19 16:31:25 +01:00
b37cac93ff Fix 2023-03-19 16:31:13 +01:00
eea953efb6 Relase candidate 2023-03-19 16:28:51 +01:00
7ad9d7b291 Remove all unessesary relative import 2023-03-19 16:28:38 +01:00
20937c4f72 Relase candidate 2023-03-19 15:53:13 +01:00
dbbfa07639 Feature new script: Eject-keycloak-page 2023-03-19 15:52:41 +01:00
9e1a4cad5c Update homepage 2023-03-19 15:49:27 +01:00
02bbedcfca Remove no longer used tools 2023-03-19 14:56:30 +01:00
cd70d90914 Refactor completed 2023-03-19 14:48:01 +01:00
819f297de8 Better i18n API 2023-03-19 14:03:06 +01:00
0608adde89 Better naming convention for i18n API 2023-03-19 13:54:39 +01:00
ad7bcf4669 Fix lining script 2023-03-18 19:05:27 +01:00
2eccc86e83 Remove eventEmitter warning 2023-03-18 19:02:17 +01:00
16d18f23a1 Refactor of the main component and i18n 2023-03-18 18:54:33 +01:00
5631ae1b6c Better naming convention 2023-03-18 18:27:50 +01:00
5fb29992f6 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-18 16:58:21 +01:00
910d633ac2 Fix build test app 2023-03-18 16:57:58 +01:00
32f8380e56 Fix build:test 2023-03-18 16:20:21 +01:00
43e4dd6bb6 Fix build for real 2023-03-18 16:17:33 +01:00
4f0b1688db Thank you @justkey007 for tsc-alias 2023-03-18 15:49:45 +01:00
9e75ee09bb fix(deps): update garronej_modules_update 2023-03-18 09:11:03 +00:00
9ae8822e00 Fix build 2023-03-18 06:25:19 +01:00
babffd1fe6 Make the project compile 2023-03-18 06:14:05 +01:00
5615d62032 Scripts dir outside of the src dir 2023-03-18 02:12:12 +01:00
4b89d15c1e progressing 2023-03-17 20:40:29 +01:00
815f510d5f Exclude tsbuild info of the bin dir from the bundle 2023-03-16 23:03:18 +01:00
199ba193be Rename getKcContext dir to kcContext 2023-03-16 23:02:06 +01:00
4ae9bd3f9a Fix repo url in package.json 2023-03-16 22:57:24 +01:00
1c9cf639ea Remove console log 2023-03-16 22:44:44 +01:00
0040464ca1 Move lib up one level 2023-03-16 22:43:09 +01:00
79997efbb6 First commit towars supporting account theme 2023-03-16 22:13:46 +01:00
0e42009798 Fix bin test script 2023-03-16 14:39:40 +01:00
93fdcb8739 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-16 13:33:01 +01:00
aca926e202 Fix link script command 2023-03-16 13:31:56 +01:00
9941027b10 Update package.json 2023-03-15 12:45:58 +01:00
9104de4290 Merge pull request #263 from mkreuzmayr/main
Fix start container script paths for windows
2023-03-15 12:45:43 +01:00
5dc692809c Fix start container script paths for windows 2023-03-15 10:07:01 +01:00
8dc1d1bd21 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-13 10:47:10 +01:00
fe588485a9 Update changelog 2023-03-13 10:47:02 +01:00
19ef1d7025 Bump version 2023-03-13 10:44:46 +01:00
62523a8662 Merge pull request #260 from InseeFrLab/lordvlad/issue257
Run keycloakify behind corporate proxy
2023-03-13 10:44:08 +01:00
6e97665e2e Merge branch 'main' into lordvlad/issue257 2023-03-09 18:33:18 +01:00
4988680353 Update description 2023-03-09 18:30:58 +01:00
c5de5c20c7 Bump version 2023-03-08 23:21:44 +01:00
1a0fee1aa2 Make things cleaner 2023-03-08 23:21:32 +01:00
06a44603cd Release candidate 2023-03-08 22:32:45 +01:00
e48459762e Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-03-08 22:28:46 +01:00
235ebeae97 Bump version 2023-03-08 22:25:10 +01:00
dfe909606e Deprecate useFormValidationSlice 2023-03-08 22:24:51 +01:00
6fd0c7726c Merge branch 'lordvlad/issue257' of github.com:InseeFrLab/keycloakify into lordvlad/issue257
* 'lordvlad/issue257' of github.com:InseeFrLab/keycloakify:
  Only test build on LTS node version
  Update dependency evt to ^2.4.15
  Update README.md
  Bump version
  Avoid passing unessesary realm values in the error.ftl page
2023-03-08 10:25:10 +01:00
819e045811 feat(proxy): respect XDG_CACHE_HOME if set 2023-03-08 10:24:52 +01:00
1ba780598d Relase candidate 2023-03-07 18:18:37 +01:00
aeb0cb3110 Only test build on LTS node version 2023-03-07 18:18:04 +01:00
88923838c5 Bump version 2023-03-07 17:29:30 +01:00
df9f6fd7fd Merge branch 'main' into lordvlad/issue257 2023-03-07 17:16:24 +01:00
98e46d6ac9 Update dependency evt to ^2.4.15 2023-03-07 17:07:49 +01:00
daff614fb4 Update README.md 2023-03-07 17:07:48 +01:00
5ea324c7f2 Bump version 2023-03-07 17:07:48 +01:00
23fedbf94a Avoid passing unessesary realm values in the error.ftl page 2023-03-07 17:07:45 +01:00
593d66d8d6 style: remove unused dependency 2023-03-07 16:45:17 +01:00
851dcd5bf7 Run keycloakify behind corporate proxy
Fixes #257

Use make-fetch-happen for the download step. This lib will use `PROXY`
and `HTTPS_PROXY` and `NO_PROXY` env vars out of the box.

Additionally we'll try and get proxy config from npm. Unfortunately,
the most straightforward options is to call npm config to do this, since
npm  config is not easily extracted as a lib and we don't want to
replicate the resolution mechanisms.
2023-03-07 16:43:12 +01:00
2e919681ae Update dependency evt to ^2.4.15 2023-03-07 10:14:46 +00:00
5da68cd48c Update README.md 2023-03-05 23:37:37 +01:00
27fdaeff46 Bump version 2023-02-27 11:55:39 +01:00
53c0079656 Use extract instead of subtype to ease copy paste into theme repo 2023-02-27 11:55:25 +01:00
93780b77e0 Bump version 2023-02-27 11:32:14 +01:00
b712ed0421 Avoid using tsafe utils to avoid forcing user to install tsafe 2023-02-27 11:32:00 +01:00
ee96f1b345 Bump version 2023-02-27 11:29:23 +01:00
d13464df3d Get rid of the ReactComponent type, classes based component are no longer used 2023-02-27 11:29:05 +01:00
6bde2e4d96 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-02-27 10:39:44 +01:00
0a4953c020 Bump version 2023-02-27 10:39:37 +01:00
96c488880c Abstract away Template logic 2023-02-27 10:39:22 +01:00
7e0adf3f66 Update README.md 2023-02-26 17:32:35 +01:00
09f716440a Bump version 2023-02-26 16:41:47 +01:00
2251c84171 Use the new syntax for importing as type 2023-02-26 16:37:06 +01:00
5cfe78dcd1 Update prettier config 2023-02-26 15:39:03 +01:00
6a48325132 Be more relax on the type safety to avoir headache 2023-02-26 15:37:52 +01:00
294be0a79a see prev commit 2023-02-26 15:36:52 +01:00
c94b264b44 Don't need a dir for a single file 2023-02-26 15:36:35 +01:00
7220c4e3e3 Fix deepAssign 2023-02-26 15:35:57 +01:00
5aadeba2ec fix clsx 2023-02-26 15:35:30 +01:00
0f47a5b6ba Small Template refactor 2023-02-25 20:11:55 +01:00
36f32d28f2 Stop auto updating powerhooks 2023-02-25 19:21:55 +01:00
6d69ccf229 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-02-25 19:21:17 +01:00
37073b42be Avoir introducing breaking changes for CSS only setup 2023-02-25 19:19:46 +01:00
837501c948 Refactor 2023-02-25 18:26:39 +01:00
b300966fa8 Refactor and get rid of unessesary dependencies 2023-02-25 18:11:23 +01:00
730eb06c84 fix(deps): update dependency powerhooks to ^0.26.2 2023-02-14 12:08:41 +00:00
aca8d3f4b7 fix(deps): update dependency powerhooks to ^0.26.1 2023-02-08 16:38:28 +00:00
b5b3af4659 Bump version 2023-02-07 01:32:36 +01:00
6cd231426d Import Blob from node builtins 2023-02-07 01:32:20 +01:00
0c7cd1cd75 Bump version 2023-02-07 01:21:17 +01:00
2425704ead Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-02-07 01:20:33 +01:00
4e22159206 Bump version 2023-02-07 01:20:26 +01:00
52cf1ba02c Fix tsafe related warnings 2023-02-07 01:20:12 +01:00
516e84182f fix(deps): update dependency powerhooks to ^0.26.0 2023-02-05 15:10:42 +00:00
a3a9853e18 bump version 2023-02-05 14:58:53 +01:00
08e26600fd Use keycloakify as bundler by default 2023-02-05 14:58:38 +01:00
7793c2c6ba Update package.json 2023-02-05 14:41:32 +01:00
9e826d16dd Merge pull request #241 from lordvlad/mvn-begone
Mvn begone addendum
2023-02-05 14:41:13 +01:00
80618bbd9c Merge branch 'main' into mvn-begone 2023-02-05 13:36:52 +01:00
38ad47ea75 use hand-crafted promise, pipeline does not resolve properly 2023-02-05 13:32:24 +01:00
45ed359bef fix keycloak theme source path for internal bundler 2023-02-05 13:31:34 +01:00
fcc26c3e7a now that main is a promise, we shuold catch errors 2023-02-05 13:31:03 +01:00
d4ff6b1f40 fix: bundler fix missing directory 2023-02-05 12:59:05 +01:00
557de34eea fix: bundler fix missing change 2023-02-05 12:56:01 +01:00
e034dc4d90 Merge branch 'mvn-begone' of github.com:lordvlad/keycloakify into mvn-begone
* 'mvn-begone' of github.com:lordvlad/keycloakify:
  fix(deps): update garronej_modules_update
  Update README.md
  Rollback via update
  Bump version
  keycloak test script: use env to launch bash
  fix(deps): update dependency powerhooks to ^0.22.0
  Update dependency powerhooks to ^0.21.0
  Relase candidate
  fmt
  Update README.md
  Bump version
  Update src/bin/tools/downloadAndUnzip.ts
  Bump version
  #232
  Bump version
  keycloak test script: use env to launch bash
  fix(deps): update dependency powerhooks to ^0.22.0
  Update dependency powerhooks to ^0.21.0
2023-02-05 12:35:15 +01:00
cfbd1e5e4b fix(bundler): fix type mismatch introduced in last-minute 'fixes' 2023-02-05 12:34:48 +01:00
0df661819f Bump version 2023-02-04 20:51:06 +01:00
1a9f6d10d4 Actually run the top level await 2023-02-04 20:50:53 +01:00
a787215c95 Bump version 2023-02-04 20:39:38 +01:00
64ab400af5 Temporarly restore mvn as default bundler 2023-02-04 20:38:50 +01:00
a463878bf2 Bump version 2023-02-04 20:22:45 +01:00
9f72024c61 Merge pull request #240 from InseeFrLab/mvn-begone
Mvn begone
2023-02-04 19:47:36 +01:00
243fbd4dc9 Set the artifactId name in the build option 2023-02-04 19:36:42 +01:00
4e6a290693 Make new node based bundler the default 2023-02-04 18:02:39 +01:00
ac05d529ca Minor fixes 2023-02-04 17:44:02 +01:00
b38d79004a Merge branch 'main' into mvn-begone 2023-02-03 14:42:05 +01:00
f4a547df11 introduce options to choose a bundle strategy
Pick from 'none', 'keycloakify' or 'mvn', default to 'mvn'. 'none' will
not create a jar, 'keycloakify' will create a jar file using only tools
available to native nodejs, no additional  system library required.
Choosing 'mvn' will behave as before, starting maven in a subprocess.

The bundler can be chosen in `package.json` or via `KEYCLOAKIFY_BUNDLER`
env var.

This commit also adds `KEYCLOAKIFY_GROUP_ID` and
`KEYCLOAKIFY_ARTIFACT_ID` env vars, which will be used to
define group id and artifact id in pom.xml and pom.properties, if given.
2023-02-03 14:28:06 +01:00
2b87c35058 introduce utils for creating a jar archive 2023-02-03 14:06:24 +01:00
b11833e450 fix typo 2023-02-03 13:48:32 +01:00
fa8e119514 fix(deps): update garronej_modules_update 2023-02-01 15:16:03 +00:00
677cb5c330 Update README.md 2023-01-27 15:46:30 +01:00
6e74c79bfe Merge pull request #233 from InseeFrLab/windows_compat
Windows compat
2023-01-27 15:45:31 +01:00
54474f5908 Merge branch 'main' into windows_compat 2023-01-27 15:45:25 +01:00
99cc0f519b Rollback via update 2023-01-27 01:13:16 +01:00
92a01f89ef Bump version 2023-01-27 01:12:46 +01:00
fd83a0c743 keycloak test script: use env to launch bash
This minor change allows users to use the latest version of `bash` on Mac OS.

`/bin/bash` is very old on Mac OS, and many users will have `bash` from MacPorts or Homebrew. This change allows such users to be able to use the newer `bash` installation.
2023-01-27 01:12:46 +01:00
988e46c875 fix(deps): update dependency powerhooks to ^0.22.0 2023-01-27 01:12:46 +01:00
f081c2fc20 Update dependency powerhooks to ^0.21.0 2023-01-27 01:12:46 +01:00
b4b376a1a5 Relase candidate 2023-01-27 01:09:11 +01:00
0db4179d47 fmt 2023-01-27 00:32:41 +01:00
795b7c6234 Update README.md 2023-01-27 00:27:46 +01:00
091b9a57f5 Bump version 2023-01-27 00:22:28 +01:00
564e1422ac Merge pull request #226 from lordvlad/fix-win-build
windows compatibility
2023-01-27 00:20:12 +01:00
8ed4ed3fc4 Update src/bin/tools/downloadAndUnzip.ts 2023-01-26 22:29:51 +01:00
29fe4566a7 style: remove unused variable 2023-01-26 22:20:16 +01:00
ae3bfb28ed refactor: follow suggesion to make methods read a bit more sync 2023-01-26 22:00:31 +01:00
14aab97d8a Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-01-26 10:50:01 +01:00
52d7a47cd7 Bump version 2023-01-26 10:48:57 +01:00
f338dcbeed #232 2023-01-26 10:48:24 +01:00
dcec058a22 Bump version 2023-01-23 02:57:12 +01:00
2bdc6b156b Merge pull request #231 from rome-user/patch-1
keycloak test script: use env to launch bash
2023-01-23 02:56:32 +01:00
84ca9e6b81 keycloak test script: use env to launch bash
This minor change allows users to use the latest version of `bash` on Mac OS.

`/bin/bash` is very old on Mac OS, and many users will have `bash` from MacPorts or Homebrew. This change allows such users to be able to use the newer `bash` installation.
2023-01-22 17:28:27 -08:00
11cb0fd2db Merge pull request #230 from InseeFrLab/renovate_main-garronej_modules_update
Update dependency powerhooks to ^0.22.0 (main)
2023-01-18 00:58:38 +01:00
3f620ffb6f fix(deps): update dependency powerhooks to ^0.22.0 2023-01-16 18:23:50 +00:00
1a0e05d073 rewrite download and unzip to use nodejs native methods 2023-01-16 14:42:20 +01:00
a4d2de23a1 Update dependency powerhooks to ^0.21.0 2023-01-16 02:38:07 +00:00
85cecc9811 fix(build): _properly_ fix bin paths 2023-01-12 22:56:02 +01:00
9899f742a8 fix(build): also rewrite bin paths in dist/package.json 2023-01-12 22:51:48 +01:00
b5484740b7 fix(build): remove dependency on *nix chown 2023-01-12 22:27:22 +01:00
016b15b437 Bump version 2023-01-10 09:05:33 +01:00
6fb936798e https://github.com/InseeFrLab/keycloakify/issues/217#issuecomment-1376849781 2023-01-10 09:05:19 +01:00
a692b87843 Bump version 2023-01-08 15:01:12 +01:00
19663885a4 Merge pull request #222 from InseeFrLab/pr/lordvlad/221
Pr/lordvlad/221
2023-01-08 14:59:22 +01:00
49b87777f9 Fmt 2023-01-08 14:57:18 +01:00
d4523bb1e6 Merge branch 'main' into patch-1 2023-01-08 12:22:14 +01:00
e3200899e2 fix: replace rm system calls with nodejs native functions; closes #219 2023-01-08 12:19:41 +01:00
36c7a1ab9e Bump version 2023-01-08 00:14:01 +01:00
c54fbd5eca Merge pull request #218 from pedrodsRocha1/improve-secure-login-flow
fix - handle username and password login errors the same way
2023-01-08 00:03:16 +01:00
bbe828071e fix - handle username and password login errors the same 2023-01-06 15:41:01 +01:00
23f6c7db00 Update garronej_modules_update 2022-11-29 18:31:14 +00:00
b1ea9e7a71 Update dependency powerhooks to ^0.20.27 2022-11-16 05:21:45 +00:00
fb71d0e272 Update dependency powerhooks to ^0.20.26 2022-11-13 22:11:59 +00:00
fa72a29999 Bump version 2022-11-11 15:49:05 +01:00
af77b31d54 Update default test container to Keycloak 20.0.1 2022-11-11 15:48:32 +01:00
8280dace26 Update garronej_modules_update 2022-10-25 03:18:01 +00:00
ecaf1c7b7c Bump version 2022-10-16 00:51:11 +02:00
8702ec29a8 Drop dependency to @emotion/react 2022-10-16 00:49:49 +02:00
d8206434bc Bump version 2022-10-15 15:39:56 +02:00
c71c2a8710 Fix breaking change of 6.8 2022-10-15 15:39:32 +02:00
464 changed files with 20392 additions and 38189 deletions

151
.all-contributorsrc Normal file
View File

@ -0,0 +1,151 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "angular",
"contributors": [
{
"login": "lordvlad",
"name": "Waldemar Reusch",
"avatar_url": "https://avatars.githubusercontent.com/u/1217769?v=4",
"profile": "https://github.com/lordvlad",
"contributions": [
"code"
]
},
{
"login": "willwill96",
"name": "William Will",
"avatar_url": "https://avatars.githubusercontent.com/u/10997562?v=4",
"profile": "https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/",
"contributions": [
"code"
]
},
{
"login": "Ann2827",
"name": "Bystrova Ann",
"avatar_url": "https://avatars.githubusercontent.com/u/32645809?v=4",
"profile": "https://github.com/Ann2827",
"contributions": [
"code"
]
},
{
"login": "mkreuzmayr",
"name": "Michael Kreuzmayr",
"avatar_url": "https://avatars.githubusercontent.com/u/20108212?v=4",
"profile": "https://github.com/mkreuzmayr",
"contributions": [
"code"
]
},
{
"login": "Mstrodl",
"name": "Mary ",
"avatar_url": "https://avatars.githubusercontent.com/u/6877780?v=4",
"profile": "https://coolmathgames.tech",
"contributions": [
"code"
]
},
{
"login": "Tasyp",
"name": "German Öö",
"avatar_url": "https://avatars.githubusercontent.com/u/6623212?v=4",
"profile": "https://tasyp.xyz/",
"contributions": [
"code"
]
},
{
"login": "revolunet",
"name": "Julien Bouquillon",
"avatar_url": "https://avatars.githubusercontent.com/u/124937?v=4",
"profile": "https://revolunet.com",
"contributions": [
"code"
]
},
{
"login": "aidangilmore",
"name": "Aidan Gilmore",
"avatar_url": "https://avatars.githubusercontent.com/u/32880357?v=4",
"profile": "https://github.com/aidangilmore",
"contributions": [
"code"
]
},
{
"login": "0x-Void",
"name": "Void",
"avatar_url": "https://avatars.githubusercontent.com/u/32745739?v=4",
"profile": "https://github.com/0x-Void",
"contributions": [
"code"
]
},
{
"login": "juffe",
"name": "juffe",
"avatar_url": "https://avatars.githubusercontent.com/u/5393231?v=4",
"profile": "https://github.com/juffe",
"contributions": [
"code"
]
},
{
"login": "lazToum",
"name": "Lazaros Toumanidis",
"avatar_url": "https://avatars.githubusercontent.com/u/4764837?v=4",
"profile": "https://github.com/lazToum",
"contributions": [
"code"
]
},
{
"login": "marcmrf",
"name": "Marc",
"avatar_url": "https://avatars.githubusercontent.com/u/9928519?v=4",
"profile": "https://github.com/marcmrf",
"contributions": [
"code"
]
},
{
"login": "kasir-barati",
"name": "Kasir Barati",
"avatar_url": "https://avatars.githubusercontent.com/u/73785723?v=4",
"profile": "http://kasir-barati.github.io",
"contributions": [
"doc"
]
},
{
"login": "asashay",
"name": "Alex Oliynyk",
"avatar_url": "https://avatars.githubusercontent.com/u/10714670?v=4",
"profile": "https://github.com/asashay",
"contributions": [
"code"
]
},
{
"login": "thosil",
"name": "Thomas Silvestre",
"avatar_url": "https://avatars.githubusercontent.com/u/1140574?v=4",
"profile": "https://www.gravitysoftware.be",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "keycloakify",
"projectOwner": "keycloakify"
}

View File

@ -17,44 +17,48 @@ jobs:
- uses: actions/setup-node@v3
- uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again.
run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run format:check
run: yarn format:check
test:
runs-on: ${{ matrix.os }}
needs: test_lint
strategy:
matrix:
node: [ '14', '15' ,'16', '17' ]
os: [ windows-latest, ubuntu-latest ]
node: [ '18' ]
os: [ ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- name: Tell if project is using npm or yarn
id: step1
uses: garronej/ts-ci@v2.0.2
with:
action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn build
yarn test
- if: steps.step1.outputs.npm_or_yarn == 'npm'
run: |
npm run build
npm test
- run: yarn build
- run: yarn test
- run: yarn test:keycloakify-starter
storybook:
runs-on: ubuntu-latest
if: github.event_name == 'push'
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>"
check_if_version_upgraded:
name: Check if version upgrade
# We run this only if it's a push on the default branch or if it's a PR from a
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is
# defined but GitHub Action don't allow it yet.
# When someone forks the repo and opens a PR we want to enables the tests to be run (the previous jobs)
# but obviously only us should be allowed to release.
# In the following check we make sure that we own the branch this CI workflow is running on before continuing.
# Without this check, trying to release would fail anyway because only us have the correct secret.NPM_TOKEN but
# it's cleaner to stop the execution instead of letting the CI crash.
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
@ -66,7 +70,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.0.2
- uses: garronej/ts-ci@v2.1.0
id: step1
with:
action_name: is_package_json_version_upgraded
@ -74,8 +78,8 @@ jobs:
create_github_release:
runs-on: ubuntu-latest
# We create a release only if the version have been upgraded and we are on the main branch
# or if we are on a branch of the repo that has an PR open on main.
# We create release only if the version in the package.json have been upgraded and this CI is running against the main branch.
# We allow branches with a PR open on main to publish pre-release (x.y.z-rc.u) but not actual releases.
if: |
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
(
@ -109,15 +113,13 @@ jobs:
with:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run build
- run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
- run: yarn build
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
env:
DRY_RUN: "0"
- uses: garronej/ts-ci@v2.1.0
with:
action_name: remove_dark_mode_specific_images_from_readme
- name: Publishing on NPM
run: |
if [ "$(npm show . version)" = "$VERSION" ]; then
@ -136,4 +138,4 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}

17
.gitignore vendored
View File

@ -41,12 +41,21 @@ jspm_packages
.DS_Store
/dist
/dist_test
/keycloakify_starter_test/
/sample_custom_react_project/
/sample_react_project/
/.yarn_home/
.idea
/keycloak_email
/build_keycloak
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
# VS Code devcontainers
.devcontainer
/.yarn
/.yarnrc.yml
/stories/assets/fonts/
/build_storybook/
/storybook-static/

View File

@ -1,9 +1,15 @@
node_modules/
/dist/
/dist_test/
/CHANGELOG.md
/.yarn_home/
/src/test/apps/
/src/tools/types/
/sample_react_project
/build_keycloak/
/build_keycloak/
/.vscode/
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
/dist_test
/sample_react_project/
/sample_custom_react_project/
/keycloakify_starter_test/
/.storybook/static/keycloak-resources/

76
.storybook/Containers.js Normal file
View File

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

35
.storybook/customTheme.js Normal file
View File

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

15
.storybook/main.js Normal file
View File

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

View File

@ -0,0 +1,32 @@
<!-- start favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon_package/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon_package/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon_package/favicon-16x16.png">
<link rel="manifest" href="/favicon_package/site.webmanifest">
<link rel="mask-icon" href="/favicon_package/safari-pinned-tab.svg" color="#5bbad5">
<!-- end favicon -->
<!-- Meta tags generated by metatags.io -->
<!-- Primary Meta Tags -->
<title>Keycloakify Storybook</title>
<meta name="title" content="Keycloakify Storybook">
<meta name="description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://www.keycloakify.dev">
<meta property="og:type" content="website">
<meta property="og:title" content="Keycloakify Storybook">
<meta property="og:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<meta property="og:image" content="https://storybook.keycloakify.dev/preview.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Keycloakify Storybook">
<meta name="twitter:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<meta name="twitter:image" content="https://storybook.keycloakify.dev/preview.png">
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">

6
.storybook/manager.js Normal file
View File

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

137
.storybook/preview.js Normal file
View File

@ -0,0 +1,137 @@
import { darkTheme, lightTheme } from "./customTheme";
import { DocsContainer, CanvasContainer } from "./Containers";
export const parameters = {
"actions": { "argTypesRegex": "^on[A-Z].*" },
"controls": {
"matchers": {
"color": /(background|color)$/i,
"date": /Date$/,
},
},
"backgrounds": { "disable": true },
"darkMode": {
"light": lightTheme,
"dark": darkTheme,
},
"docs": {
"container": DocsContainer
},
"controls": {
"disable": true,
},
"actions": {
"disable": true
},
"viewport": {
"viewports": {
"1440p": {
"name": "1440p",
"styles": {
"width": "2560px",
"height": "1440px",
},
},
"fullHD": {
"name": "Full HD",
"styles": {
"width": "1920px",
"height": "1080px",
},
},
"macBookProBig": {
"name": "MacBook Pro Big",
"styles": {
"width": "1024px",
"height": "640px",
},
},
"macBookProMedium": {
"name": "MacBook Pro Medium",
"styles": {
"width": "1440px",
"height": "900px",
},
},
"macBookProSmall": {
"name": "MacBook Pro Small",
"styles": {
"width": "1680px",
"height": "1050px",
},
},
"pcAgent": {
"name": "PC Agent",
"styles": {
"width": "960px",
"height": "540px",
},
},
"iphone12Pro": {
"name": "Iphone 12 pro",
"styles": {
"width": "390px",
"height": "844px",
},
},
"iphone5se": {
"name": "Iphone 5/SE",
"styles": {
"width": "320px",
"height": "568px",
},
},
"ipadPro": {
"name": "Ipad pro",
"styles": {
"width": "1240px",
"height": "1366px",
},
},
"Galaxy s9+": {
"name": "Galaxy S9+",
"styles": {
"width": "320px",
"height": "658px",
},
}
},
},
"options": {
"storySort": (a, b) =>
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
},
};
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",
];
function getHardCodedWeight(kind) {
for (let i = 0; i < orderedPagesPrefix.length; i++) {
if (kind.toLowerCase().startsWith(orderedPagesPrefix[i].toLowerCase())) {
return orderedPagesPrefix.length - i;
}
}
return 0;
}
return { getHardCodedWeight };
})();

1
.storybook/static/CNAME Normal file
View File

@ -0,0 +1 @@
storybook.keycloakify.dev

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,193 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="447.000000pt" height="447.000000pt" viewBox="0 0 447.000000 447.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,447.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2177 4413 c-3 -2 -17 -6 -33 -9 -85 -15 -204 -109 -286 -225 -95
-133 -229 -437 -263 -597 -4 -18 -10 -30 -13 -28 -4 2 -7 -11 -8 -30 0 -19 -3
-36 -6 -39 -3 -4 -23 1 -44 10 -51 22 -213 73 -289 92 -301 73 -516 74 -670 3
-124 -57 -186 -153 -188 -295 -1 -67 5 -128 18 -180 3 -13 15 -45 26 -70 43
-99 57 -135 53 -135 -3 0 4 -10 16 -22 11 -12 20 -26 20 -31 0 -5 9 -22 21
-38 11 -16 18 -29 14 -29 -3 0 4 -10 15 -22 11 -12 18 -28 15 -36 -2 -7 -1
-11 3 -8 4 2 19 -12 32 -32 13 -20 33 -47 44 -59 11 -13 17 -23 13 -23 -3 0
12 -19 33 -42 22 -24 38 -49 35 -55 -2 -7 -1 -12 4 -10 4 1 32 -25 62 -58 30
-33 88 -94 131 -135 l76 -75 -71 -70 c-112 -110 -174 -181 -262 -300 -106
-144 -142 -202 -203 -325 -9 -19 -21 -38 -27 -42 -5 -4 -7 -8 -3 -8 4 0 -4
-27 -18 -60 -25 -63 -58 -199 -50 -208 3 -2 1 -12 -4 -22 -5 -10 -6 -20 -3
-24 4 -4 8 -23 10 -44 9 -107 77 -201 183 -251 33 -16 56 -32 52 -36 -4 -5 -2
-5 4 -2 6 4 44 1 85 -6 41 -7 102 -12 136 -12 41 1 60 -2 55 -9 -3 -6 2 -5 11
3 11 8 44 14 86 15 38 0 67 4 64 9 -4 6 10 8 79 11 14 0 24 3 22 5 -4 4 33 14
145 37 13 3 33 10 43 15 11 6 26 8 34 5 10 -4 12 -2 8 6 -5 8 -2 9 9 5 10 -3
17 -2 17 3 0 6 8 10 18 10 9 0 36 9 58 19 32 15 45 16 54 8 9 -9 11 -9 8 2 -2
7 2 15 8 18 10 3 23 -34 38 -108 2 -8 6 -21 9 -29 15 -36 70 -206 70 -217 0
-7 4 -13 8 -13 11 0 30 -61 22 -74 -3 -6 -3 -8 2 -4 8 7 86 -135 88 -159 0 -7
4 -13 8 -13 13 0 39 -56 31 -66 -4 -5 -3 -6 2 -2 5 4 22 -12 39 -35 17 -23 49
-59 71 -80 23 -21 40 -42 39 -47 -2 -6 0 -9 5 -8 15 3 63 -22 68 -37 3 -8 10
-12 15 -10 5 3 22 -1 39 -10 17 -9 35 -13 40 -10 6 3 10 2 10 -2 0 -5 12 -8
28 -8 15 1 33 -4 41 -9 11 -8 13 -8 8 0 -4 7 7 13 33 17 21 3 53 12 71 21 17
9 36 13 42 9 5 -3 7 -1 3 5 -3 6 3 13 14 17 11 3 20 11 20 16 0 5 5 9 11 9 5
0 7 -6 3 -13 -4 -7 -3 -9 2 -4 5 5 9 12 9 17 0 4 14 21 30 37 69 67 162 196
179 251 4 12 12 20 17 16 5 -3 8 -2 7 3 -1 4 6 29 17 56 14 33 24 45 35 41 11
-4 12 -2 4 6 -14 14 -6 52 9 43 5 -3 7 -2 4 4 -3 5 8 45 24 88 17 44 33 86 35
94 7 34 56 206 59 209 1 1 15 -3 31 -9 16 -7 34 -13 39 -15 61 -18 144 -51
139 -56 -3 -4 3 -4 14 -1 11 2 23 0 26 -6 4 -5 14 -7 23 -4 9 4 14 2 10 -3 -3
-5 7 -9 21 -8 34 1 63 -6 58 -15 -2 -3 19 -7 47 -9 28 -2 55 -6 61 -10 13 -8
18 -9 79 -10 27 -1 46 -5 44 -9 -3 -5 20 -6 50 -5 44 3 54 1 48 -10 -6 -10 -5
-11 6 0 16 15 33 16 23 0 -5 -7 -3 -8 7 -3 7 5 29 10 49 12 74 5 116 11 135
18 11 4 37 13 57 21 28 10 38 11 41 1 4 -8 6 -7 6 3 1 8 21 28 46 44 43 26 92
86 109 131 28 73 27 217 -3 313 -5 18 -10 33 -9 35 2 23 -118 257 -184 356
-44 67 -124 177 -138 191 -3 3 -34 39 -70 80 -36 41 -97 107 -137 146 l-72 71
25 22 c14 12 30 19 36 15 6 -4 8 -3 4 4 -3 6 24 42 61 80 38 38 86 91 108 117
22 27 46 54 53 61 6 7 12 17 12 23 0 6 3 11 8 11 4 0 22 22 41 50 19 27 37 47
41 45 4 -3 7 3 6 13 0 9 5 16 12 14 8 -1 11 2 7 7 -5 9 36 83 55 101 5 5 101
200 121 245 7 18 26 84 36 125 2 8 4 27 5 42 0 16 5 25 11 21 6 -4 7 -1 2 7
-11 18 -11 62 0 80 5 8 4 11 -2 7 -6 -4 -12 10 -16 36 -6 51 -10 70 -18 77 -3
3 -15 21 -27 41 -42 70 -184 145 -292 155 -205 20 -451 -18 -709 -108 -30 -10
-60 -17 -68 -14 -8 3 -12 2 -9 -3 5 -7 -42 -31 -60 -31 -1 0 -5 15 -9 33 -18
86 -108 342 -156 444 -17 35 -29 66 -29 69 1 4 -2 10 -7 13 -5 3 -24 32 -42
64 -108 190 -245 296 -403 311 -20 2 -39 2 -41 -1z m53 -124 c0 -5 5 -7 10 -4
14 9 52 -4 45 -16 -3 -5 0 -6 8 -4 17 7 69 -33 61 -47 -5 -7 -2 -8 5 -4 13 8
43 -23 35 -36 -3 -4 1 -6 8 -3 7 2 24 -11 38 -31 14 -19 30 -41 35 -47 50 -56
148 -233 139 -249 -4 -6 -3 -9 2 -5 12 7 97 -192 90 -210 -3 -8 -2 -12 3 -9
11 7 45 -120 35 -135 -4 -8 -3 -9 4 -5 7 4 12 3 12 -3 0 -5 3 -16 6 -25 4 -11
-23 -30 -113 -75 -138 -71 -276 -145 -350 -189 -29 -17 -53 -29 -53 -26 0 3
-7 -1 -15 -10 -14 -14 -19 -13 -53 6 -20 11 -62 34 -92 51 -30 16 -59 33 -65
37 -5 4 -86 47 -180 95 -93 48 -173 91 -177 94 -11 10 3 52 15 44 6 -3 7 -1 3
6 -9 14 12 113 22 107 4 -2 8 7 8 20 2 24 42 134 53 144 3 3 7 12 9 20 2 8 17
44 33 80 24 50 32 61 40 50 8 -11 9 -10 4 7 -5 17 10 47 60 123 36 55 71 98
76 94 5 -3 9 -2 8 3 -4 23 2 35 17 29 8 -3 12 -2 9 3 -7 12 60 78 100 99 25
13 70 27 98 31 4 1 7 -4 7 -10z m-1411 -783 c9 -6 12 -5 8 1 -4 6 10 10 34 11
29 1 38 -1 33 -11 -5 -9 -4 -9 7 1 7 6 19 12 27 12 8 0 10 -5 6 -12 -6 -10 -5
-10 7 -1 9 7 26 10 40 8 13 -3 47 -8 74 -11 63 -7 155 -23 175 -31 15 -6 35
-10 71 -12 12 -1 17 -5 13 -13 -4 -7 -3 -8 4 -4 6 4 31 1 54 -5 24 -7 49 -13
55 -15 25 -5 73 -29 73 -37 0 -5 4 -6 9 -3 12 8 41 -4 41 -16 0 -5 -4 -6 -10
-3 -6 3 -7 -1 -4 -9 3 -9 1 -45 -5 -81 -6 -36 -14 -91 -17 -122 -4 -32 -11
-61 -18 -65 -8 -6 -7 -8 2 -8 7 0 11 -4 8 -8 -3 -4 -8 -42 -11 -83 -4 -40 -9
-81 -12 -89 -3 -8 -6 -44 -8 -80 -1 -36 -3 -65 -4 -65 -1 0 -3 -27 -4 -61 -3
-66 0 -62 -92 -139 -33 -27 -62 -53 -63 -58 -2 -4 -8 -5 -13 -1 -5 3 -9 1 -9
-3 0 -5 -41 -44 -91 -88 -50 -43 -98 -85 -106 -93 -13 -14 -23 -6 -91 64 -86
90 -172 188 -186 213 -5 9 -12 18 -16 21 -12 9 -106 154 -139 215 -18 33 -37
66 -43 72 -6 7 -8 20 -4 28 3 9 2 14 -3 11 -19 -12 -102 225 -105 296 0 27 -4
48 -7 48 -16 0 9 108 32 142 22 31 125 81 151 74 10 -2 18 0 18 5 0 5 14 7 30
5 17 -2 30 0 30 4 0 9 42 7 59 -4z m2823 1 c6 -9 8 -9 8 1 0 7 4 10 10 7 5 -3
29 -8 52 -11 113 -13 201 -66 197 -118 0 -5 4 -12 10 -15 9 -6 11 -27 11 -124
0 -16 -4 -26 -9 -23 -5 3 -7 -2 -4 -13 11 -41 -89 -307 -110 -294 -6 3 -7 1
-3 -6 13 -20 -142 -281 -166 -281 -6 0 -8 -3 -5 -7 11 -10 -51 -84 -197 -238
l-86 -89 -47 44 c-26 25 -52 50 -58 55 -7 6 -39 33 -71 61 -33 29 -86 73 -119
100 -86 69 -87 71 -90 112 -5 69 -16 207 -21 242 -4 33 -10 92 -18 170 -4 37
-14 114 -21 165 -2 17 -9 52 -14 80 -5 27 -9 50 -8 51 50 22 109 44 129 48 15
2 36 10 48 16 12 6 28 9 35 6 8 -3 15 -1 17 5 2 5 19 11 39 13 20 2 40 6 45 9
5 3 29 8 54 12 25 4 48 9 52 11 5 3 8 -1 8 -8 0 -9 2 -10 8 -2 9 16 43 21 61
10 10 -7 12 -6 6 4 -6 10 -3 12 12 8 12 -3 25 -3 31 1 20 12 206 11 214 -2z
m-1950 -189 c23 -13 46 -27 49 -31 3 -5 9 -8 13 -7 15 3 64 -25 59 -34 -3 -5
-1 -6 5 -2 14 9 63 -14 55 -27 -3 -6 -2 -7 4 -4 5 3 56 -21 113 -53 57 -33
107 -60 111 -60 4 0 11 -5 15 -12 5 -8 2 -9 -9 -5 -9 3 -16 2 -14 -2 1 -5 -48
-42 -110 -83 -61 -40 -144 -95 -183 -123 -40 -27 -80 -54 -89 -60 -9 -5 -18
-12 -21 -15 -17 -17 -80 -60 -80 -54 0 3 -9 -4 -20 -17 l-20 -24 6 105 c3 58
7 121 9 140 2 19 5 52 6 72 1 20 5 36 8 34 3 -2 7 19 7 47 2 79 15 149 31 176
8 13 10 20 4 17 -6 -4 -11 -2 -11 3 0 6 4 11 10 11 5 0 7 7 4 15 -8 20 -4 19
48 -7z m1114 -66 c3 -26 8 -56 10 -67 10 -50 19 -145 19 -192 0 -28 3 -49 7
-47 4 3 6 -16 5 -41 -1 -25 0 -45 3 -45 3 0 6 -32 7 -70 2 -39 -1 -70 -5 -70
-5 0 -18 9 -30 20 -12 11 -26 20 -31 20 -5 0 -17 10 -25 22 -9 12 -16 19 -16
15 0 -6 -92 56 -135 90 -25 20 -253 169 -278 182 -15 8 -26 15 -25 16 9 7 158
95 184 108 17 9 34 13 38 10 3 -4 6 -1 6 5 0 11 175 102 197 102 7 0 13 4 13
9 0 5 8 11 18 13 20 4 29 -14 38 -80z m-531 -268 c30 -20 55 -41 55 -45 0 -5
6 -8 13 -6 6 1 11 -4 9 -11 -1 -8 2 -11 7 -7 14 8 82 -38 76 -52 -2 -7 2 -10
11 -6 8 3 25 -3 37 -14 12 -11 35 -27 50 -37 16 -9 25 -22 21 -28 -4 -7 -3 -8
4 -4 10 6 242 -148 250 -166 2 -5 9 -8 16 -8 7 0 21 -9 31 -20 17 -19 14 -24
-10 -21 -5 1 -3 -4 5 -11 12 -10 15 -40 16 -138 0 -77 -4 -128 -10 -132 -7 -5
-7 -8 -1 -8 12 0 14 -259 2 -277 -5 -7 -4 -13 1 -13 12 0 7 -79 -5 -90 -7 -7
-244 -174 -287 -203 -10 -6 -24 -16 -30 -22 -12 -10 -34 -26 -211 -146 -96
-65 -108 -71 -131 -60 -14 6 -22 16 -19 21 3 5 0 7 -7 5 -8 -3 -23 4 -35 15
-12 11 -24 20 -27 20 -5 0 -246 166 -271 187 -5 4 -30 22 -55 38 -25 17 -49
33 -55 38 -5 4 -44 31 -85 61 l-75 54 0 337 c1 317 2 338 19 351 11 7 23 11
29 7 6 -3 7 -1 3 5 -4 7 3 17 18 24 14 6 26 15 26 20 0 4 7 8 15 8 8 0 15 5
15 11 0 6 7 8 16 5 8 -3 13 -2 9 3 -3 5 12 20 32 32 21 13 41 27 44 32 4 5 12
6 19 2 8 -5 11 -4 7 2 -9 15 38 44 60 37 12 -4 14 -3 6 3 -9 6 1 19 37 46 27
20 54 37 59 37 5 0 11 3 13 8 5 12 131 95 144 95 7 0 12 4 11 8 -2 8 51 47 66
48 4 1 32 -15 62 -35z m-822 -752 c2 -270 4 -262 -48 -215 -12 10 -41 34 -65
53 -43 33 -83 67 -157 136 l-34 31 57 49 c177 153 219 185 228 176 4 -4 7 -2
6 3 -4 17 0 29 6 23 3 -4 6 -119 7 -256z m1539 245 c7 -7 26 -20 41 -30 15
-10 25 -23 21 -29 -4 -7 -3 -8 4 -4 7 4 12 2 12 -3 0 -6 6 -10 13 -8 6 1 11
-4 9 -11 -1 -8 2 -11 7 -8 5 4 14 -2 20 -11 5 -10 13 -18 18 -18 4 0 7 -3 6
-7 -2 -5 1 -7 5 -5 5 1 37 -22 71 -52 l62 -54 -53 -49 c-29 -27 -66 -59 -81
-71 -16 -12 -34 -27 -41 -33 -43 -40 -129 -105 -133 -101 -2 3 0 14 6 25 7 13
7 23 -2 32 -9 10 -9 14 1 17 6 3 9 9 6 14 -5 9 -8 289 -4 314 1 6 2 14 1 19
-4 22 -8 86 -5 86 1 0 9 -6 16 -13z m-1862 -353 c48 -45 72 -65 109 -93 17
-13 31 -30 31 -37 0 -8 3 -13 8 -13 21 4 32 -3 32 -18 0 -9 3 -14 6 -10 4 3
13 -1 21 -9 8 -8 30 -26 49 -40 18 -15 31 -31 27 -37 -3 -5 -2 -7 4 -4 23 14
43 -19 48 -78 10 -127 16 -186 40 -410 4 -33 10 -79 14 -102 5 -27 4 -46 -3
-54 -8 -9 -8 -10 0 -6 7 4 14 -2 17 -13 3 -11 0 -20 -5 -20 -6 0 -5 -6 2 -15
7 -8 10 -26 8 -40 -3 -13 -1 -22 3 -19 5 3 9 1 9 -5 0 -8 -35 -25 -50 -23 -3
0 -14 -5 -25 -11 -11 -6 -22 -12 -25 -12 -3 -1 -36 -11 -75 -23 -61 -18 -120
-33 -190 -48 -11 -2 -40 -7 -65 -10 -25 -3 -49 -8 -53 -11 -5 -3 -70 -6 -145
-8 -122 -3 -168 0 -263 18 -72 14 -145 78 -155 136 -3 20 -8 44 -11 54 -2 9
-1 17 4 17 4 0 8 17 7 37 -1 50 6 74 20 66 8 -4 8 -3 0 6 -12 14 33 164 47
155 5 -3 6 2 3 10 -3 9 6 36 20 62 14 26 26 53 26 60 0 8 5 14 10 14 6 0 10 5
10 11 0 22 93 168 103 162 6 -3 7 -2 4 4 -10 17 64 118 77 105 4 -3 5 0 2 8
-4 9 12 35 39 65 25 28 73 80 106 117 34 38 65 65 70 62 5 -3 8 -2 7 3 -5 13
26 42 37 35 6 -3 26 -21 45 -38z m2364 -103 c-1 -3 7 -12 18 -19 10 -7 15 -18
12 -24 -4 -6 -2 -8 3 -5 6 4 21 -8 34 -25 13 -17 29 -36 34 -42 30 -33 63 -84
58 -90 -4 -3 -1 -6 5 -6 17 0 85 -108 76 -121 -4 -7 -3 -9 3 -6 13 8 106 -169
97 -185 -4 -6 -3 -8 3 -5 12 7 36 -50 28 -63 -3 -4 1 -10 9 -13 7 -3 17 -23
21 -44 5 -21 10 -45 12 -53 2 -8 4 -24 6 -35 1 -11 6 -23 10 -26 5 -3 8 -36 8
-73 0 -178 -108 -238 -417 -231 -78 2 -150 5 -161 8 -10 3 -34 8 -53 11 -19 3
-48 8 -65 11 -114 22 -337 94 -329 106 3 5 -2 6 -12 2 -12 -5 -15 -2 -11 13 6
20 16 78 22 129 2 17 6 46 9 65 3 19 8 60 11 90 3 30 8 69 11 87 2 17 7 62 10
100 3 37 10 73 15 80 7 7 6 14 -1 18 -6 3 -7 12 -3 18 4 6 6 26 5 45 -2 25 1
32 10 26 10 -6 10 -5 2 7 -16 21 -6 71 18 88 11 8 30 25 41 38 11 12 24 23 29
23 4 0 25 16 45 36 63 60 110 94 118 86 5 -4 5 -2 2 4 -4 7 9 24 29 39 20 15
49 40 66 57 l30 29 71 -72 c40 -39 71 -74 71 -78z m-1901 -294 c-3 -5 -2 -7 4
-4 12 8 83 -39 83 -55 0 -6 3 -8 6 -5 7 7 136 -80 142 -95 2 -4 10 -8 18 -8 8
0 14 -3 14 -8 0 -4 20 -18 45 -32 25 -14 45 -28 45 -32 0 -5 5 -8 10 -8 12 0
124 -72 128 -82 2 -5 11 -8 20 -8 10 0 -36 -32 -101 -70 -65 -39 -124 -67
-130 -64 -7 4 -8 3 -4 -2 5 -5 -41 -33 -110 -66 -80 -38 -120 -52 -125 -45 -3
7 -9 32 -12 57 -3 25 -8 58 -11 74 -3 15 -8 49 -11 75 -3 25 -7 62 -9 81 -2
19 -7 62 -10 94 -3 33 -8 64 -11 68 -3 4 0 8 6 8 6 0 8 5 4 11 -3 6 -8 43 -11
82 -5 65 -4 70 11 58 9 -7 13 -18 9 -24z m1261 -64 c-4 -94 -9 -162 -18 -228
-2 -16 -7 -55 -10 -85 -4 -30 -8 -62 -10 -70 -1 -8 -7 -37 -11 -65 -5 -27 -10
-53 -11 -57 -1 -5 -2 -13 -3 -20 -1 -9 -4 -9 -13 0 -7 7 -20 12 -30 12 -10 0
-18 5 -18 12 0 6 -3 9 -6 5 -4 -3 -54 19 -113 50 -58 31 -123 64 -143 75 -21
10 -38 23 -38 29 0 6 -4 8 -9 5 -5 -3 -19 2 -32 12 -13 10 -37 24 -52 32 -25
12 -26 15 -10 24 10 6 22 11 26 11 4 0 11 5 15 12 4 6 13 13 21 15 8 2 24 12
36 23 13 12 36 27 52 35 15 8 35 21 43 28 8 7 18 12 23 12 4 0 16 8 26 18 42
37 52 43 64 36 6 -4 9 -3 4 1 -9 11 43 46 56 38 6 -3 7 -2 4 4 -3 5 27 33 68
62 41 28 77 56 80 61 12 20 13 8 9 -87z m-524 -403 c8 -5 30 -17 48 -26 17 -9
32 -21 32 -26 0 -5 4 -7 9 -3 5 3 36 -11 68 -30 32 -19 63 -35 69 -35 5 0 21
-10 34 -22 14 -13 25 -21 25 -18 1 8 154 -64 155 -73 0 -5 -4 -5 -10 -2 -6 4
-7 -1 -3 -10 3 -10 0 -21 -8 -26 -11 -6 -11 -9 -1 -9 9 0 10 -5 4 -17 -5 -10
-15 -40 -21 -68 -7 -27 -18 -63 -26 -80 -7 -16 -24 -58 -36 -92 -13 -34 -28
-59 -33 -56 -5 3 -6 1 -3 -4 9 -14 -22 -75 -34 -68 -5 4 -6 -1 -3 -10 4 -9 -5
-33 -20 -55 -14 -22 -26 -42 -26 -46 0 -3 -19 -33 -42 -67 -185 -267 -312
-305 -471 -142 -56 56 -164 205 -150 205 4 0 3 4 -3 8 -14 8 -97 164 -109 202
-4 14 -16 43 -26 65 -25 55 -76 216 -81 251 -2 23 1 29 14 28 10 -1 16 1 14 5
-3 4 19 18 47 31 29 13 55 29 59 35 4 5 8 7 8 3 0 -4 23 6 50 22 28 16 50 26
50 22 0 -4 4 -2 8 3 4 6 25 19 47 30 22 11 47 25 55 31 38 28 169 92 176 86 4
-4 4 -2 1 5 -19 32 42 11 133 -47z"/>
<path d="M2556 3192 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
-9 -8z"/>
<path d="M2455 2831 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
<path d="M2500 2790 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
<path d="M2144 2592 c-70 -35 -108 -103 -100 -179 3 -38 32 -93 62 -118 9 -7
1 -43 -31 -140 -65 -196 -66 -176 5 -173 33 2 60 -1 60 -5 0 -5 4 -6 8 -3 13
8 227 10 239 3 18 -11 23 7 13 39 -6 16 -28 83 -49 149 l-39 120 29 27 c16 16
29 30 29 33 0 22 1 26 8 22 4 -3 8 20 8 51 3 88 -33 145 -108 176 -44 19 -96
18 -134 -2z"/>
<path d="M1113 2105 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
<path d="M859 1903 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
<path d="M1436 1803 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
<path d="M3760 1596 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
13z"/>
<path d="M1616 1691 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
<path d="M2710 1590 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
-4 -4 -4 -10z"/>
<path d="M1090 831 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,37 @@
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: normal;
/*400*/
font-display: swap;
src: url("./worksans-regular-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./worksans-medium-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./worksans-semibold-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: bold;
/*700*/
font-display: swap;
src: url("./worksans-bold-webfont.woff2") format("woff2");
}

BIN
.storybook/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

153
README.md
View File

@ -8,34 +8,30 @@
<a href="https://github.com/garronej/keycloakify/actions">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
</a>
<a href="https://bundlephobia.com/package/keycloakify">
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
</a>
<a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/keycloakify">
</a>
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/keycloakify">
</a>
<a href="https://github.com/InseeFrLab/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
</a>
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
<a href="https://discord.gg/rBzsYtUn">
<img src="https://img.shields.io/discord/1097708346976505977"/>
</a>
<p align="center">
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
</p>
<p align="center"> ---- Project starter / Demo setup ---- </p>
<p align="center">
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
-
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
<a href="https://storybook.keycloakify.dev">Storybook</a>
-
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
</p>
<p align="center"> ---- </p>
</p>
<p align="center">
@ -43,29 +39,134 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
> 🗣 V6 have been released 🎉
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
## Sponsor 👼
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), a French company offering Keycloak as a service.
Their dedicated support helps us continue the development and maintenance of this project.
[Cloud IAM](https://www.cloud-iam.com/) provides the following services:
- Perfectly configured and optimized Keycloak IAM, ready in seconds.
- Custom theme building for your brand using Keycloakify.
<p align="center">
<a href="https://www.cloud-iam.com/">
<img src="https://user-images.githubusercontent.com/6702424/232165752-17134e68-4a55-4d6e-8672-e9132ecac5d5.svg" alt="Cloud IAM Logo" width="200"/>
</a>
<br/>
<i>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>
</p>
Thank you, [Cloud IAM](https://www.cloud-iam.com/), for your support!
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lordvlad"><img src="https://avatars.githubusercontent.com/u/1217769?v=4?s=100" width="100px;" alt="Waldemar Reusch"/><br /><sub><b>Waldemar Reusch</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lordvlad" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/"><img src="https://avatars.githubusercontent.com/u/10997562?v=4?s=100" width="100px;" alt="William Will"/><br /><sub><b>William Will</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=willwill96" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ann2827"><img src="https://avatars.githubusercontent.com/u/32645809?v=4?s=100" width="100px;" alt="Bystrova Ann"/><br /><sub><b>Bystrova Ann</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Ann2827" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkreuzmayr"><img src="https://avatars.githubusercontent.com/u/20108212?v=4?s=100" width="100px;" alt="Michael Kreuzmayr"/><br /><sub><b>Michael Kreuzmayr</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=mkreuzmayr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://coolmathgames.tech"><img src="https://avatars.githubusercontent.com/u/6877780?v=4?s=100" width="100px;" alt="Mary "/><br /><sub><b>Mary </b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Mstrodl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tasyp.xyz/"><img src="https://avatars.githubusercontent.com/u/6623212?v=4?s=100" width="100px;" alt="German Öö"/><br /><sub><b>German Öö</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Tasyp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://revolunet.com"><img src="https://avatars.githubusercontent.com/u/124937?v=4?s=100" width="100px;" alt="Julien Bouquillon"/><br /><sub><b>Julien Bouquillon</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=revolunet" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aidangilmore"><img src="https://avatars.githubusercontent.com/u/32880357?v=4?s=100" width="100px;" alt="Aidan Gilmore"/><br /><sub><b>Aidan Gilmore</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=aidangilmore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0x-Void"><img src="https://avatars.githubusercontent.com/u/32745739?v=4?s=100" width="100px;" alt="Void"/><br /><sub><b>Void</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=0x-Void" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juffe"><img src="https://avatars.githubusercontent.com/u/5393231?v=4?s=100" width="100px;" alt="juffe"/><br /><sub><b>juffe</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=juffe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lazToum"><img src="https://avatars.githubusercontent.com/u/4764837?v=4?s=100" width="100px;" alt="Lazaros Toumanidis"/><br /><sub><b>Lazaros Toumanidis</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lazToum" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcmrf"><img src="https://avatars.githubusercontent.com/u/9928519?v=4?s=100" width="100px;" alt="Marc"/><br /><sub><b>Marc</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=marcmrf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://kasir-barati.github.io"><img src="https://avatars.githubusercontent.com/u/73785723?v=4?s=100" width="100px;" alt="Kasir Barati"/><br /><sub><b>Kasir Barati</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kasir-barati" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asashay"><img src="https://avatars.githubusercontent.com/u/10714670?v=4?s=100" width="100px;" alt="Alex Oliynyk"/><br /><sub><b>Alex Oliynyk</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=asashay" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
# Changelog highlights
## 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/InseeFrLab/keycloakify/issues/191).
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/InseeFrLab/keycloakify/pull/185).
- 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/InseeFrLab/keycloakify/pull/184).
- 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/InseeFrLab/keycloakify/pull/183).
- 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
@ -84,11 +185,11 @@ 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/InseeFrLab/keycloakify/issues/141)
- [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/InseeFrLab/keycloakify/pull/120)
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
## 5.6.4
@ -96,7 +197,7 @@ Fix `login-verify-email.ftl` page. [Before](https://user-images.githubuserconten
## v5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/InseeFrLab/keycloakify/pull/127).
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## v5.3.0
@ -111,7 +212,7 @@ Import of terms and services have changed. [See example](https://github.com/garr
## v4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycloakify/pull/92).
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## v4.8.0
@ -124,7 +225,7 @@ Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycl
## v4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
@ -158,12 +259,12 @@ Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77c
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/InseeFrLab/keycloakify#implement-context-persistence-optional).
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
## v2.5
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2

View File

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

91
package.json Executable file → Normal file
View File

@ -1,30 +1,39 @@
{
"name": "keycloakify",
"version": "6.8.2",
"description": "Keycloak theme generator for Reacts app",
"version": "7.10.0",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
"url": "git://github.com/garronej/keycloakify.git"
"url": "git://github.com/keycloakify/keycloakify.git"
},
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/lib && yarn grant-exec-perms && yarn copy-files dist/",
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
"prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"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\")",
"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",
"pretest": "yarn build:test",
"test": "node dist_test/test/bin && node dist_test/test/lib",
"generate-messages": "node dist/bin/generate-i18n-messages.js",
"link_in_test_app": "node dist/bin/link_in_test_app.js",
"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"
"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",
"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"
},
"bin": {
"keycloakify": "dist/bin/keycloakify/index.js",
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
"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"
},
"lint-staged": {
"*.{ts,tsx,json,md}": [
@ -41,7 +50,8 @@
"files": [
"src/",
"dist/",
"!dist/tsconfig.tsbuildinfo"
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/tsconfig.tsbuildinfo"
],
"keywords": [
"bluehats",
@ -53,42 +63,63 @@
"login",
"register"
],
"homepage": "https://github.com/garronej/keycloakify",
"homepage": "https://www.keycloakify.dev",
"peerDependencies": {
"@emotion/react": "^11.4.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@emotion/react": "^11.10.6",
"@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/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"@types/node": "^18.15.3",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/yauzl": "^2.10.0",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"eslint-plugin-storybook": "^0.6.7",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"powerhooks": "^0.26.7",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "18.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.3",
"tss-react": "^4.8.2",
"typescript": "^4.9.1-beta",
"vitest": "^0.29.8",
"zod-to-json-schema": "^3.20.4"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"@types/yazl": "^2.4.2",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.6",
"memoizee": "^0.4.15",
"evt": "^2.4.18",
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"powerhooks": "^0.20.23",
"react-markdown": "^5.0.3",
"rfc4648": "^1.5.2",
"scripting-tools": "^0.19.13",
"tsafe": "^1.1.3",
"tss-react": "^4.3.4",
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
"zod": "^3.17.10"
}
}

View File

@ -13,11 +13,11 @@
"packageRules": [
{
"packagePatterns": ["*"],
"excludePackagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
"excludePackagePatterns": ["tsafe", "evt"],
"enabled": false
},
{
"packagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
"packagePatterns": ["tsafe", "evt"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "branch",

View File

@ -0,0 +1,122 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getLogger } from "../src/bin/tools/logger";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
//@ts-ignore
const propertiesParser = require("properties-parser");
const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "21.0.1";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl(baseThemeDirPath).forEach(filePath => {
const match = filePath.match(re);
if (match === null) {
return;
}
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
});
}
fs.rmSync(tmpDirPath, { recursive: true, force: true });
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
if (themeType !== "login" && themeType !== "account") {
return;
}
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
""
].join("\n");
languages.forEach(language => {
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.writeFileSync(
filePath,
Buffer.from(
[
generatedFileHeader,
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
].join("\n"),
"utf8"
)
);
logger.log(`${filePath} wrote`);
});
fs.writeFileSync(
pathJoin(baseMessagesDirPath, "index.ts"),
Buffer.from(
[
generatedFileHeader,
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
" switch (currentLanguageTag) {",
...languages.map(language => ` case "${language}": return import("./${language}");`),
' default: return { "default": {} };',
" }",
" })();",
" return messages;",
"}"
].join("\n"),
"utf8"
)
);
});
}
if (require.main === module) {
main();
}

View File

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

143
scripts/link-in-app.ts Normal file
View File

@ -0,0 +1,143 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import * as fs from "fs";
const singletonDependencies: string[] = ["react", "@types/react"];
const rootDirPath = getProjectRoot();
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync(
pathJoin(rootDirPath, "dist", "package.json"),
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
return {
...packageJsonParsed,
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
"exports": !("exports" in packageJsonParsed)
? undefined
: Object.fromEntries(
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
])
)
};
})(),
null,
2
),
"utf8"
)
);
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
return [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
})();
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
...process.env,
"HOME": yarnGlobalDirPath
}
});
};
const testAppPaths = (() => {
const [, , ...testAppNames] = process.argv;
return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(rootDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter((path): path is string => path !== undefined);
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ===");
const total = commonThirdPartyDeps.length;
let current = 0;
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
current++;
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
);
execYarnLink({ "cwd": localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
})
);
export {};

View File

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

26
src/account/Fallback.tsx Normal file
View File

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

131
src/account/Template.tsx Normal file
View File

@ -0,0 +1,131 @@
import { clsx } from "keycloakify/tools/clsx";
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { type TemplateProps } from "keycloakify/account/TemplateProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
import { assert } from "keycloakify/tools/assert";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext;
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
url,
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
"styles": ["css/account.css"],
"htmlClassName": undefined,
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
});
if (!isReady) {
return null;
}
return (
<>
<header className="navbar navbar-default navbar-pf navbar-main header">
<nav className="navbar" role="navigation">
<div className="navbar-header">
<div className="container">
<h1 className="navbar-title">Keycloak</h1>
</div>
</div>
<div className="navbar-collapse navbar-collapse-1">
<div className="container">
<ul className="nav navbar-nav navbar-utility">
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<li>
<div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
</li>
))}
</ul>
</div>
</li>
)}
{referrer?.url && (
<li>
<a href={referrer.url} id="referrer">
{msg("backTo", referrer.name)}
</a>
</li>
)}
<li>
<a href={url.getLogoutUrl()}>{msg("doSignOut")}</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div className="container">
<div className="bs-sidebar col-sm-3">
<ul>
<li className={clsx(active === "account" && "active")}>
<a href={url.accountUrl}>{msg("account")}</a>
</li>
{features.passwordUpdateSupported && (
<li className={clsx(active === "password" && "active")}>
<a href={url.passwordUrl}>{msg("password")}</a>
</li>
)}
<li className={clsx(active === "totp" && "active")}>
<a href={url.totpUrl}>{msg("authenticator")}</a>
</li>
{features.identityFederation && (
<li className={clsx(active === "social" && "active")}>
<a href={url.socialUrl}>{msg("federatedIdentity")}</a>
</li>
)}
<li className={clsx(active === "sessions" && "active")}>
<a href={url.sessionsUrl}>{msg("sessions")}</a>
</li>
<li className={clsx(active === "applications" && "active")}>
<a href={url.applicationsUrl}>{msg("applications")}</a>
</li>
{features.log && (
<li className={clsx(active === "log" && "active")}>
<a href={url.logUrl}>{msg("log")}</a>
</li>
)}
{realm.userManagedAccessAllowed && features.authorization && (
<li className={clsx(active === "authorization" && "active")}>
<a href={url.resourceUrl}>{msg("myResources")}</a>
</li>
)}
</ul>
</div>
<div className="col-sm-9 content-area">
{message !== undefined && (
<div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className="pficon pficon-ok"></span>}
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
<span className="kc-feedback-text">{message.summary}</span>
</div>
)}
{children}
</div>
</div>
</>
);
}

View File

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

View File

@ -1,10 +1,11 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import React, { useEffect, useState, useRef } from "react";
import type baseMessages from "./generated_messages/18.0.1/login/en";
import { useEffect, useState, useRef } from "react";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { Markdown } from "../tools/Markdown";
import type { KcContext } from "../kcContext/KcContext";
import { Markdown } from "keycloakify/tools/Markdown";
export const fallbackLanguageTag = "en";
@ -15,11 +16,11 @@ export type KcContextLike = {
};
};
assert<KcContextBase extends KcContextLike ? true : false>();
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKeyBase = keyof typeof baseMessages | keyof typeof keycloakifyExtraMessages[typeof fallbackLanguageTag];
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type I18n<MessageKey extends string = MessageKeyBase> = {
export type GenericI18n<MessageKey extends string> = {
/**
* e.g: "en", "fr", "zh-CN"
*
@ -67,134 +68,72 @@ export type I18n<MessageKey extends string = MessageKeyBase> = {
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
};
export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params: {
kcContext: KcContextLike;
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
doSkip: boolean;
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
const { kcContext, extraMessages, doSkip } = params;
export type I18n = GenericI18n<MessageKey>;
const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
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 refHasStartedFetching = useRef(false);
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
useEffect(() => {
if (doSkip || refHasStartedFetching.current) {
return;
}
const refHasStartedFetching = useRef(false);
refHasStartedFetching.current = true;
useEffect(() => {
if (refHasStartedFetching.current) {
return;
}
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
refHasStartedFetching.current = true;
const [fallbackMessages, messages] = await Promise.all([
import("./generated_messages/18.0.1/login/en"),
(() => {
switch (currentLanguageTag) {
case "ca":
return import("./generated_messages/18.0.1/login/ca");
case "cs":
return import("./generated_messages/18.0.1/login/cs");
case "da":
return import("./generated_messages/18.0.1/login/da");
case "de":
return import("./generated_messages/18.0.1/login/de");
case "en":
return import("./generated_messages/18.0.1/login/en");
case "es":
return import("./generated_messages/18.0.1/login/es");
case "fi":
return import("./generated_messages/18.0.1/login/fi");
case "fr":
return import("./generated_messages/18.0.1/login/fr");
case "hu":
return import("./generated_messages/18.0.1/login/hu");
case "it":
return import("./generated_messages/18.0.1/login/it");
case "ja":
return import("./generated_messages/18.0.1/login/ja");
case "lt":
return import("./generated_messages/18.0.1/login/lt");
case "lv":
return import("./generated_messages/18.0.1/login/lv");
case "nl":
return import("./generated_messages/18.0.1/login/nl");
case "no":
return import("./generated_messages/18.0.1/login/no");
case "pl":
return import("./generated_messages/18.0.1/login/pl");
case "pt-BR":
return import("./generated_messages/18.0.1/login/pt-BR");
case "ru":
return import("./generated_messages/18.0.1/login/ru");
case "sk":
return import("./generated_messages/18.0.1/login/sk");
case "sv":
return import("./generated_messages/18.0.1/login/sv");
case "tr":
return import("./generated_messages/18.0.1/login/tr");
case "zh-CN":
return import("./generated_messages/18.0.1/login/zh-CN");
default:
return { "default": {} };
}
})()
]).then(modules => modules.map(module => module.default));
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
...messages,
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
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");
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
window.location.href = targetSupportedLocale.url;
assert(false, "never");
},
"labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
})();
}, []);
assert(false, "never");
},
"labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
})();
}, []);
return i18n ?? null;
}
return i18n ?? null;
}
const useI18n_private = __unsafe_useI18n;
export function useI18n<ExtraMessageKey extends string = never>(params: {
kcContext: KcContextLike;
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
return useI18n_private({
...params,
"doSkip": false
});
return { useI18n };
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
messages: Record<MessageKey, string>;
}): Pick<I18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
}): 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 {

View File

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

9
src/account/index.ts Normal file
View File

@ -0,0 +1,9 @@
import Fallback from "keycloakify/account/Fallback";
export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -0,0 +1,87 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
keycloakifyVersion: string;
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
url: {
accountUrl: string;
passwordUrl: string;
totpUrl: string;
socialUrl: string;
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
referrerURI?: string;
getLogoutUrl: () => string;
};
features: {
passwordUpdateSupported: boolean;
identityFederation: boolean;
log: boolean;
authorization: boolean;
};
realm: {
internationalizationEnabled: boolean;
userManagedAccessAllowed: boolean;
};
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
referrer?: {
url: string; // The url of the App
name: string; // Client id
};
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
existsError: (fieldName: string) => boolean;
get: (fieldName: string) => string;
exists: (fieldName: string) => boolean;
};
account: {
email?: string;
firstName: string;
lastName?: string;
username?: string;
};
};
export type Password = Common & {
pageId: "password.ftl";
password: {
passwordSet: boolean;
};
stateChecker: string;
};
export type Account = Common & {
pageId: "account.ftl";
url: {
accountUrl: string;
};
realm: {
registrationEmailAsUsername: boolean;
editUsernameAllowed: boolean;
};
stateChecker: string;
};
}
assert<Equals<KcContext["pageId"], AccountThemePageId>>();

View File

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

View File

@ -0,0 +1,21 @@
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

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,133 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
}
});
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="account">
<div className="row">
<div className="col-md-10">
<h2>{msg("editAccountHtmlTitle")}</h2>
</div>
<div className="col-md-2 subtitle">
<span className="subtitle">
<span className="required">*</span> {msg("requiredFields")}
</span>
</div>
</div>
<form action={url.accountUrl} className="form-horizontal" method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
{!realm.registrationEmailAsUsername && (
<div className={clsx("form-group", messagesPerField.printIfExists("username", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="username" className="control-label">
{msg("username")}
</label>
{realm.editUsernameAllowed && <span className="required">*</span>}
</div>
<div className="col-sm-10 col-md-10">
<input
type="text"
className="form-control"
id="username"
name="username"
disabled={!realm.editUsernameAllowed}
value={account.username ?? ""}
/>
</div>
</div>
)}
<div className={clsx("form-group", messagesPerField.printIfExists("email", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="email" className="control-label">
{msg("email")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
</div>
</div>
<div className={clsx("form-group", messagesPerField.printIfExists("firstName", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="firstName" className="control-label">
{msg("firstName")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
</div>
</div>
<div className={clsx("form-group", messagesPerField.printIfExists("lastName", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="lastName" className="control-label">
{msg("lastName")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
</div>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Save"
>
{msg("doSave")}
</button>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Cancel"
>
{msg("doCancel")}
</button>
</div>
</div>
</div>
</form>
</Template>
);
}

View File

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

View File

@ -0,0 +1,105 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
}
});
const { url, password, account, stateChecker } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
<div className="row">
<div className="col-md-10">
<h2>{msg("changePasswordHtmlTitle")}</h2>
</div>
<div className="col-md-2 subtitle">
<span className="subtitle">{msg("allFieldsRequired")}</span>
</div>
</div>
<form action={url.passwordUrl} className="form-horizontal" method="post">
<input
type="text"
id="username"
name="username"
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none" }}
/>
{password.passwordSet && (
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password" className="control-label">
{msg("password")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
</div>
</div>
)}
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password-new" className="control-label">
{msg("passwordNew")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
</div>
</div>
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password-confirm" className="control-label two-lines">
{msg("passwordConfirm")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
</div>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Save"
>
{msg("doSave")}
</button>
</div>
</div>
</div>
</form>
</Template>
);
}

View File

@ -0,0 +1,48 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { themeTypes } from "./keycloakify/generateFtl";
import * as fs from "fs";
(async () => {
const projectDirPath = process.cwd();
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2),
"projectDirPath": process.cwd()
});
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
return;
}
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"isSilent": false,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeType": themeType,
"themeDirPath": keycloakDirInPublicDir
});
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "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(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`);
})();

View File

@ -1,41 +0,0 @@
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { keycloakThemeEmailDirPath } from "./keycloakify";
import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
if (fs.existsSync(keycloakThemeEmailDirPath)) {
logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
const { keycloakVersion } = await promptKeycloakVersion();
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": keycloakThemeEmailDirPath
});
logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})();
}

View File

@ -1,40 +1,44 @@
#!/usr/bin/env node
import { keycloakThemeBuildingDirPath } from "./keycloakify";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions";
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath, isSilent } = params;
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath } = params;
for (const ext of ["", "-community"]) {
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
isSilent
});
}
await Promise.all(
["", "-community"].map(ext =>
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
})
)
);
}
async function main() {
const buildOptions = readBuildOptions({
"projectDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
"isSilent": buildOptions.isSilent
});
}
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
isSilent
});
})();
main();
}

View File

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

View File

@ -1,87 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
rm_rf(tmpDirPath);
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
crawl(baseThemeDirPath).forEach(filePath => {
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/);
if (match === null) {
return;
}
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
});
}
rm_r(tmpDirPath);
Object.keys(record).forEach(pageType => {
const recordForPageType = record[pageType];
Object.keys(recordForPageType).forEach(language => {
const filePath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_messages", keycloakVersion, pageType, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.writeFileSync(
filePath,
Buffer.from(
[
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
].join("\n"),
"utf8"
)
);
logger.log(`${filePath} wrote`);
});
});
}

43
src/bin/getSrcDirPath.ts Normal file
View File

@ -0,0 +1,43 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
const themeSrcDirBasename = "keycloak-theme";
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
.map(fileRelativePath => {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length !== 2) {
return undefined;
}
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath === undefined) {
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
return { "themeSrcDirPath": srcDirPath };
}
return { "themeSrcDirPath": undefined };
}
return { themeSrcDirPath };
}
export function getEmailThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
return { emailThemeSrcDirPath };
}

View File

@ -0,0 +1,64 @@
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as fs from "fs";
import { getLogger } from "./tools/logger";
import { getEmailThemeSrcDirPath } from "./getSrcDirPath";
export async function main() {
const { isSilent } = readBuildOptions({
"projectDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ isSilent });
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({
"projectDirPath": process.cwd()
});
if (emailThemeSrcDirPath === undefined) {
logger.warn("Couldn't locate your theme source directory");
process.exit(-1);
}
if (fs.existsSync(emailThemeSrcDirPath)) {
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
const { keycloakVersion } = await promptKeycloakVersion();
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": emailThemeSrcDirPath
});
{
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
}
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {
main();
}

View File

@ -1,34 +1,12 @@
import { z } from "zod";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
import { typeGuard } from "tsafe/typeGuard";
import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import * as fs from "fs";
import { join as pathJoin, sep as pathSep } from "path";
import parseArgv from "minimist";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
@ -36,12 +14,20 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeVersion: string;
themeName: string;
extraPages?: string[];
extraLoginPages: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[];
//NOTE: Only for the pom.xml file, questionable utility...
groupId: string;
artifactId: string;
bundler: Bundler;
keycloakVersionDefaultAssets: string;
/** Directory of your built react project. Defaults to {cwd}/build */
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
customUserAttributes: string[];
};
export type Standalone = Common & {
@ -68,15 +54,19 @@ export namespace BuildOptions {
}
}
export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
isSilent: boolean;
}): BuildOptions {
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv);
return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
})();
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
const url = (() => {
const { homepage } = parsedPackageJson;
@ -87,6 +77,16 @@ export function readBuildOptions(params: {
url = new URL(homepage);
}
const CNAME = (() => {
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})();
if (CNAME !== undefined) {
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
}
@ -108,19 +108,38 @@ export function readBuildOptions(params: {
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraPages, extraThemeProperties } = keycloakify ?? {};
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } =
keycloakify ?? {};
const themeName = name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
const themeName =
keycloakify.themeName ??
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
"bundler": (() => {
const { KEYCLOAKIFY_BUNDLER } = process.env;
assert(
typeGuard<Bundler | undefined>(
KEYCLOAKIFY_BUNDLER,
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
);
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
@ -130,10 +149,47 @@ export function readBuildOptions(params: {
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"version": version,
extraPages,
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
isSilent
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
"reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (reactAppBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build");
}
if (pathSep === "\\") {
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
}
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, reactAppBuildDirPath);
}
return reactAppBuildDirPath;
})(),
"keycloakifyBuildDirPath": (() => {
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
if (keycloakifyBuildDirPath === undefined) {
return pathJoin(projectDirPath, "build_keycloak");
}
if (pathSep === "\\") {
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
}
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
}
return keycloakifyBuildDirPath;
})(),
"customUserAttributes": keycloakify.customUserAttributes ?? []
};
})();

View File

@ -2,8 +2,7 @@
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out =
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
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'"); };
@ -14,7 +13,7 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
]>
<#attempt>
@ -36,7 +35,11 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>x<#else>undefined</#if>;
<#else>
return <#if messagesPerField.existsError('${fieldName}')>x<#else>undefined</#if>;
</#if>
<#recover>
</#attempt>
}
@ -51,7 +54,11 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>true<#else>false</#if>;
<#else>
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
</#if>
<#recover>
</#attempt>
}
@ -66,8 +73,14 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if messagesPerField.existsError('${fieldName}')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#if messagesPerField.existsError('username', 'password')>
return 'Invalid username or password.';
</#if>
<#else>
<#if messagesPerField.existsError('${fieldName}')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
</#if>
</#if>
<#recover>
</#attempt>
@ -83,7 +96,11 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.exists('username') || messagesPerField.exists('password')>true<#else>false</#if>;
<#else>
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
</#if>
<#recover>
</#attempt>
}
@ -93,6 +110,17 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
}
};
<#if account??>
out["url"]["getLogoutUrl"] = function () {
<#attempt>
return "${url.getLogoutUrl()}";
<#recover>
</#attempt>
};
</#if>
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["pageId"] = "${pageId}";
return out;
@ -138,9 +166,9 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/InseeFrLab/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/InseeFrLab/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
@ -156,6 +184,10 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
) || (
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
are_same_path(path, ["realm"])
) || (
"error.ftl" == pageId &&
are_same_path(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>

View File

@ -8,37 +8,20 @@ import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [
"login.ftl",
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl"
] as const;
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
export type Common = {
customUserAttributes: string[];
themeVersion: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
@ -50,33 +33,30 @@ export namespace BuildOptionsLike {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type SameDomain = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
export type DifferentDomains = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export type PageId = typeof pageIds[number];
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
const $ = cheerio.load(indexHtmlCode);
@ -146,7 +126,13 @@ export function generateFtlFilesCodeFactory(params: {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace(
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
)
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
@ -179,7 +165,6 @@ export function generateFtlFilesCodeFactory(params: {
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { themeTypes } from "./generateFtl/generateFtl";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
@ -7,6 +8,8 @@ import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
groupId: string;
artifactId?: string;
themeVersion: string;
};
{
@ -16,7 +19,6 @@ export type BuildOptionsLike = {
}
export function generateJavaStackFiles(params: {
version: string;
keycloakThemeBuildingDirPath: string;
doBundlesEmailTemplate: boolean;
buildOptions: BuildOptionsLike;
@ -24,8 +26,7 @@ export function generateJavaStackFiles(params: {
jarFilePath: string;
} {
const {
version,
buildOptions: { groupId, themeName },
buildOptions: { groupId, themeName, themeVersion, artifactId },
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate
} = params;
@ -34,8 +35,6 @@ export function generateJavaStackFiles(params: {
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const artefactId = `${themeName}-keycloak-theme`;
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
@ -43,9 +42,9 @@ export function generateJavaStackFiles(params: {
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artefactId}</artifactId>`,
` <version>${version}</version>`,
` <name>${artefactId}</name>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${themeVersion}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
@ -71,7 +70,7 @@ export function generateJavaStackFiles(params: {
"themes": [
{
"name": themeName,
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])]
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
}
]
},
@ -84,6 +83,6 @@ export function generateJavaStackFiles(params: {
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`)
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
};
}

View File

@ -1,203 +0,0 @@
import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import * as child_process from "child_process";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateKeycloakThemeResources(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
keycloakThemeEmailDirPath: string;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
let doBundlesEmailTemplate: boolean;
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
].join("\n")
);
doBundlesEmailTemplate = false;
break email;
}
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": keycloakThemeEmailDirPath,
"destDirPath": pathJoin(themeDirPath, "..", "email")
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
"buildOptions": buildOptions
});
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
child_process.execSync(`rm -r ${tmpDirPath}`);
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
return { doBundlesEmailTemplate };
}

View File

@ -30,15 +30,18 @@ export function generateStartKeycloakTestingContainer(params: {
buildOptions: { themeName }
} = params;
const keycloakThemePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(/\\/g, "/");
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from(
[
"#!/bin/bash",
"#!/usr/bin/env bash",
"",
`docker rm ${containerName} || true`,
"",
`cd ${keycloakThemeBuildingDirPath}`,
`cd "${keycloakThemeBuildingDirPath.replace(/\\/g, "/")}"`,
"",
"docker run \\",
" -p 8080:8080 \\",
@ -46,14 +49,7 @@ export function generateStartKeycloakTestingContainer(params: {
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v ${pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName
)}:/opt/keycloak/themes/${themeName}:rw \\`,
` -v "${keycloakThemePath}":"/opt/keycloak/themes/${themeName}":rw \\`,
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
""

View File

@ -0,0 +1,47 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import type { ThemeType } from "../generateFtl";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import {
resourcesCommonDirPathRelativeToPublicDir,
resourcesDirPathRelativeToPublicDir,
basenameOfKeycloakDirInPublicDir
} from "../../mockTestingResourcesPath";
import * as crypto from "crypto";
export async function downloadKeycloakStaticResources(
// prettier-ignore
params: {
themeType: ThemeType;
themeDirPath: string;
isSilent: boolean;
keycloakVersion: string;
}
) {
const { themeType, isSilent, themeDirPath, keycloakVersion } = params;
const tmpDirPath = pathJoin(
themeDirPath,
"..",
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir))
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir))
});
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
}

View File

@ -0,0 +1,239 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl";
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
customUserAttributes: string[];
themeVersion: string;
keycloakVersionDefaultAssets: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
emailThemeSrcDirPath: string | undefined;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}): Promise<{ doBundlesEmailTemplate: boolean }> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
let allCssGlobalsToDefine: Record<string, string> = {};
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
for (const themeType of themeTypes) {
const themeDirPath = getThemeDirPath(themeType);
copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0;
if (!isFirstPass && !buildOptions.isStandalone) {
break copy_app_resources_to_theme_path;
}
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
register_css_variables: {
if (!isFirstPass) {
break register_css_variables;
}
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
}
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
}
const generateFtlFilesCode = (() => {
if (generateFtlFilesCode_glob !== undefined) {
return generateFtlFilesCode_glob;
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion
});
return generateFtlFilesCode;
})();
[
...(() => {
switch (themeType) {
case "login":
return loginThemePageIds;
case "account":
return accountThemePageIds;
}
})(),
...((() => {
switch (themeType) {
case "login":
return buildOptions.extraLoginPages;
case "account":
return buildOptions.extraAccountPages;
}
})() ?? [])
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
break copy_keycloak_resources_to_public;
}
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir,
themeType
});
if (themeType !== themeTypes[0]) {
break copy_keycloak_resources_to_public;
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "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(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
}
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
themeDirPath,
themeType
});
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
}
let doBundlesEmailTemplate: boolean;
email: {
if (emailThemeSrcDirPath === undefined) {
doBundlesEmailTemplate = false;
break email;
}
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email")
});
}
return { doBundlesEmailTemplate };
}

View File

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

View File

@ -1,65 +1,83 @@
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
import { generateTheme } from "./generateTheme";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
import jar from "../tools/jar";
import { assert } from "tsafe/assert";
import { Equals } from "tsafe";
import { getEmailThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
const reactProjectDirPath = process.cwd();
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
export function main() {
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
export async function main() {
const projectDirPath = process.cwd();
const buildOptions = readBuildOptions({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
"CNAME": (() => {
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
projectDirPath,
"processArgv": process.argv.slice(2)
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const { doBundlesEmailTemplate } = await generateTheme({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
"emailThemeSrcDirPath": (() => {
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath });
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
return;
}
return emailThemeSrcDirPath;
})(),
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
buildOptions,
//We have to leave it at that otherwise we break our default theme.
//Problem is that we can't guarantee that the the old resources
//will still be available on the newer keycloak version.
"keycloakVersion": "11.0.3"
"keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"];
assert(typeof version === "string");
return version;
})()
});
const { jarFilePath } = generateJavaStackFiles({
"version": buildOptions.version,
keycloakThemeBuildingDirPath,
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
doBundlesEmailTemplate,
buildOptions
});
child_process.execSync("mvn package", {
"cwd": keycloakThemeBuildingDirPath
});
switch (buildOptions.bundler) {
case "none":
logger.log("😱 Skipping bundling step, there will be no jar");
break;
case "keycloakify":
logger.log("🫶 Let keycloakify do its thang");
await jar({
"rootPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"),
"version": buildOptions.themeVersion,
"groupId": buildOptions.groupId,
"artifactId": buildOptions.artifactId,
"targetPath": jarFilePath
});
break;
case "mvn":
logger.log("🫙 Run maven to deliver a jar");
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakifyBuildDirPath });
break;
default:
assert<Equals<typeof buildOptions.bundler, never>>(false);
}
//We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "19.0.1";
// We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "20.0.1";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath,
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
"keycloakVersion": containerKeycloakVersion,
buildOptions
});
@ -67,7 +85,7 @@ export function main() {
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(projectDirPath, jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
"",
//TODO: Restore when we find a good Helm chart for Keycloak.
@ -101,19 +119,33 @@ export function main() {
"",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename))} 👈`,
`👉 $ .${pathSep}${pathRelative(
projectDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`,
"",
"Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags",
"",
"Once your container is up and running: ",
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
'- Create a realm named "myrealm"',
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
"",
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
""
`- Create a realm: Master -> AddRealm -> Name: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeName}`,
` Clients -> account -> Login theme: ${buildOptions.themeName}`,
`- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeName} (option will appear only if you have ran npx initialize-email-theme)`,
`- Create a client Clients -> Create -> Client ID: myclient`,
` Root URL: https://www.keycloak.org/app/`,
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
` Web origins: *`,
` Login Theme: ${buildOptions.themeName}`,
` Save (button at the bottom of the page)`,
``,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,
``,
`Video tutorial: https://youtu.be/WMyGZNHQkjU`,
``
].join("\n")
);
}

View File

@ -0,0 +1,64 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { pathJoin } from "../tools/pathJoin";
export const bundlers = ["mvn", "keycloakify", "none"] as const;
export type Bundler = (typeof bundlers)[number];
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
bundler?: Bundler;
keycloakVersionDefaultAssets?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
customUserAttributes?: string[];
themeName?: string;
};
};
export const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraLoginPages": z.array(z.string()).optional(),
"extraAccountPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"bundler": z.enum(bundlers).optional(),
"keycloakVersionDefaultAssets": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"customUserAttributes": z.array(z.string()).optional(),
"themeName": z.string().optional()
})
.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
export function getParsedPackageJson(params: { projectDirPath: string }) {
const { projectDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;
}
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")));
return parsedPackageJson;
}

View File

@ -1,126 +0,0 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { exclude } from "tsafe/exclude";
import * as fs from "fs";
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
fs.writeFileSync(
pathJoin(keycloakifyDirPath, "dist", "package.json"),
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"));
return {
...packageJsonParsed,
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
"types": packageJsonParsed["types"].replace(/^dist\//, "")
};
})(),
null,
2
),
"utf8"
)
);
const commonThirdPartyDeps = (() => {
const namespaceModuleNames = ["@emotion"];
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"];
return [
...namespaceModuleNames
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...standaloneModuleNames
];
})();
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
execSync(["rm -rf", "mkdir"].map(cmd => `${cmd} ${yarnHomeDirPath}`).join(" && "));
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : [])].join(" ");
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
...process.env,
"HOME": yarnHomeDirPath
}
});
};
const testAppPaths = (() => {
const arg = process.argv[2];
const testAppNames = arg !== undefined ? [arg] : ["keycloakify-starter", "keycloakify-advanced-starter"];
return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(keycloakifyDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter(exclude(undefined));
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ===");
const total = commonThirdPartyDeps.length;
let current = 0;
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
current++;
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
);
execYarnLink({ "cwd": localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": "keycloakify"
})
);

View File

@ -1,5 +1,5 @@
import { pathJoin } from "./tools/pathJoin";
export const mockTestingSubDirOfPublicDirBasename = "keycloak_static";
export const mockTestingResourcesPath = pathJoin(mockTestingSubDirOfPublicDirBasename, "resources");
export const mockTestingResourcesCommonPath = pathJoin(mockTestingResourcesPath, "resources_common");
export const basenameOfKeycloakDirInPublicDir = "keycloak-resources";
export const resourcesDirPathRelativeToPublicDir = pathJoin(basenameOfKeycloakDirInPublicDir, "resources");
export const resourcesCommonDirPathRelativeToPublicDir = pathJoin(resourcesDirPathRelativeToPublicDir, "resources_common");

View File

@ -1,15 +0,0 @@
import parseArgv from "minimist";
export type CliOptions = {
isSilent: boolean;
hasExternalAssets: boolean;
};
export const getCliOptions = (processArgv: string[]): CliOptions => {
const argv = parseArgv(processArgv);
return {
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
};

55
src/bin/tools/crc32.ts Normal file
View File

@ -0,0 +1,55 @@
import { Readable } from "stream";
const crc32tab = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
];
/**
*
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
* @returns a promise for a checksum (uint32)
*/
export function crc32(input: Readable | String | Buffer): Promise<number> {
if (typeof input === "string") {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Buffer) {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Readable) {
return new Promise<number>((resolve, reject) => {
let crc = ~0;
input.setMaxListeners(Infinity);
input.on("end", () => resolve((crc ^ -1) >>> 0));
input.on("error", e => reject(e));
input.on("data", (chunk: Buffer) => {
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
});
});
} else {
throw new Error("Unsupported input " + typeof input);
}
}

61
src/bin/tools/deflate.ts Normal file
View File

@ -0,0 +1,61 @@
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
import { pipeline } from "stream/promises";
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
import { promisify } from "util";
import { crc32 } from "./crc32";
import tee from "./tee";
const deflateRaw = promisify(deflateRawCb);
/**
* A stream transformer that records the number of bytes
* passed in its `size` property.
*/
class ByteCounter extends PassThrough {
size: number = 0;
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
if ("length" in chunk) this.size += chunk.length;
super._transform(chunk, encoding, callback);
}
}
/**
* @param data buffer containing the data to be compressed
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
* of the source data
*/
export async function deflateBuffer(data: Buffer) {
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
return { deflated, crc32: checksum };
}
/**
* @param input a byte stream, containing data to be compressed
* @param sink a method that will accept chunks of compressed data; We don't pass
* a writable here, since we don't want the writablestream to be closed after
* a single file
* @returns a promise, which will resolve with the crc32 checksum and the
* compressed size
*/
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
const deflateWriter = new Writable({
write(chunk, _, callback) {
sink(chunk);
callback();
}
});
// tee the input stream, so we can compress and calc crc32 in parallel
const [rs1, rs2] = tee(input);
const byteCounter = new ByteCounter();
const [_, crc] = await Promise.all([
// pipe input into zip compressor, count the bytes
// returned and pass compressed data to the sink
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
// calc checksum
crc32(rs2)
]);
return { crc32: crc, compressedSize: byteCounter.size };
}

View File

@ -1,81 +1,87 @@
import { basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process";
import * as fs from "fs";
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, stat, writeFile } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe";
import { promisify } from "util";
import { getProjectRoot } from "./getProjectRoot";
import { transformCodebase } from "./transformCodebase";
import { rm, rm_rf } from "./rm";
import * as crypto from "crypto";
import { unzip } from "./unzip";
/** assert url ends with .zip */
export function downloadAndUnzip(params: {
isSilent: boolean;
url: string;
destDirPath: string;
pathOfDirToExtractInArchive?: string;
cacheDirPath: string;
}) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const exec = promisify(execCallback);
const extractDirPath = pathJoin(
cacheDirPath,
`_${crypto.createHash("sha256").update(JSON.stringify({ url, pathOfDirToExtractInArchive })).digest("hex").substring(0, 15)}`
);
function hash(s: string) {
return createHash("sha256").update(s).digest("hex");
}
fs.mkdirSync(cacheDirPath, { "recursive": true });
async function exists(path: string) {
try {
await stat(path);
return true;
} catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return false;
throw error;
}
}
const { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath } = (() => {
const filePath = pathJoin(cacheDirPath, "isSuccessByExtractDirPath.json");
/**
* Get npm configuration as map
*/
async function getNmpConfig(): Promise<Record<string, string>> {
const { stdout } = await exec("npm config get", { encoding: "utf8" });
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2))
.reduce((cfg, [key, value]) => ({ ...cfg, [key]: value }), {});
}
type IsSuccessByExtractDirPath = Record<string, boolean | undefined>;
/**
* Get proxy configuration from npm config files. Note that we don't care about
* proxy config in env vars, because make-fetch-happen will do that for us.
*
* @returns proxy configuration
*/
async function getNpmProxyConfig(): Promise<Pick<FetchOptions, "proxy" | "noProxy">> {
const cfg = await getNmpConfig();
function readIsSuccessByExtractDirPath(): IsSuccessByExtractDirPath {
if (!fs.existsSync(filePath)) {
return {};
}
const proxy = cfg["https-proxy"] ?? cfg["proxy"];
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
return JSON.parse(fs.readFileSync(filePath).toString("utf8"));
}
return { proxy, noProxy };
}
function writeIsSuccessByExtractDirPath(isSuccessByExtractDirPath: IsSuccessByExtractDirPath): void {
fs.writeFileSync(filePath, Buffer.from(JSON.stringify(isSuccessByExtractDirPath, null, 2), "utf8"));
}
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
return { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath };
})();
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
const projectRoot = getProjectRoot();
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
downloadAndUnzip: {
const isSuccessByExtractDirPath = readIsSuccessByExtractDirPath();
if (isSuccessByExtractDirPath[extractDirPath]) {
break downloadAndUnzip;
}
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: false
});
rm_rf(extractDirPath);
fs.mkdirSync(extractDirPath);
const zipFileBasename = pathBasename(url);
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath
});
rm(zipFileBasename, { "cwd": extractDirPath });
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: true
});
if (!(await exists(zipFilePath))) {
const proxyOpts = await getNpmProxyConfig();
const response = await fetch(url, proxyOpts);
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
* does not support node-fetch 3.x. So we stick around with this band-aid until
* octokit upgrades.
*/
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
}
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
transformCodebase({
"srcDirPath": pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive),
destDirPath
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
}

View File

@ -1,10 +1,17 @@
import { getProjectRoot } from "./getProjectRoot";
import { join as pathJoin } from "path";
import * as child_process from "child_process";
import * as fs from "fs";
import { constants } from "fs";
import { chmod, stat } from "fs/promises";
Object.entries<string>(JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["bin"]).forEach(([, scriptPath]) =>
child_process.execSync(`chmod +x ${scriptPath}`, {
"cwd": getProjectRoot()
})
);
(async () => {
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
const promises = Object.values<string>(bin).map(async scriptPath => {
const fullPath = pathJoin(getProjectRoot(), scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);
});
await Promise.all(promises);
})();

87
src/bin/tools/jar.ts Normal file
View File

@ -0,0 +1,87 @@
import { dirname, relative, sep } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
import { ZipFile } from "yazl";
import { mkdir } from "fs/promises";
import trimIndent from "./trimIndent";
export type ZipEntry = { zipPath: string } & ({ fsPath: string } | { buffer: Buffer });
export type ZipEntryGenerator = AsyncGenerator<ZipEntry, void, unknown>;
type CommonJarArgs = {
groupId: string;
artifactId: string;
version: string;
};
export type JarStreamArgs = CommonJarArgs & {
asyncPathGeneratorFn(): ZipEntryGenerator;
};
export type JarArgs = CommonJarArgs & {
targetPath: string;
rootPath: string;
};
export async function jarStream({ groupId, artifactId, version, asyncPathGeneratorFn }: JarStreamArgs) {
const manifestPath = "META-INF/MANIFEST.MF";
const manifestData = Buffer.from(trimIndent`
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Keycloakify
Built-By: unknown
Build-Jdk: 19.0.0
`);
const pomPropsPath = `META-INF/maven/${groupId}/${artifactId}/pom.properties`;
const pomPropsData = Buffer.from(trimIndent`
# Generated by keycloakify
# ${new Date()}
artifactId=${artifactId}
groupId=${groupId}
version=${version}
`);
const zipFile = new ZipFile();
for await (const entry of asyncPathGeneratorFn()) {
if ("buffer" in entry) {
zipFile.addBuffer(entry.buffer, entry.zipPath);
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
zipFile.addFile(entry.fsPath, entry.zipPath);
}
}
zipFile.addBuffer(manifestData, manifestPath);
zipFile.addBuffer(pomPropsData, pomPropsPath);
zipFile.end();
return zipFile;
}
/**
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
* the contents of the pom.properties file which is going to be added to the archive.
*/
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
await mkdir(dirname(targetPath), { recursive: true });
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
for await (const fsPath of walk(rootPath)) {
const zipPath = relative(rootPath, fsPath).split(sep).join("/");
yield { fsPath, zipPath };
}
};
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });
await new Promise<void>(async (resolve, reject) => {
zipFile.outputStream
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
.on("close", () => resolve())
.on("error", e => reject(e));
});
}

View File

@ -0,0 +1,7 @@
import { capitalize } from "tsafe/capitalize";
export function kebabCaseToCamelCase(kebabCaseString: string): string {
const [first, ...rest] = kebabCaseString.split("-");
return [first, ...rest.map(capitalize)].join("");
}

View File

@ -0,0 +1,11 @@
export type PromiseSettledAndPartitioned<T> = [T[], any[]];
export function partitionPromiseSettledResults<T>() {
return [
([successes, failures]: PromiseSettledAndPartitioned<T>, item: PromiseSettledResult<T>) =>
item.status === "rejected"
? ([successes, [item.reason, ...failures]] as PromiseSettledAndPartitioned<T>)
: ([[item.value, ...successes], failures] as PromiseSettledAndPartitioned<T>),
[[], []] as PromiseSettledAndPartitioned<T>
] as const;
}

View File

@ -1,31 +0,0 @@
import { execSync } from "child_process";
function rmInternal(params: { pathToRemove: string; args: string | undefined; cwd: string | undefined }) {
const { pathToRemove, args, cwd } = params;
execSync(`rm ${args ? `-${args} ` : ""}${pathToRemove.replace(/ /g, "\\ ")}`, cwd !== undefined ? { cwd } : undefined);
}
export function rm(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": undefined,
"cwd": options?.cwd
});
}
export function rm_r(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": "r",
"cwd": options?.cwd
});
}
export function rm_rf(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": "rf",
"cwd": options?.cwd
});
}

39
src/bin/tools/tee.ts Normal file
View File

@ -0,0 +1,39 @@
import { PassThrough, Readable } from "stream";
export default function tee(input: Readable) {
const a = new PassThrough();
const b = new PassThrough();
let aFull = false;
let bFull = false;
a.setMaxListeners(Infinity);
a.on("drain", () => {
aFull = false;
if (!aFull && !bFull) input.resume();
});
b.on("drain", () => {
bFull = false;
if (!aFull && !bFull) input.resume();
});
input.on("error", e => {
a.emit("error", e);
b.emit("error", e);
});
input.on("data", chunk => {
aFull = !a.write(chunk);
bFull = !b.write(chunk);
if (aFull || bFull) input.pause();
});
input.on("end", () => {
a.end();
b.end();
});
return [a, b] as const;
}

View File

@ -0,0 +1,46 @@
/**
* Concatenate the string fragments and interpolated values
* to get a single string.
*/
function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
const chunks: string[] = [];
for (let i = 0; i < strings.length; i++) {
let lastStringLineLength = 0;
if (strings[i]) {
chunks.push(strings[i]);
// remember last indent of the string portion
lastStringLineLength = strings[i].split("\n").at(-1)?.length ?? 0;
}
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values
// using the last known string indent
const chunk = String(args[i]).replace(/([\r?\n])/g, "$1" + " ".repeat(lastStringLineLength));
chunks.push(chunk);
}
}
return chunks.join("");
}
/**
* Shift all lines left by the *smallest* indentation level,
* and remove initial newline and all trailing spaces.
*/
export default function trimIndent(strings: TemplateStringsArray, ...args: any[]) {
// Remove initial and final newlines
let string = populateTemplate(strings, ...args)
.replace(/^[\r\n]/, "")
.replace(/\r?\n *$/, "");
const dents =
string
.match(/^([ \t])+/gm)
?.filter(s => /^\s+$/.test(s))
?.map(s => s.length) ?? [];
// No dents? no change required
if (!dents || dents.length == 0) return string;
const minDent = Math.min(...dents);
// The min indentation is 0, no change needed
if (!minDent) return string;
const re = new RegExp(`^${" ".repeat(minDent)}`, "gm");
const dedented = string.replace(re, "");
return dedented;
}

92
src/bin/tools/unzip.ts Normal file
View File

@ -0,0 +1,92 @@
import fsp from "node:fs/promises";
import fs from "fs";
import path from "node:path";
import yauzl from "yauzl";
import stream from "node:stream";
import { promisify } from "node:util";
const pipeline = promisify(stream.pipeline);
async function pathExists(path: string) {
try {
await fsp.stat(path);
return true;
} catch (error) {
if ((error as { code: string }).code === "ENOENT") {
return false;
}
throw error;
}
}
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
// add trailing slash to unzipSubPath and targetFolder
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
unzipSubPath += "/";
}
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
targetFolder += "/";
}
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder, { recursive: true });
}
return new Promise<void>((resolve, reject) => {
yauzl.open(file, { lazyEntries: true }, async (err, zipfile) => {
if (err) {
reject(err);
return;
}
zipfile.readEntry();
zipfile.on("entry", async entry => {
if (unzipSubPath) {
// Skip files outside of the unzipSubPath
if (!entry.fileName.startsWith(unzipSubPath)) {
zipfile.readEntry();
return;
}
// Remove the unzipSubPath from the file name
entry.fileName = entry.fileName.substring(unzipSubPath.length);
}
const target = path.join(targetFolder, entry.fileName);
// Directory file names end with '/'.
// Note that entries for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
if (/[\/\\]$/.test(target)) {
await fsp.mkdir(target, { recursive: true });
zipfile.readEntry();
return;
}
// Skip existing files
if (await pathExists(target)) {
zipfile.readEntry();
return;
}
zipfile.openReadStream(entry, async (err, readStream) => {
if (err) {
reject(err);
return;
}
await pipeline(readStream, fs.createWriteStream(target));
zipfile.readEntry();
});
});
zipfile.once("end", function () {
zipfile.close();
resolve();
});
});
});
}

19
src/bin/tools/walk.ts Normal file
View File

@ -0,0 +1,19 @@
import { readdir } from "fs/promises";
import { resolve, sep } from "path";
/**
* Asynchronously and recursively walk a directory tree, yielding every file and directory
* found. Directory paths will _always_ end with a path separator.
*
* @param root the starting directory
* @returns AsyncGenerator
*/
export default async function* walk(root: string): AsyncGenerator<string, void, unknown> {
for (const entry of await readdir(root, { withFileTypes: true })) {
const absolutePath = resolve(root, entry.name);
if (entry.isDirectory()) {
yield absolutePath.endsWith(sep) ? absolutePath : absolutePath + sep;
yield* walk(absolutePath);
} else yield absolutePath.endsWith(sep) ? absolutePath.substring(0, absolutePath.length - 1) : absolutePath;
}
}

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export { createKeycloakAdapter } from "keycloakify/lib/keycloakJsAdapter";

View File

@ -1,43 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type ErrorProps = KcProps & {
kcContext: KcContextBase.Error;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Error = memo((props: ErrorProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { message, client } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{client !== undefined && client.baseUrl !== undefined && (
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
)}
</div>
}
/>
);
});
export default Error;

View File

@ -1,60 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
export type IdpReviewUserProfileProps = KcProps & {
kcContext: KcContextBase.IdpReviewUserProfile;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const IdpReviewUserProfile = memo((props: IdpReviewUserProfileProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("loginIdpReviewProfileTitle")}
formNode={
<form id="kc-idp-review-profile-form" className={cx(kcProps.kcFormClass)} action={url.loginAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
<div className={cx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(kcProps.kcFormOptionsClass)}>
<div className={cx(kcProps.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormButtonsClass)}>
<input
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
});
export default IdpReviewUserProfile;

View File

@ -1,60 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type InfoProps = KcProps & {
kcContext: KcContextBase.Info;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Info = memo((props: InfoProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined);
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
formNode={
<div id="kc-info-message">
<p className="instruction">
{message.summary}
{requiredActions !== undefined && (
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
)}
</p>
{!skipLink && pageRedirectUri !== undefined ? (
<p>
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
</p>
) : actionUri !== undefined ? (
<p>
<a href={actionUri}>{msg("proceedWithAction")}</a>
</p>
) : (
client.baseUrl !== undefined && (
<p>
<a href={client.baseUrl}>{msg("backToApplication")}</a>
</p>
)
)}
</div>
}
/>
);
});
export default Info;

View File

@ -1,109 +0,0 @@
import React, { lazy, memo, Suspense } from "react";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps";
import { __unsafe_useI18n as useI18n } from "../i18n";
import type { I18n } from "../i18n";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
const Login = lazy(() => import("./Login"));
const Register = lazy(() => import("./Register"));
const RegisterUserProfile = lazy(() => import("./RegisterUserProfile"));
const Info = lazy(() => import("./Info"));
const Error = lazy(() => import("./Error"));
const LoginResetPassword = lazy(() => import("./LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail"));
const Terms = lazy(() => import("./Terms"));
const LoginOtp = lazy(() => import("./LoginOtp"));
const LoginPassword = lazy(() => import("./LoginPassword"));
const LoginUsername = lazy(() => import("./LoginUsername"));
const WebauthnAuthenticate = lazy(() => import("./WebauthnAuthenticate"));
const LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
export type KcAppProps = KcProps & {
kcContext: KcContextBase;
i18n?: I18n;
doFetchDefaultThemeResources?: boolean;
Template: (props: TemplateProps) => JSX.Element | null;
};
const KcApp = memo((props_: KcAppProps) => {
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
"extraMessages": {},
"doSkip": userProvidedI18n !== undefined
});
return userProvidedI18n ?? i18n;
})();
if (i18n === null) {
return null;
}
const commonProps = { i18n, Template, ...kcProps };
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login {...{ kcContext, ...commonProps }} />;
case "register.ftl":
return <Register {...{ kcContext, ...commonProps }} />;
case "register-user-profile.ftl":
return <RegisterUserProfile {...{ kcContext, ...commonProps }} />;
case "info.ftl":
return <Info {...{ kcContext, ...commonProps }} />;
case "error.ftl":
return <Error {...{ kcContext, ...commonProps }} />;
case "login-reset-password.ftl":
return <LoginResetPassword {...{ kcContext, ...commonProps }} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, ...commonProps }} />;
case "terms.ftl":
return <Terms {...{ kcContext, ...commonProps }} />;
case "login-otp.ftl":
return <LoginOtp {...{ kcContext, ...commonProps }} />;
case "login-username.ftl":
return <LoginUsername {...{ kcContext, ...commonProps }} />;
case "login-password.ftl":
return <LoginPassword {...{ kcContext, ...commonProps }} />;
case "webauthn-authenticate.ftl":
return <WebauthnAuthenticate {...{ kcContext, ...commonProps }} />;
case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, ...commonProps }} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, ...commonProps }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...commonProps }} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, ...commonProps }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...commonProps }} />;
case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, ...commonProps }} />;
case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, ...commonProps }} />;
case "update-user-profile.ftl":
return <UpdateUserProfile {...{ kcContext, ...commonProps }} />;
case "idp-review-user-profile.ftl":
return <IdpReviewUserProfile {...{ kcContext, ...commonProps }} />;
}
})()}
</Suspense>
);
});
export default KcApp;

View File

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

View File

@ -1,206 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
export type LoginProps = KcProps & {
kcContext: KcContextBase.Login;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Login = memo((props: LoginProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
//NOTE: Even if we login with email Keycloak expect username and password in
//the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
formElement.submit();
});
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
<div
id="kc-form-wrapper"
className={cx(
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return (
<>
<label htmlFor={autoCompleteHelper} className={cx(kcProps.kcLabelClass)}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={cx(kcProps.kcInputClass)}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off"
})}
/>
</>
);
})()}
</div>
<div className={cx(kcProps.kcFormGroupClass)}>
<label htmlFor="password" className={cx(kcProps.kcLabelClass)}>
{msg("password")}
</label>
<input
tabIndex={2}
id="password"
className={cx(kcProps.kcInputClass)}
name="password"
type="password"
autoComplete="off"
/>
</div>
<div className={cx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options">
{realm.rememberMe && !usernameEditDisabled && (
<div className="checkbox">
<label>
<input
tabIndex={3}
id="rememberMe"
name="rememberMe"
type="checkbox"
{...(login.rememberMe
? {
"checked": true
}
: {})}
/>
{msg("rememberMe")}
</label>
</div>
)}
</div>
<div className={cx(kcProps.kcFormOptionsWrapperClass)}>
{realm.resetPasswordAllowed && (
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
{msg("doForgotPassword")}
</a>
</span>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
type="hidden"
id="id-hidden-input"
name="credentialId"
{...(auth?.selectedCredential !== undefined
? {
"value": auth.selectedCredential
}
: {})}
/>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
)}
</div>
{realm.password && social.providers !== undefined && (
<div id="kc-social-providers" className={cx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
<ul
className={cx(
kcProps.kcFormSocialAccountListClass,
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
)}
>
{social.providers.map(p => (
<li key={p.providerId} className={cx(kcProps.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
<span>{p.displayName}</span>
</a>
</li>
))}
</ul>
</div>
)}
</div>
}
infoNode={
realm.password &&
realm.registrationAllowed &&
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
}
/>
);
});
export default Login;

View File

@ -1,195 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type LoginConfigTotpProps = KcProps & {
kcContext: KcContextBase.LoginConfigTotp;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginConfigTotp = memo((props: LoginConfigTotpProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
};
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("loginTotpTitle")}
formNode={
<>
<ol id="kc-totp-settings">
<li>
<p>{msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.policy.supportedApplications.map(app => (
<li>{app}</li>
))}
</ul>
</li>
{mode && mode == "manual" ? (
<>
<li>
<p>{msg("loginTotpManualStep2")}</p>
<p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
</p>
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("loginTotpScanBarcode")}
</a>
</p>
</li>
<li>
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (
<li>
<p>{msg("loginTotpStep2")}</p>
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" />
<br />
<p>
<a href={totp.manualUrl} id="mode-manual">
{msg("loginTotpUnableToScan")}
</a>
</p>
</li>
)}
<li>
<p>{msg("loginTotpStep3")}</p>
<p>{msg("loginTotpStep3DeviceName")}</p>
</li>
</ol>
<form action={url.loginAction} className={cx(kcProps.kcFormClass)} id="kc-totp-settings-form" method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<div className={cx(kcProps.kcInputWrapperClass)}>
<label htmlFor="totp" className={cx(kcProps.kcLabelClass)}>
{msg("authenticatorCode")}
</label>{" "}
<span className="required">*</span>
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="totp"
name="totp"
autoComplete="off"
className={cx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("totp")}
/>
{messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={cx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("totp")}
</span>
)}
</div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div>
<div className={cx(kcProps.kcFormGroupClass)}>
<div className={cx(kcProps.kcInputWrapperClass)}>
<label htmlFor="userLabel" className={cx(kcProps.kcLabelClass)}>
{msg("loginTotpDeviceName")}
</label>{" "}
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={cx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={cx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
)}
</div>
</div>
{isAppInitiatedAction ? (
<>
<input
type="submit"
className={cx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonLargeClass,
kcProps.kcButtonLargeClass
)}
id="cancelTOTPBtn"
name="cancel-aia"
value="true"
>
${msg("doCancel")}
</button>
</>
) : (
<input
type="submit"
className={cx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
)}
</form>
</>
}
/>
);
});
export default LoginConfigTotp;

View File

@ -1,67 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type LoginIdpLinkConfirmProps = KcProps & {
kcContext: KcContextBase.LoginIdpLinkConfirm;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginIdpLinkConfirm = memo((props: LoginIdpLinkConfirmProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, idpAlias } = kcContext;
const { msg } = i18n;
const { cx } = useCssAndCx();
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("confirmLinkIdpTitle")}
formNode={
<form id="kc-register-form" action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction"
id="updateProfile"
value="updateProfile"
>
{msg("confirmLinkIdpReviewProfile")}
</button>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction"
id="linkAccount"
value="linkAccount"
>
{msg("confirmLinkIdpContinue", idpAlias)}
</button>
</div>
</form>
}
/>
);
});
export default LoginIdpLinkConfirm;

View File

@ -1,43 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LoginIdpLinkEmailProps = KcProps & {
kcContext: KcContextBase.LoginIdpLinkEmail;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginIdpLinkEmail = memo((props: LoginIdpLinkEmailProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={
<>
<p id="instruction1" className="instruction">
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
</p>
<p id="instruction2" className="instruction">
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
</p>
<p id="instruction3" className="instruction">
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
</p>
</>
}
/>
);
});
export default LoginIdpLinkEmail;

View File

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

View File

@ -1,47 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LoginPageExpired = KcProps & {
kcContext: KcContextBase.LoginPageExpired;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginPageExpired = memo((props: LoginPageExpired) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("pageExpiredTitle")}
formNode={
<>
<p id="instruction1" className="instruction">
{msg("pageExpiredMsg1")}
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
{msg("doClickHere")}
</a>{" "}
.<br />
{msg("pageExpiredMsg2")}{" "}
<a id="loginContinueLink" href={url.loginAction}>
{msg("doClickHere")}
</a>{" "}
.
</p>
</>
}
/>
);
});
export default LoginPageExpired;

View File

@ -1,99 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
export type LoginPasswordProps = KcProps & {
kcContext: KcContextBase.LoginPassword;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginPassword = memo((props: LoginPasswordProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { realm, url, login } = kcContext;
const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
formElement.submit();
});
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form">
<div id="kc-form-wrapper">
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<hr />
<label htmlFor="password" className={cx(kcProps.kcLabelClass)}>
{msg("password")}
</label>
<input
tabIndex={2}
id="password"
className={cx(kcProps.kcInputClass)}
name="password"
type="password"
autoFocus={true}
autoComplete="on"
defaultValue={login.password ?? ""}
/>
</div>
<div className={cx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options" />
<div className={cx(kcProps.kcFormOptionsWrapperClass)}>
{realm.resetPasswordAllowed && (
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
{msg("doForgotPassword")}
</a>
</span>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
</div>
</div>
}
/>
);
});
export default LoginPassword;

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