keycloakify 10.0.0-rc.23 → 10.0.0-rc.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (232) hide show
  1. package/PUBLIC_URL.d.ts +1 -1
  2. package/PUBLIC_URL.js +1 -1
  3. package/PUBLIC_URL.js.map +1 -1
  4. package/account/Fallback.js.map +1 -1
  5. package/account/Template.js +3 -3
  6. package/account/Template.js.map +1 -1
  7. package/account/i18n/baseMessages/index.js.map +1 -1
  8. package/account/i18n/i18n.js.map +1 -1
  9. package/account/index.d.ts +2 -2
  10. package/account/index.js +1 -2
  11. package/account/index.js.map +1 -1
  12. package/account/kcContext/KcContext.d.ts +12 -0
  13. package/account/kcContext/KcContext.js.map +1 -1
  14. package/account/kcContext/getKcContextMock.d.ts +24 -0
  15. package/account/kcContext/getKcContextMock.js +28 -0
  16. package/account/kcContext/getKcContextMock.js.map +1 -0
  17. package/account/kcContext/index.d.ts +2 -1
  18. package/account/kcContext/index.js +1 -1
  19. package/account/kcContext/index.js.map +1 -1
  20. package/account/kcContext/kcContextMocks.js +23 -91
  21. package/account/kcContext/kcContextMocks.js.map +1 -1
  22. package/account/pages/Account.js +1 -1
  23. package/account/pages/Account.js.map +1 -1
  24. package/account/pages/Applications.js +2 -2
  25. package/account/pages/Applications.js.map +1 -1
  26. package/account/pages/FederatedIdentity.js +1 -1
  27. package/account/pages/FederatedIdentity.js.map +1 -1
  28. package/account/pages/Log.js +1 -1
  29. package/account/pages/Log.js.map +1 -1
  30. package/account/pages/Password.js +6 -4
  31. package/account/pages/Password.js.map +1 -1
  32. package/account/pages/Sessions.js +1 -1
  33. package/account/pages/Sessions.js.map +1 -1
  34. package/account/pages/Totp.js +1 -1
  35. package/account/pages/Totp.js.map +1 -1
  36. package/bin/main.js +152 -162
  37. package/lib/BASE_URL.js.map +1 -1
  38. package/login/Fallback.js.map +1 -1
  39. package/login/Template.js +7 -7
  40. package/login/Template.js.map +1 -1
  41. package/login/UserProfileFormFields.js +24 -68
  42. package/login/UserProfileFormFields.js.map +1 -1
  43. package/login/i18n/baseMessages/index.js.map +1 -1
  44. package/login/i18n/i18n.js.map +1 -1
  45. package/login/index.d.ts +2 -3
  46. package/login/index.js +1 -2
  47. package/login/index.js.map +1 -1
  48. package/login/kcContext/KcContext.d.ts +23 -12
  49. package/login/kcContext/KcContext.js.map +1 -1
  50. package/login/kcContext/getKcContextMock.d.ts +24 -0
  51. package/login/kcContext/getKcContextMock.js +28 -0
  52. package/login/kcContext/getKcContextMock.js.map +1 -0
  53. package/login/kcContext/index.d.ts +2 -1
  54. package/login/kcContext/index.js +1 -1
  55. package/login/kcContext/index.js.map +1 -1
  56. package/login/kcContext/kcContextMocks.js +32 -99
  57. package/login/kcContext/kcContextMocks.js.map +1 -1
  58. package/login/lib/useDownloadTerms.js +8 -14
  59. package/login/lib/useDownloadTerms.js.map +1 -1
  60. package/login/lib/useGetClassName.js +1 -1
  61. package/login/lib/useGetClassName.js.map +1 -1
  62. package/login/lib/useUserProfileForm.d.ts +9 -1
  63. package/login/lib/useUserProfileForm.js +94 -15
  64. package/login/lib/useUserProfileForm.js.map +1 -1
  65. package/login/pages/Code.js +1 -1
  66. package/login/pages/Code.js.map +1 -1
  67. package/login/pages/DeleteAccountConfirm.js +2 -2
  68. package/login/pages/DeleteAccountConfirm.js.map +1 -1
  69. package/login/pages/DeleteCredential.js +1 -1
  70. package/login/pages/DeleteCredential.js.map +1 -1
  71. package/login/pages/Error.js +1 -1
  72. package/login/pages/Error.js.map +1 -1
  73. package/login/pages/FrontchannelLogout.js +1 -1
  74. package/login/pages/FrontchannelLogout.js.map +1 -1
  75. package/login/pages/IdpReviewUserProfile.js +1 -1
  76. package/login/pages/IdpReviewUserProfile.js.map +1 -1
  77. package/login/pages/Info.js +5 -5
  78. package/login/pages/Info.js.map +1 -1
  79. package/login/pages/Login.js +4 -4
  80. package/login/pages/Login.js.map +1 -1
  81. package/login/pages/LoginConfigTotp.js +2 -2
  82. package/login/pages/LoginConfigTotp.js.map +1 -1
  83. package/login/pages/LoginIdpLinkConfirm.js +1 -1
  84. package/login/pages/LoginIdpLinkConfirm.js.map +1 -1
  85. package/login/pages/LoginIdpLinkEmail.js +1 -1
  86. package/login/pages/LoginIdpLinkEmail.js.map +1 -1
  87. package/login/pages/LoginOauth2DeviceVerifyUserCode.js +1 -1
  88. package/login/pages/LoginOauth2DeviceVerifyUserCode.js.map +1 -1
  89. package/login/pages/LoginOauthGrant.js +2 -2
  90. package/login/pages/LoginOauthGrant.js.map +1 -1
  91. package/login/pages/LoginOtp.js +1 -1
  92. package/login/pages/LoginOtp.js.map +1 -1
  93. package/login/pages/LoginPageExpired.js +1 -1
  94. package/login/pages/LoginPageExpired.js.map +1 -1
  95. package/login/pages/LoginPassword.js +3 -3
  96. package/login/pages/LoginPassword.js.map +1 -1
  97. package/login/pages/LoginRecoveryAuthnCodeConfig.js +10 -10
  98. package/login/pages/LoginRecoveryAuthnCodeConfig.js.map +1 -1
  99. package/login/pages/LoginRecoveryAuthnCodeInput.js +1 -1
  100. package/login/pages/LoginRecoveryAuthnCodeInput.js.map +1 -1
  101. package/login/pages/LoginResetOtp.js +1 -1
  102. package/login/pages/LoginResetOtp.js.map +1 -1
  103. package/login/pages/LoginResetPassword.js +2 -2
  104. package/login/pages/LoginResetPassword.js.map +1 -1
  105. package/login/pages/LoginUpdatePassword.js +3 -3
  106. package/login/pages/LoginUpdatePassword.js.map +1 -1
  107. package/login/pages/LoginUpdateProfile.js +4 -2
  108. package/login/pages/LoginUpdateProfile.js.map +1 -1
  109. package/login/pages/LoginUsername.js +3 -3
  110. package/login/pages/LoginUsername.js.map +1 -1
  111. package/login/pages/LoginVerifyEmail.js +1 -1
  112. package/login/pages/LoginVerifyEmail.js.map +1 -1
  113. package/login/pages/LoginX509Info.js +1 -1
  114. package/login/pages/LoginX509Info.js.map +1 -1
  115. package/login/pages/LogoutConfirm.js +1 -1
  116. package/login/pages/LogoutConfirm.js.map +1 -1
  117. package/login/pages/Register.js +8 -4
  118. package/login/pages/Register.js.map +1 -1
  119. package/login/pages/SamlPostForm.js +1 -1
  120. package/login/pages/SamlPostForm.js.map +1 -1
  121. package/login/pages/SelectAuthenticator.js +2 -2
  122. package/login/pages/SelectAuthenticator.js.map +1 -1
  123. package/login/pages/Terms.js +1 -1
  124. package/login/pages/Terms.js.map +1 -1
  125. package/login/pages/UpdateEmail.js +5 -3
  126. package/login/pages/UpdateEmail.js.map +1 -1
  127. package/login/pages/WebauthnAuthenticate.js +8 -8
  128. package/login/pages/WebauthnAuthenticate.js.map +1 -1
  129. package/login/pages/WebauthnError.js +2 -2
  130. package/login/pages/WebauthnError.js.map +1 -1
  131. package/login/pages/WebauthnRegister.js +5 -5
  132. package/login/pages/WebauthnRegister.js.map +1 -1
  133. package/package.json +26 -38
  134. package/src/PUBLIC_URL.ts +1 -1
  135. package/src/account/Template.tsx +2 -3
  136. package/src/account/index.ts +2 -2
  137. package/src/account/kcContext/KcContext.ts +19 -1
  138. package/src/account/kcContext/getKcContextMock.ts +80 -0
  139. package/src/account/kcContext/index.ts +2 -1
  140. package/src/account/kcContext/kcContextMocks.ts +26 -91
  141. package/src/bin/copy-keycloak-resources-to-public.ts +1 -4
  142. package/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl +4 -6
  143. package/src/bin/tools/getNpmWorkspaceRootDirPath.ts +25 -25
  144. package/src/login/Template.tsx +4 -5
  145. package/src/login/UserProfileFormFields.tsx +28 -80
  146. package/src/login/index.ts +6 -3
  147. package/src/login/kcContext/KcContext.ts +41 -27
  148. package/src/login/kcContext/getKcContextMock.ts +80 -0
  149. package/src/login/kcContext/index.ts +7 -1
  150. package/src/login/kcContext/kcContextMocks.ts +95 -156
  151. package/src/login/lib/useDownloadTerms.ts +10 -24
  152. package/src/login/lib/useGetClassName.ts +1 -1
  153. package/src/login/lib/useUserProfileForm.tsx +117 -13
  154. package/src/login/pages/LoginConfigTotp.tsx +1 -1
  155. package/src/login/pages/LoginRecoveryAuthnCodeConfig.tsx +7 -8
  156. package/src/login/pages/WebauthnAuthenticate.tsx +2 -3
  157. package/src/login/pages/WebauthnRegister.tsx +2 -3
  158. package/src/tools/ExtractAfterStartingWith.ts +4 -0
  159. package/src/tools/StatefulObservable/hooks/useRerenderOnChange.ts +4 -4
  160. package/src/tools/ValueOf.ts +2 -0
  161. package/src/tools/deepAssign.ts +51 -20
  162. package/src/tools/structuredCloneButFunctions.ts +24 -0
  163. package/src/tools/useInsertLinkTags.ts +78 -87
  164. package/src/tools/useInsertScriptTags.ts +69 -78
  165. package/src/tools/useOnFirstMount.ts +18 -0
  166. package/tools/Array.prototype.every.js.map +1 -1
  167. package/tools/ExtractAfterStartingWith.d.ts +1 -0
  168. package/tools/ExtractAfterStartingWith.js +2 -0
  169. package/tools/ExtractAfterStartingWith.js.map +1 -0
  170. package/tools/HTMLElement.prototype.prepend.js.map +1 -1
  171. package/tools/StatefulObservable/StatefulObservable.js.map +1 -1
  172. package/tools/StatefulObservable/hooks/useRerenderOnChange.d.ts +1 -1
  173. package/tools/StatefulObservable/hooks/useRerenderOnChange.js +4 -4
  174. package/tools/StatefulObservable/hooks/useRerenderOnChange.js.map +1 -1
  175. package/tools/ValueOf.d.ts +2 -0
  176. package/tools/ValueOf.js +2 -0
  177. package/tools/ValueOf.js.map +1 -0
  178. package/tools/clsx.js.map +1 -1
  179. package/tools/deepAssign.d.ts +1 -0
  180. package/tools/deepAssign.js +39 -16
  181. package/tools/deepAssign.js.map +1 -1
  182. package/tools/formatNumber.js.map +1 -1
  183. package/tools/structuredCloneButFunctions.d.ts +7 -0
  184. package/tools/structuredCloneButFunctions.js +19 -0
  185. package/tools/structuredCloneButFunctions.js.map +1 -0
  186. package/tools/useInsertLinkTags.d.ts +11 -6
  187. package/tools/useInsertLinkTags.js +53 -53
  188. package/tools/useInsertLinkTags.js.map +1 -1
  189. package/tools/useInsertScriptTags.d.ts +15 -6
  190. package/tools/useInsertScriptTags.js +56 -64
  191. package/tools/useInsertScriptTags.js.map +1 -1
  192. package/tools/useOnFirstMount.d.ts +2 -0
  193. package/tools/useOnFirstMount.js +15 -0
  194. package/tools/useOnFirstMount.js.map +1 -0
  195. package/tools/useSetClassName.js.map +1 -1
  196. package/vite-plugin/index.js +66 -64
  197. package/account/kcContext/createGetKcContext.d.ts +0 -19
  198. package/account/kcContext/createGetKcContext.js +0 -78
  199. package/account/kcContext/createGetKcContext.js.map +0 -1
  200. package/account/kcContext/getKcContext.d.ts +0 -13
  201. package/account/kcContext/getKcContext.js +0 -13
  202. package/account/kcContext/getKcContext.js.map +0 -1
  203. package/account/kcContext/getKcContextFromWindow.d.ts +0 -10
  204. package/account/kcContext/getKcContextFromWindow.js +0 -5
  205. package/account/kcContext/getKcContextFromWindow.js.map +0 -1
  206. package/login/kcContext/createGetKcContext.d.ts +0 -19
  207. package/login/kcContext/createGetKcContext.js +0 -114
  208. package/login/kcContext/createGetKcContext.js.map +0 -1
  209. package/login/kcContext/getKcContext.d.ts +0 -13
  210. package/login/kcContext/getKcContext.js +0 -13
  211. package/login/kcContext/getKcContext.js.map +0 -1
  212. package/login/kcContext/getKcContextFromWindow.d.ts +0 -10
  213. package/login/kcContext/getKcContextFromWindow.js +0 -5
  214. package/login/kcContext/getKcContextFromWindow.js.map +0 -1
  215. package/src/account/kcContext/createGetKcContext.ts +0 -134
  216. package/src/account/kcContext/getKcContext.ts +0 -23
  217. package/src/account/kcContext/getKcContextFromWindow.ts +0 -15
  218. package/src/login/kcContext/createGetKcContext.ts +0 -199
  219. package/src/login/kcContext/getKcContext.ts +0 -23
  220. package/src/login/kcContext/getKcContextFromWindow.ts +0 -15
  221. package/src/tools/AndByDiscriminatingKey.ts +0 -31
  222. package/src/tools/deepClone.ts +0 -19
  223. package/src/tools/memoize.ts +0 -55
  224. package/tools/AndByDiscriminatingKey.d.ts +0 -5
  225. package/tools/AndByDiscriminatingKey.js +0 -2
  226. package/tools/AndByDiscriminatingKey.js.map +0 -1
  227. package/tools/deepClone.d.ts +0 -2
  228. package/tools/deepClone.js +0 -14
  229. package/tools/deepClone.js.map +0 -1
  230. package/tools/memoize.d.ts +0 -7
  231. package/tools/memoize.js +0 -38
  232. package/tools/memoize.js.map +0 -1
