keycloakify 6.0.3 → 6.3.0

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 (74) hide show
  1. package/README.md +8 -1
  2. package/bin/create-keycloak-email-directory.js +9 -4
  3. package/bin/create-keycloak-email-directory.js.map +1 -1
  4. package/bin/download-builtin-keycloak-theme.d.ts +1 -0
  5. package/bin/download-builtin-keycloak-theme.js +13 -6
  6. package/bin/download-builtin-keycloak-theme.js.map +1 -1
  7. package/bin/generate-i18n-messages.js +8 -3
  8. package/bin/generate-i18n-messages.js.map +1 -1
  9. package/bin/keycloakify/BuildOptions.d.ts +2 -0
  10. package/bin/keycloakify/BuildOptions.js +3 -2
  11. package/bin/keycloakify/BuildOptions.js.map +1 -1
  12. package/bin/keycloakify/generateFtl/generateFtl.d.ts +1 -1
  13. package/bin/keycloakify/generateFtl/generateFtl.js +3 -1
  14. package/bin/keycloakify/generateFtl/generateFtl.js.map +1 -1
  15. package/bin/keycloakify/generateKeycloakThemeResources.d.ts +1 -0
  16. package/bin/keycloakify/generateKeycloakThemeResources.js +5 -2
  17. package/bin/keycloakify/generateKeycloakThemeResources.js.map +1 -1
  18. package/bin/keycloakify/keycloakify.js +8 -4
  19. package/bin/keycloakify/keycloakify.js.map +1 -1
  20. package/bin/tools/cliOptions.d.ts +5 -0
  21. package/bin/tools/cliOptions.js +16 -0
  22. package/bin/tools/cliOptions.js.map +1 -0
  23. package/bin/tools/downloadAndUnzip.d.ts +1 -0
  24. package/bin/tools/downloadAndUnzip.js +1 -1
  25. package/bin/tools/downloadAndUnzip.js.map +1 -1
  26. package/bin/tools/logger.d.ts +12 -0
  27. package/bin/tools/logger.js +23 -0
  28. package/bin/tools/logger.js.map +1 -0
  29. package/bin/tsconfig.tsbuildinfo +1 -1
  30. package/lib/components/IdpReviewUserProfile.d.ts +9 -0
  31. package/lib/components/IdpReviewUserProfile.js +31 -0
  32. package/lib/components/IdpReviewUserProfile.js.map +1 -0
  33. package/lib/components/KcApp.js +6 -0
  34. package/lib/components/KcApp.js.map +1 -1
  35. package/lib/components/RegisterUserProfile.js +2 -61
  36. package/lib/components/RegisterUserProfile.js.map +1 -1
  37. package/lib/components/UpdateUserProfile.d.ts +9 -0
  38. package/lib/components/UpdateUserProfile.js +32 -0
  39. package/lib/components/UpdateUserProfile.js.map +1 -0
  40. package/lib/components/shared/UserProfileCommons.d.ts +16 -0
  41. package/lib/components/shared/UserProfileCommons.js +76 -0
  42. package/lib/components/shared/UserProfileCommons.js.map +1 -0
  43. package/lib/getKcContext/KcContextBase.d.ts +16 -1
  44. package/lib/getKcContext/KcContextBase.js.map +1 -1
  45. package/lib/getKcContext/getKcContext.js +6 -3
  46. package/lib/getKcContext/getKcContext.js.map +1 -1
  47. package/lib/getKcContext/kcContextMocks/kcContextMocks.js +107 -98
  48. package/lib/getKcContext/kcContextMocks/kcContextMocks.js.map +1 -1
  49. package/lib/tsconfig.tsbuildinfo +1 -1
  50. package/lib/useFormValidationSlice.d.ts +5 -1
  51. package/lib/useFormValidationSlice.js +4 -0
  52. package/lib/useFormValidationSlice.js.map +1 -1
  53. package/package.json +24 -2
  54. package/src/bin/create-keycloak-email-directory.ts +8 -3
  55. package/src/bin/download-builtin-keycloak-theme.ts +11 -5
  56. package/src/bin/generate-i18n-messages.ts +9 -3
  57. package/src/bin/keycloakify/BuildOptions.ts +5 -2
  58. package/src/bin/keycloakify/generateFtl/generateFtl.ts +3 -1
  59. package/src/bin/keycloakify/generateKeycloakThemeResources.ts +6 -2
  60. package/src/bin/keycloakify/keycloakify.ts +8 -3
  61. package/src/bin/tools/cliOptions.ts +15 -0
  62. package/src/bin/tools/downloadAndUnzip.ts +8 -2
  63. package/src/bin/tools/logger.ts +27 -0
  64. package/src/lib/components/IdpReviewUserProfile.tsx +46 -0
  65. package/src/lib/components/KcApp.tsx +6 -0
  66. package/src/lib/components/RegisterUserProfile.tsx +3 -156
  67. package/src/lib/components/UpdateUserProfile.tsx +71 -0
  68. package/src/lib/components/shared/UserProfileCommons.tsx +170 -0
  69. package/src/lib/getKcContext/KcContextBase.ts +20 -1
  70. package/src/lib/getKcContext/getKcContext.ts +10 -4
  71. package/src/lib/getKcContext/kcContextMocks/kcContextMocks.ts +113 -98
  72. package/src/lib/useFormValidationSlice.tsx +5 -1
  73. package/src/test/bin/generateKeycloakThemeResources.ts +2 -1
  74. package/src/test/bin/setupSampleReactProject.ts +2 -1