@@ -1,14 +1,11 @@
1
- import { useEffect } from "react";
2
- import { memoize } from "keycloakify/tools/memoize";
3
1
  import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n";
4
- import { useConst } from "keycloakify/tools/useConst";
5
- import { useConstCallback } from "keycloakify/tools/useConstCallback";
6
2
  import { assert } from "tsafe/assert";
7
3
  import {
8
4
  createStatefulObservable,
9
5
  useRerenderOnChange
10
6
  } from "keycloakify/tools/StatefulObservable";
11
7
  import { KcContext } from "../kcContext";
8
+ import { useOnFistMount } from "keycloakify/tools/useOnFirstMount";
12
9
 
13
10
  const obsTermsMarkdown = createStatefulObservable<string | undefined>(() => undefined);
14
11
 
@@ -27,29 +24,18 @@ export function useDownloadTerms(params: {
27
24
  kcContext: KcContextLike;
28
25
  downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
29
26
  }) {
30
- const { kcContext } = params;
27
+ const { kcContext, downloadTermMarkdown } = params;
31
28
 
32
- const { downloadTermMarkdownMemoized } = (function useClosure() {
33
- const { downloadTermMarkdown } = params;
34
-
35
- const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
36
-
37
- const downloadTermMarkdownMemoized = useConst(() =>
38
- memoize((currentLanguageTag: string) =>
39
- downloadTermMarkdownConst({ currentLanguageTag })
40
- )
41
- );
42
-
43
- return { downloadTermMarkdownMemoized };
44
- })();
45
-
46
- useEffect(() => {
29
+ useOnFistMount(async () => {
47
30
  if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
48
- downloadTermMarkdownMemoized(
49
- kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
50
- ).then(thermMarkdown => (obsTermsMarkdown.current = thermMarkdown));
31
+ const termsMarkdown = await downloadTermMarkdown({
32
+ currentLanguageTag:
33
+ kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
34
+ });
35
+
36
+ obsTermsMarkdown.current = termsMarkdown;
51
37
  }
52
- }, []);
38
+ });
53
39
  }