@@ -1,12 +1,10 @@
1
- import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
1
+ import React, { useMemo, memo, useState } from "react";
2
2
  import Template from "./Template";
3
3
  import type { KcProps } from "./KcProps";
4
- import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
4
+ import type { KcContextBase } from "../getKcContext/KcContextBase";
5
5
  import { useCssAndCx } from "../tools/useCssAndCx";
6
- import type { ReactComponent } from "../tools/ReactComponent";
7
- import { useCallbackFactory } from "powerhooks/useCallbackFactory";
8
- import { useFormValidationSlice } from "../useFormValidationSlice";
9
6
  import type { I18n } from "../i18n";
7
+ import { UserProfileFormFields } from "./shared/UserProfileCommons";
10
8
 
11
9
  const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
12
10
  const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
@@ -66,155 +64,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
66
64
  );
67
65
  });
68
66
 
69
- type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
70
- Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
71
- onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
72
- };
73
-
74
- const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
75
- const { cx, css } = useCssAndCx();
76
-
77
- const { advancedMsg } = i18n;
78
-
79
- const {
80
- formValidationState: { fieldStateByAttributeName, isFormSubmittable },
81
- formValidationReducer,
82
- attributesWithPassword
83
- } = useFormValidationSlice({
84
- kcContext,
85
- i18n
86
- });
87
-
88
- useEffect(() => {
89
- onIsFormSubmittableValueChange(isFormSubmittable);
90
- }, [isFormSubmittable]);
91
-
92
- const onChangeFactory = useCallbackFactory(
93
- (
94
- [name]: [string],
95
- [
96
- {
97
- target: { value }
98
- }
99
- ]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
100
- ) =>
101
- formValidationReducer({
102
- "action": "update value",
103
- name,
104
- "newValue": value
105
- })
106
- );
107
-
108
- const onBlurFactory = useCallbackFactory(([name]: [string]) =>
109
- formValidationReducer({
110
- "action": "focus lost",
111
- name
112
- })
113
- );
114
-
115
- let currentGroup = "";
116
-
117
- return (
118
- <>
119
- {attributesWithPassword.map((attribute, i) => {
120
- const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
121
-
122
- const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
123
-
124
- const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
125
-
126
- return (
127
- <Fragment key={i}>
128
- {group !== currentGroup && (currentGroup = group) !== "" && (
129
- <div className={formGroupClassName}>
130
- <div className={cx(props.kcContentWrapperClass)}>
131
- <label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
132
- {advancedMsg(groupDisplayHeader) || currentGroup}
133
- </label>
134
- </div>
135
- {groupDisplayDescription !== "" && (
136
- <div className={cx(props.kcLabelWrapperClass)}>
137
- <label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
138
- {advancedMsg(groupDisplayDescription)}
139
- </label>
140
- </div>
141
- )}
142
- </div>
143
- )}
144
- <div className={formGroupClassName}>
145
- <div className={cx(props.kcLabelWrapperClass)}>
146
- <label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
147
- {advancedMsg(attribute.displayName ?? "")}
148
- </label>
149
- {attribute.required && <>*</>}
150
- </div>
151
- <div className={cx(props.kcInputWrapperClass)}>
152
- {(() => {
153
- const { options } = attribute.validators;
154
-
155
- if (options !== undefined) {
156
- return (
157
- <select
158
- id={attribute.name}
159
- name={attribute.name}
160
- onChange={onChangeFactory(attribute.name)}
161
- onBlur={onBlurFactory(attribute.name)}
162
- value={value}
163
- >
164
- {options.options.map(option => (
165
- <option key={option} value={option}>
166
- {option}
167
- </option>
168
- ))}
169
- </select>
170
- );
171
- }
172
-
173
- return (
174
- <input
175
- type={(() => {
176
- switch (attribute.name) {
177
- case "password-confirm":
178
- case "password":
179
- return "password";
180
- default:
181
- return "text";
182
- }
183
- })()}
184
- id={attribute.name}
185
- name={attribute.name}
186
- value={value}
187
- onChange={onChangeFactory(attribute.name)}
188
- className={cx(props.kcInputClass)}
189
- aria-invalid={displayableErrors.length !== 0}
190
- disabled={attribute.readOnly}
191
- autoComplete={attribute.autocomplete}
192
- onBlur={onBlurFactory(attribute.name)}
193
- />
194
- );
195
- })()}
196
- {displayableErrors.length !== 0 && (
197
- <span
198
- id={`input-error-${attribute.name}`}
199
- className={cx(
200
- props.kcInputErrorMessageClass,
201
- css({
202
- "position": displayableErrors.length === 1 ? "absolute" : undefined,
203
- "& > span": { "display": "block" }
204
- })
205
- )}
206
- aria-live="polite"
207
- >
208
- {displayableErrors.map(({ errorMessage }) => errorMessage)}
209
- </span>
210
- )}
211
- </div>
212
- </div>
213
- </Fragment>
214
- );
215
- })}
216
- </>
217
- );
218
- });
219
-
220
67
  export default RegisterUserProfile;