54
40
 
55
41
  export function useTermsMarkdown() {
@@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
3
3
 
4
4
  export const { useGetClassName } = createUseClassName<ClassKey>({
5
5
  defaultClasses: {
6
+ kcHtmlClass: "login-pf",
6
7
  kcBodyClass: undefined,
7
8
  kcHeaderWrapperClass: undefined,
8
9
  kcLocaleWrapperClass: undefined,
@@ -54,7 +55,6 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
54
55
  kcLogoLink: "http://www.keycloak.org",
55
56
  kcContainerClass: "container-fluid",
56
57
  kcSelectAuthListItemTitle: "select-auth-box-paragraph",
57
- kcHtmlClass: "login-pf",
58
58
  kcLoginOTPListItemTitleClass: "pf-c-tile__title",
59
59
  "kcLogoIdP-openshift-v4": "pf-icon pf-icon-openshift",
60
60
  kcWebAuthnUnknownIcon: "pficon pficon-key unknown-transport-class",
@@ -8,7 +8,8 @@ import { emailRegexp } from "keycloakify/tools/emailRegExp";
8
8
  import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/KcContext";
9
9
  import { assert, type Equals } from "tsafe/assert";
10
10
  import { formatNumber } from "keycloakify/tools/formatNumber";
11
- import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
11
+ import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
12
+ import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
12
13
  import type { I18n } from "../i18n";
13
14
 
14
15
  export type FormFieldError = {
@@ -67,7 +68,7 @@ export type FormAction =
67
68
  export type KcContextLike = {
68
69
  messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
69
70
  profile: {
70
- attributes: Attribute[];
71
+ attributesByName: Record<string, Attribute>;
71
72
  html5DataAnnotations?: Record<string, string>;
72
73
  };
73
74
  passwordRequired?: boolean;
@@ -102,12 +103,11 @@ namespace internal {
102
103
  };
103
104
  }
104
105
 
105
- const { useInsertScriptTags } = createUseInsertScriptTags();
106
-
107
106
  export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
108
107
  const { kcContext, i18n, doMakeUserConfirmPassword } = params;
109
108
 
110
109
  const { insertScriptTags } = useInsertScriptTags({
110
+ componentOrHookName: "useUserProfileForm",
111
111
  scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
112
112
  .filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
113
113
  .map(key => ({
@@ -136,7 +136,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
136
136
 
137
137
  const attributes = (() => {
138
138
  retrocompat_patch: {
139
- if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
139
+ if (
140
+ "profile" in kcContext &&
141
+ "attributesByName" in kcContext.profile &&
142
+ Object.keys(kcContext.profile.attributesByName).length !== 0
143
+ ) {
140
144
  break retrocompat_patch;
141
145
  }
142
146
 
@@ -216,7 +220,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
216
220
  assert(false, "Unable to mock user profile from the current kcContext");
217
221
  }
218
222
 
219
- return kcContext.profile.attributes.map(attribute_pre_group_patch => {
223
+ return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => {
220
224
  if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
221
225
  const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
222
226
  attribute_pre_group_patch as Attribute & {
@@ -242,7 +246,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
242
246
  })();
243
247
 
244
248
  for (const attribute of attributes) {
245
- syntheticAttributes.push(attribute);
249
+ syntheticAttributes.push(structuredCloneButFunctions(attribute));
246
250
 
247
251
  add_password_and_password_confirm: {
248
252
  if (!kcContext.passwordRequired) {
@@ -284,6 +288,21 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
284
288
  }
285
289
  }
286
290
 
291
+ // NOTE: Consistency patch
292
+ syntheticAttributes.forEach(attribute => {
293
+ if (getIsMultivaluedSingleField({ attribute })) {
294
+ attribute.multivalued = true;
295
+ }
296
+
297
+ if (attribute.multivalued) {
298
+ attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
299
+ delete attribute.value;
300
+ } else {
301
+ attribute.value ??= attribute.values?.[0];
302
+ delete attribute.values;
303
+ }
304
+ });
305
+
287
306
  return syntheticAttributes;
288
307
  })();
289
308
 
@@ -299,10 +318,10 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
299
318
  break handle_multi_valued_attribute;
300
319
  }
301
320
 
302
- const values = attribute.values ?? [""];
321
+ const values = attribute.values?.length ? attribute.values : [""];
303
322
 
304
323
  apply_validator_min_range: {
305
- if (attribute.annotations.inputType?.startsWith("multiselect")) {
324
+ if (getIsMultivaluedSingleField({ attribute })) {
306
325
  break apply_validator_min_range;
307
326
  }
308
327
 
@@ -349,7 +368,8 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
349
368
  attributeName: attribute.name,
350
369
  formFieldStates: initialFormFieldState
351
370
  }),
352
- hasLostFocusAtLeastOnce: valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
371
+ hasLostFocusAtLeastOnce:
372
+ valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
353
373
  valueOrValues: valueOrValues
354
374
  }))
355
375
  };
@@ -543,7 +563,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
543
563
 
544
564
  server_side_error: {
545
565
  if (attribute.multivalued) {
546
- const defaultValues = attribute.values ?? [""];
566
+ const defaultValues = attribute.values?.length ? attribute.values : [""];
547
567
 
548
568
  assert(valueOrValues instanceof Array);
549
569
 
@@ -595,7 +615,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
595
615
  break handle_multi_valued_multi_fields;
596
616
  }
597
617
 
598
- if (attribute.annotations.inputType?.startsWith("multiselect")) {
618
+ if (getIsMultivaluedSingleField({ attribute })) {
599
619
  break handle_multi_valued_multi_fields;
600
620
  }
601
621
 
@@ -674,7 +694,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
674
694
  break handle_multi_valued_single_field;
675
695
  }
676
696
 
677
- if (!attribute.annotations.inputType?.startsWith("multiselect")) {
697
+ if (!getIsMultivaluedSingleField({ attribute })) {
678
698
  break handle_multi_valued_single_field;
679
699
  }
680
700
 
@@ -913,6 +933,10 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
913
933
  return valueOrValues;
914
934
  })();
915
935
 
936
+ if (usernameValue === "") {
937
+ break check_password_policy_x;
938
+ }
939
+
916
940
  if (value !== usernameValue) {
917
941
  break check_password_policy_x;
918
942
  }
@@ -950,6 +974,10 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
950
974
  {
951
975
  const emailValue = emailFormFieldState.valueOrValues;
952
976
 
977
+ if (emailValue === "") {
978
+ break check_password_policy_x;
979
+ }
980
+
953
981
  if (value !== emailValue) {
954
982
  break check_password_policy_x;
955
983
  }
@@ -1239,3 +1267,79 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
1239
1267
 
1240
1268
  return { getErrors };
1241
1269
  }
1270
+
1271
+ function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
1272
+ const { attribute } = params;
1273
+
1274
+ return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
1275
+ }
1276
+
1277
+ export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
1278
+ const { attribute, values, fieldIndex } = params;
1279
+
1280
+ const hasRemove = (() => {
1281
+ if (values.length === 1) {
1282
+ return false;
1283
+ }
1284
+
1285
+ const minCount = (() => {
1286
+ const { multivalued } = attribute.validators;
1287
+
1288
+ if (multivalued === undefined) {
1289
+ return undefined;
1290
+ }
1291
+
1292
+ const minStr = multivalued.min;
1293
+
1294
+ if (minStr === undefined) {
1295
+ return undefined;
1296
+ }
1297
+
1298
+ return parseInt(`${minStr}`);
1299
+ })();
1300
+
1301
+ if (minCount === undefined) {
1302
+ return true;
1303
+ }
1304
+
1305
+ if (values.length === minCount) {
1306
+ return false;
1307
+ }
1308
+
1309
+ return true;
1310
+ })();
1311
+
1312
+ const hasAdd = (() => {
1313
+ if (fieldIndex + 1 !== values.length) {
1314
+ return false;
1315
+ }
1316
+
1317
+ const maxCount = (() => {
1318
+ const { multivalued } = attribute.validators;
1319
+
1320
+ if (multivalued === undefined) {
1321
+ return undefined;
1322
+ }
1323
+
1324
+ const maxStr = multivalued.max;
1325
+
1326
+ if (maxStr === undefined) {
1327
+ return undefined;
1328
+ }
1329
+
1330
+ return parseInt(`${maxStr}`);
1331
+ })();
1332
+
1333
+ if (maxCount === undefined) {
1334
+ return false;
1335
+ }
1336
+
1337
+ if (values.length === maxCount) {
1338
+ return false;
1339
+ }
1340
+
1341
+ return true;
1342
+ })();
1343
+
1344
+ return { hasRemove, hasAdd };
1345
+ }
@@ -25,7 +25,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
25
25
 
26
26
  <ul id="kc-totp-supported-apps">
27
27
  {totp.supportedApplications.map(app => (
28
- <li>{advancedMsg(app)}</li>
28
+ <li key={app}>{advancedMsg(app)}</li>
29
29
  ))}
30
30
  </ul>
31
31
  </li>
@@ -2,12 +2,10 @@ import { useEffect } from "react";
2
2
  import { clsx } from "keycloakify/tools/clsx";
3
3
  import type { PageProps } from "keycloakify/login/pages/PageProps";
4
4
  import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
5
- import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
5
+ import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
6
6
  import type { KcContext } from "../kcContext";
7
7
  import type { I18n } from "../i18n";
8
8
 
9
- const { useInsertScriptTags } = createUseInsertScriptTags();
10
-
11
9
  export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>, I18n>) {
12
10
  const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
13
11
 
@@ -21,6 +19,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
21
19
  const { msg, msgStr } = i18n;
22
20
 
23
21
  const { insertScriptTags } = useInsertScriptTags({
22
+ componentOrHookName: "LoginRecoveryAuthnCodeConfig",
24
23
  scriptTags: [
25
24
  {
26
25
  type: "text/javascript",
@@ -31,7 +30,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
31
30
  var tmpTextarea = document.createElement("textarea");
32
31
  var codes = document.getElementById("kc-recovery-codes-list").getElementsByTagName("li");
33
32
  for (i = 0; i < codes.length; i++) {
34
- tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\n";
33
+ tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\\n";
35
34
  }
36
35
  document.body.appendChild(tmpTextarea);
37
36
  tmpTextarea.select();
@@ -65,7 +64,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
65
64
 
66
65
  for (var i = 0; i < recoveryCodes.length; i++) {
67
66
  var recoveryCodeLiElement = recoveryCodes[i].innerText;
68
- recoveryCodeList += recoveryCodeLiElement + "\r\n";
67
+ recoveryCodeList += recoveryCodeLiElement + "\\r\\n";
69
68
  }
70
69
 
71
70
  return recoveryCodeList;
@@ -84,9 +83,9 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
84
83
  };
85
84
 
86
85
  return fileBodyContent =
87
- "${msgStr("recovery-codes-download-file-header")}\n\n" +
88
- recoveryCodeList + "\n" +
89
- "${msgStr("recovery-codes-download-file-description")}\n\n" +
86
+ "${msgStr("recovery-codes-download-file-header")}\\n\\n" +
87
+ recoveryCodeList + "\\n" +
88
+ "${msgStr("recovery-codes-download-file-description")}\\n\\n" +
90
89
  "${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime();
91
90
  }
92
91
 
@@ -3,12 +3,10 @@ import { clsx } from "keycloakify/tools/clsx";
3
3
  import type { PageProps } from "keycloakify/login/pages/PageProps";
4
4
  import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
5
5
  import { assert } from "tsafe/assert";
6
- import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
6
+ import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
7
7
  import type { KcContext } from "../kcContext";
8
8
  import type { I18n } from "../i18n";
9
9
 
10
- const { useInsertScriptTags } = createUseInsertScriptTags();
11
-
12
10
  export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext, { pageId: "webauthn-authenticate.ftl" }>, I18n>) {
13
11
  const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
14
12
 
@@ -31,6 +29,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
31
29
  const { msg, msgStr, advancedMsg } = i18n;
32
30
 
33
31
  const { insertScriptTags } = useInsertScriptTags({
32
+ componentOrHookName: "WebauthnAuthenticate",
34
33
  scriptTags: [
35
34
  {
36
35
  type: "text/javascript",
@@ -3,12 +3,10 @@ import { clsx } from "keycloakify/tools/clsx";
3
3
  import type { PageProps } from "keycloakify/login/pages/PageProps";
4
4
  import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
5
5
  import { assert } from "tsafe/assert";
6
- import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
6
+ import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
7
7
  import type { KcContext } from "../kcContext";
8
8
  import type { I18n } from "../i18n";
9
9
 
10
- const { useInsertScriptTags } = createUseInsertScriptTags();
11
-
12
10
  export default function WebauthnRegister(props: PageProps<Extract<KcContext, { pageId: "webauthn-register.ftl" }>, I18n>) {
13
11
  const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
14
12
 
@@ -35,6 +33,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
35
33
  const { msg, msgStr } = i18n;
36
34
 
37
35
  const { insertScriptTags } = useInsertScriptTags({
36
+ componentOrHookName: "WebauthnRegister",
38
37
  scriptTags: [
39
38
  {
40
39
  type: "text/javascript",
@@ -0,0 +1,4 @@
1
+ export type ExtractAfterStartingWith<
2
+ Prefix extends string,
3
+ StrEnum
4
+ > = StrEnum extends `${Prefix}${infer U}` ? U : never;
@@ -5,15 +5,15 @@ import type { StatefulObservable } from "../StatefulObservable";
5
5
  /**
6
6
  * Equivalent of https://docs.evt.land/api/react-hooks
7
7
  * */
8
- export function useRerenderOnChange($: StatefulObservable<unknown>): void {
8
+ export function useRerenderOnChange(obs: StatefulObservable<unknown>): void {
9
9
  //NOTE: We use function in case the state is a function
10
- const [, setCurrent] = useState(() => $.current);
10
+ const [, setCurrent] = useState(() => obs.current);
11
11
 
12
12
  useObservable(
13
13
  ({ registerSubscription }) => {
14
- const subscription = $.subscribe(current => setCurrent(() => current));
14
+ const subscription = obs.subscribe(current => setCurrent(() => current));
15
15
  registerSubscription(subscription);
16
16
  },
17
- [$]
17
+ [obs]
18
18
  );
19
19
  }
@@ -0,0 +1,2 @@
1
+ /** Pendant of `keyof T` */
2
+ export type ValueOf<T> = T[keyof T];
@@ -1,45 +1,61 @@
1
1
  import { assert } from "tsafe/assert";
2
2
  import { is } from "tsafe/is";
3
- import { deepClone } from "./deepClone";
3
+ import { structuredCloneButFunctions } from "./structuredCloneButFunctions";
4
4
 
5
- //Warning: Be mindful that because of array this is not idempotent.
5
+ /** NOTE: Array a copied over, not merged. */
6
6
  export function deepAssign(params: {
7
7
  target: Record<string, unknown>;
8
8
  source: Record<string, unknown>;
9
- }) {
10
- const { target } = params;
11
-
12
- const source = deepClone(params.source);
9
+ }): void {
10
+ const { target, source } = params;
13
11
 
14
12
  Object.keys(source).forEach(key => {
15
13
  var dereferencedSource = source[key];
16
14
 
15
+ if (dereferencedSource === undefined) {
16
+ delete target[key];
17
+ return;
18
+ }
19
+
20
+ if (dereferencedSource instanceof Date) {
21
+ assign({
22
+ target,
23
+ key,
24
+ value: new Date(dereferencedSource.getTime())
25
+ });
26
+
27
+ return;
28
+ }
29
+
30
+ if (dereferencedSource instanceof Array) {
31
+ assign({
32
+ target,
33
+ key,
34
+ value: structuredCloneButFunctions(dereferencedSource)
35
+ });
36
+
37
+ return;
38
+ }
39
+
17
40
  if (
18
- target[key] === undefined ||
19
41
  dereferencedSource instanceof Function ||
20
42
  !(dereferencedSource instanceof Object)
21
43
  ) {
22
- Object.defineProperty(target, key, {
23
- enumerable: true,
24
- writable: true,
25
- configurable: true,
44
+ assign({
45
+ target,
46
+ key,
26
47
  value: dereferencedSource
27
48
  });
28
49
 
29
50
  return;
30
51
  }
31
52
 
32
- const dereferencedTarget = target[key];
33
-
34
- if (dereferencedSource instanceof Array) {
35
- assert(is<unknown[]>(dereferencedTarget));
36
- assert(is<unknown[]>(dereferencedSource));
37
-
38
- dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
39
-
40
- return;
53
+ if (!(target[key] instanceof Object)) {
54
+ target[key] = {};
41
55
  }
42
56
 
57
+ const dereferencedTarget = target[key];
58
+
43
59
  assert(is<Record<string, unknown>>(dereferencedTarget));
44
60
  assert(is<Record<string, unknown>>(dereferencedSource));
45
61
 
@@ -49,3 +65,18 @@ export function deepAssign(params: {
49
65
  });
50
66
  });
51
67
  }
68
+
69
+ function assign(params: {
70
+ target: Record<string, unknown>;
71
+ key: string;
72
+ value: unknown;
73
+ }): void {
74
+ const { target, key, value } = params;
75
+
76
+ Object.defineProperty(target, key, {
77
+ enumerable: true,
78
+ writable: true,
79
+ configurable: true,
80
+ value
81
+ });
82
+ }
@@ -0,0 +1,24 @@
1
+ import "minimal-polyfills/Object.fromEntries";
2
+
3
+ /**
4
+ * Functionally equivalent to structuredClone but
5
+ * functions are not cloned but kept as is.
6
+ * (as opposed to structuredClone that chokes if it encounters a function)
7
+ */
8
+ export function structuredCloneButFunctions<T>(o: T): T {
9
+ if (!(o instanceof Object)) {
10
+ return o;
11
+ }
12
+
13
+ if (typeof o === "function") {
14
+ return o;
15
+ }
16
+
17
+ if (o instanceof Array) {
18
+ return o.map(structuredCloneButFunctions) as any;
19
+ }
20
+
21
+ return Object.fromEntries(
22
+ Object.entries(o).map(([key, value]) => [key, structuredCloneButFunctions(value)])
23
+ ) as any;
24
+ }