@@ -0,0 +1,71 @@
1
+ import React, { useState, memo } from "react";
2
+ import Template from "./Template";
3
+ import type { KcProps } from "./KcProps";
4
+ import type { KcContextBase } from "../getKcContext/KcContextBase";
5
+ import { useCssAndCx } from "../tools/useCssAndCx";
6
+ import type { I18n } from "../i18n";
7
+ import { UserProfileFormFields } from "./shared/UserProfileCommons";
8
+
9
+ const UpdateUserProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n } & KcProps) => {
10
+ const { cx } = useCssAndCx();
11
+
12
+ const { msg, msgStr } = i18n;
13
+
14
+ const { url, isAppInitiatedAction } = kcContext;
15
+
16
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
17
+
18
+ return (
19
+ <Template
20
+ {...{ kcContext, i18n, ...props }}
21
+ doFetchDefaultThemeResources={true}
22
+ headerNode={msg("loginProfileTitle")}
23
+ formNode={
24
+ <form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
25
+ <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
26
+
27
+ <div className={cx(props.kcFormGroupClass)}>
28
+ <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
29
+ <div className={cx(props.kcFormOptionsWrapperClass)}></div>
30
+ </div>
31
+
32
+ <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
33
+ {isAppInitiatedAction ? (
34
+ <>
35
+ <input
36
+ className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
37
+ type="submit"
38
+ value={msgStr("doSubmit")}
39
+ />
40
+ <button
41
+ className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
42
+ type="submit"
43
+ name="cancel-aia"
44
+ value="true"
45
+ formNoValidate
46
+ >
47
+ {msg("doCancel")}
48
+ </button>
49
+ </>
50
+ ) : (
51
+ <input
52
+ className={cx(
53
+ props.kcButtonClass,
54
+ props.kcButtonPrimaryClass,
55
+ props.kcButtonBlockClass,
56
+ props.kcButtonLargeClass
57
+ )}
58
+ type="submit"
59
+ defaultValue={msgStr("doSubmit")}
60
+ disabled={!isFomSubmittable}
61
+ />
62
+ )}
63
+ </div>
64
+ </div>
65
+ </form>
66
+ }
67
+ />
68
+ );
69
+ });
70
+
71
+ export default UpdateUserProfile;
@@ -0,0 +1,170 @@
1
+ import React, { memo, useEffect, Fragment } from "react";
2
+ import type { KcProps } from "../KcProps";
3
+ import type { Attribute } from "../../getKcContext/KcContextBase";
4
+ import { useCssAndCx } from "../../tools/useCssAndCx";
5
+ import type { ReactComponent } from "../../tools/ReactComponent";
6
+ import { useCallbackFactory } from "powerhooks/useCallbackFactory";
7
+ import { useFormValidationSlice } from "../../useFormValidationSlice";
8
+ import type { I18n } from "../../i18n";
9
+ import type { Param0 } from "tsafe/Param0";
10
+
11
+ export type UserProfileFormFieldsProps = {
12
+ kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
13
+ i18n: I18n;
14
+ } & KcProps &
15
+ Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
16
+ onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
17
+ };
18
+
19
+ export const UserProfileFormFields = memo(
20
+ ({ kcContext, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
21
+ const { cx, css } = useCssAndCx();
22
+
23
+ const { advancedMsg } = i18n;
24
+
25
+ const {
26
+ formValidationState: { fieldStateByAttributeName, isFormSubmittable },
27
+ formValidationReducer,
28
+ attributesWithPassword
29
+ } = useFormValidationSlice({
30
+ kcContext,
31
+ i18n
32
+ });
33
+
34
+ useEffect(() => {
35
+ onIsFormSubmittableValueChange(isFormSubmittable);
36
+ }, [isFormSubmittable]);
37
+
38
+ const onChangeFactory = useCallbackFactory(
39
+ (
40
+ [name]: [string],
41
+ [
42
+ {
43
+ target: { value }
44
+ }
45
+ ]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
46
+ ) =>
47
+ formValidationReducer({
48
+ "action": "update value",
49
+ name,
50
+ "newValue": value
51
+ })
52
+ );
53
+
54
+ const onBlurFactory = useCallbackFactory(([name]: [string]) =>
55
+ formValidationReducer({
56
+ "action": "focus lost",
57
+ name
58
+ })
59
+ );
60
+
61
+ let currentGroup = "";
62
+
63
+ return (
64
+ <>
65
+ {attributesWithPassword.map((attribute, i) => {
66
+ const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
67
+
68
+ const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
69
+
70
+ const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
71
+
72
+ return (
73
+ <Fragment key={i}>
74
+ {group !== currentGroup && (currentGroup = group) !== "" && (
75
+ <div className={formGroupClassName}>
76
+ <div className={cx(props.kcContentWrapperClass)}>
77
+ <label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
78
+ {advancedMsg(groupDisplayHeader) || currentGroup}
79
+ </label>
80
+ </div>
81
+ {groupDisplayDescription !== "" && (
82
+ <div className={cx(props.kcLabelWrapperClass)}>
83
+ <label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
84
+ {advancedMsg(groupDisplayDescription)}
85
+ </label>
86
+ </div>
87
+ )}
88
+ </div>
89
+ )}
90
+
91
+ {BeforeField && <BeforeField attribute={attribute} />}
92
+
93
+ <div className={formGroupClassName}>
94
+ <div className={cx(props.kcLabelWrapperClass)}>
95
+ <label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
96
+ {advancedMsg(attribute.displayName ?? "")}
97
+ </label>
98
+ {attribute.required && <>*</>}
99
+ </div>
100
+ <div className={cx(props.kcInputWrapperClass)}>
101
+ {(() => {
102
+ const { options } = attribute.validators;
103
+
104
+ if (options !== undefined) {
105
+ return (
106
+ <select
107
+ id={attribute.name}
108
+ name={attribute.name}
109
+ onChange={onChangeFactory(attribute.name)}
110
+ onBlur={onBlurFactory(attribute.name)}
111
+ value={value}
112
+ >
113
+ {options.options.map(option => (
114
+ <option key={option} value={option}>
115
+ {option}
116
+ </option>
117
+ ))}
118
+ </select>
119
+ );
120
+ }
121
+
122
+ return (
123
+ <input
124
+ type={(() => {
125
+ switch (attribute.name) {
126
+ case "password-confirm":
127
+ case "password":
128
+ return "password";
129
+ default:
130
+ return "text";
131
+ }
132
+ })()}
133
+ id={attribute.name}
134
+ name={attribute.name}
135
+ value={value}
136
+ onChange={onChangeFactory(attribute.name)}
137
+ className={cx(props.kcInputClass)}
138
+ aria-invalid={displayableErrors.length !== 0}
139
+ disabled={attribute.readOnly}
140
+ autoComplete={attribute.autocomplete}
141
+ onBlur={onBlurFactory(attribute.name)}
142
+ />
143
+ );
144
+ })()}
145
+ {displayableErrors.length !== 0 && (
146
+ <span
147
+ id={`input-error-${attribute.name}`}
148
+ className={cx(
149
+ props.kcInputErrorMessageClass,
150
+ css({
151
+ "position": displayableErrors.length === 1 ? "absolute" : undefined,
152
+ "& > span": { "display": "block" }
153
+ })
154
+ )}
155
+ aria-live="polite"
156
+ >
157
+ {displayableErrors.map(({ errorMessage }) => errorMessage)}
158
+ </span>
159
+ )}
160
+ </div>
161
+ </div>
162
+
163
+ {AfterField && <AfterField attribute={attribute} />}
164
+ </Fragment>
165
+ );
166
+ })}
167
+ </>
168
+ );
169
+ }
170
+ );
@@ -25,7 +25,9 @@ export type KcContextBase =
25
25
  | KcContextBase.LoginIdpLinkEmail
26
26
  | KcContextBase.LoginPageExpired
27
27
  | KcContextBase.LoginConfigTotp
28
- | KcContextBase.LogoutConfirm;
28
+ | KcContextBase.LogoutConfirm
29
+ | KcContextBase.UpdateUserProfile
30
+ | KcContextBase.IdpReviewUserProfile;
29
31
 
30
32
  export declare namespace KcContextBase {
31
33
  export type Common = {
@@ -270,6 +272,23 @@ export declare namespace KcContextBase {
270
272
  skipLink?: boolean;
271
273
  };
272
274
  };
275
+
276
+ export type UpdateUserProfile = Common & {
277
+ pageId: "update-user-profile.ftl";
278
+ profile: {
279
+ context: "REGISTRATION_PROFILE";
280
+ attributes: Attribute[];
281
+ attributesByName: Record<string, Attribute>;
282
+ };
283
+ };
284
+
285
+ export type IdpReviewUserProfile = Common & {
286
+ pageId: "idp-review-user-profile.ftl";
287
+ profile: {
288
+ attributes: Attribute[];
289
+ attributesByName: Record<string, Attribute>;
290
+ };
291
+ };
273
292
  }
274
293
 
275
294
  export type Attribute = {
@@ -47,8 +47,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
47
47
  "source": partialKcContextCustomMock
48
48
  });
49
49
 
50
- if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
51
- assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
50
+ if (
51
+ partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
52
+ partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
53
+ partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
54
+ ) {
55
+ assert(
56
+ kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
57
+ kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
58
+ kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
59
+ );
52
60
 
53
61
  const { attributes } = kcContextDefaultMock.profile;
54
62
 
@@ -60,8 +68,6 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
60
68
  ].filter(exclude(undefined));
61
69
 
62
70
  attributes.forEach(attribute => {
63
- console.log("====>", attribute);
64
-
65
71
  const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
66
72
 
67
73
  const augmentedAttribute: Attribute = {} as any;