keycloakify 6.0.2 → 6.2.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 (68) 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 +2 -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/KcApp.js +3 -0
  31. package/lib/components/KcApp.js.map +1 -1
  32. package/lib/components/RegisterUserProfile.js +3 -62
  33. package/lib/components/RegisterUserProfile.js.map +1 -1
  34. package/lib/components/UpdateUserProfile.d.ts +9 -0
  35. package/lib/components/UpdateUserProfile.js +32 -0
  36. package/lib/components/UpdateUserProfile.js.map +1 -0
  37. package/lib/components/shared/UserProfileCommons.d.ts +17 -0
  38. package/lib/components/shared/UserProfileCommons.js +76 -0
  39. package/lib/components/shared/UserProfileCommons.js.map +1 -0
  40. package/lib/getKcContext/KcContextBase.d.ts +9 -1
  41. package/lib/getKcContext/KcContextBase.js.map +1 -1
  42. package/lib/getKcContext/kcContextMocks/kcContextMocks.js +103 -98
  43. package/lib/getKcContext/kcContextMocks/kcContextMocks.js.map +1 -1
  44. package/lib/i18n/index.js +0 -7
  45. package/lib/i18n/index.js.map +1 -1
  46. package/lib/tsconfig.tsbuildinfo +1 -1
  47. package/lib/useFormValidationSlice.d.ts +1 -1
  48. package/package.json +20 -2
  49. package/src/bin/create-keycloak-email-directory.ts +8 -3
  50. package/src/bin/download-builtin-keycloak-theme.ts +11 -5
  51. package/src/bin/generate-i18n-messages.ts +9 -3
  52. package/src/bin/keycloakify/BuildOptions.ts +5 -2
  53. package/src/bin/keycloakify/generateFtl/generateFtl.ts +2 -1
  54. package/src/bin/keycloakify/generateKeycloakThemeResources.ts +6 -2
  55. package/src/bin/keycloakify/keycloakify.ts +8 -3
  56. package/src/bin/tools/cliOptions.ts +15 -0
  57. package/src/bin/tools/downloadAndUnzip.ts +8 -2
  58. package/src/bin/tools/logger.ts +27 -0
  59. package/src/lib/components/KcApp.tsx +3 -0
  60. package/src/lib/components/RegisterUserProfile.tsx +10 -157
  61. package/src/lib/components/UpdateUserProfile.tsx +77 -0
  62. package/src/lib/components/shared/UserProfileCommons.tsx +172 -0
  63. package/src/lib/getKcContext/KcContextBase.ts +11 -1
  64. package/src/lib/getKcContext/kcContextMocks/kcContextMocks.ts +105 -98
  65. package/src/lib/i18n/index.tsx +0 -10
  66. package/src/lib/useFormValidationSlice.tsx +1 -1
  67. package/src/test/bin/generateKeycloakThemeResources.ts +2 -1
  68. package/src/test/bin/setupSampleReactProject.ts +2 -1
@@ -0,0 +1,77 @@
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 LoginUpdateProfile = 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
26
+ kcContext={kcContext}
27
+ doInsertPasswordFields={true}
28
+ onIsFormSubmittableValueChange={setIsFomSubmittable}
29
+ i18n={i18n}
30
+ {...props}
31
+ />
32
+
33
+ <div className={cx(props.kcFormGroupClass)}>
34
+ <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
35
+ <div className={cx(props.kcFormOptionsWrapperClass)}></div>
36
+ </div>
37
+
38
+ <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
39
+ {isAppInitiatedAction ? (
40
+ <>
41
+ <input
42
+ className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
43
+ type="submit"
44
+ value={msgStr("doSubmit")}
45
+ />
46
+ <button
47
+ className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
48
+ type="submit"
49
+ name="cancel-aia"
50
+ value="true"
51
+ formNoValidate
52
+ >
53
+ {msg("doCancel")}
54
+ </button>
55
+ </>
56
+ ) : (
57
+ <input
58
+ className={cx(
59
+ props.kcButtonClass,
60
+ props.kcButtonPrimaryClass,
61
+ props.kcButtonBlockClass,
62
+ props.kcButtonLargeClass
63
+ )}
64
+ type="submit"
65
+ defaultValue={msgStr("doSubmit")}
66
+ disabled={!isFomSubmittable}
67
+ />
68
+ )}
69
+ </div>
70
+ </div>
71
+ </form>
72
+ }
73
+ />
74
+ );
75
+ });
76
+
77
+ export default LoginUpdateProfile;
@@ -0,0 +1,172 @@
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: KcContextBase.RegisterUserProfile;
13
+ kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
14
+ doInsertPasswordFields: boolean;
15
+ i18n: I18n;
16
+ } & KcProps &
17
+ Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
18
+ onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
19
+ };
20
+
21
+ export const UserProfileFormFields = memo(
22
+ ({ kcContext, doInsertPasswordFields, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
23
+ const { cx, css } = useCssAndCx();
24
+
25
+ const { advancedMsg } = i18n;
26
+
27
+ const {
28
+ formValidationState: { fieldStateByAttributeName, isFormSubmittable },
29
+ formValidationReducer,
30
+ attributesWithPassword
31
+ } = useFormValidationSlice({
32
+ kcContext,
33
+ i18n
34
+ });
35
+
36
+ useEffect(() => {
37
+ onIsFormSubmittableValueChange(isFormSubmittable);
38
+ }, [isFormSubmittable]);
39
+
40
+ const onChangeFactory = useCallbackFactory(
41
+ (
42
+ [name]: [string],
43
+ [
44
+ {
45
+ target: { value }
46
+ }
47
+ ]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
48
+ ) =>
49
+ formValidationReducer({
50
+ "action": "update value",
51
+ name,
52
+ "newValue": value
53
+ })
54
+ );
55
+
56
+ const onBlurFactory = useCallbackFactory(([name]: [string]) =>
57
+ formValidationReducer({
58
+ "action": "focus lost",
59
+ name
60
+ })
61
+ );
62
+
63
+ let currentGroup = "";
64
+
65
+ return (
66
+ <>
67
+ {(doInsertPasswordFields ? attributesWithPassword : kcContext.profile.attributes).map((attribute, i) => {
68
+ const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
69
+
70
+ const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
71
+
72
+ const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
73
+
74
+ return (
75
+ <Fragment key={i}>
76
+ {group !== currentGroup && (currentGroup = group) !== "" && (
77
+ <div className={formGroupClassName}>
78
+ <div className={cx(props.kcContentWrapperClass)}>
79
+ <label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
80
+ {advancedMsg(groupDisplayHeader) || currentGroup}
81
+ </label>
82
+ </div>
83
+ {groupDisplayDescription !== "" && (
84
+ <div className={cx(props.kcLabelWrapperClass)}>
85
+ <label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
86
+ {advancedMsg(groupDisplayDescription)}
87
+ </label>
88
+ </div>
89
+ )}
90
+ </div>
91
+ )}
92
+
93
+ {BeforeField && <BeforeField attribute={attribute} />}
94
+
95
+ <div className={formGroupClassName}>
96
+ <div className={cx(props.kcLabelWrapperClass)}>
97
+ <label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
98
+ {advancedMsg(attribute.displayName ?? "")}
99
+ </label>
100
+ {attribute.required && <>*</>}
101
+ </div>
102
+ <div className={cx(props.kcInputWrapperClass)}>
103
+ {(() => {
104
+ const { options } = attribute.validators;
105
+
106
+ if (options !== undefined) {
107
+ return (
108
+ <select
109
+ id={attribute.name}
110
+ name={attribute.name}
111
+ onChange={onChangeFactory(attribute.name)}
112
+ onBlur={onBlurFactory(attribute.name)}
113
+ value={value}
114
+ >
115
+ {options.options.map(option => (
116
+ <option key={option} value={option}>
117
+ {option}
118
+ </option>
119
+ ))}
120
+ </select>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <input
126
+ type={(() => {
127
+ switch (attribute.name) {
128
+ case "password-confirm":
129
+ case "password":
130
+ return "password";
131
+ default:
132
+ return "text";
133
+ }
134
+ })()}
135
+ id={attribute.name}
136
+ name={attribute.name}
137
+ value={value}
138
+ onChange={onChangeFactory(attribute.name)}
139
+ className={cx(props.kcInputClass)}
140
+ aria-invalid={displayableErrors.length !== 0}
141
+ disabled={attribute.readOnly}
142
+ autoComplete={attribute.autocomplete}
143
+ onBlur={onBlurFactory(attribute.name)}
144
+ />
145
+ );
146
+ })()}
147
+ {displayableErrors.length !== 0 && (
148
+ <span
149
+ id={`input-error-${attribute.name}`}
150
+ className={cx(
151
+ props.kcInputErrorMessageClass,
152
+ css({
153
+ "position": displayableErrors.length === 1 ? "absolute" : undefined,
154
+ "& > span": { "display": "block" }
155
+ })
156
+ )}
157
+ aria-live="polite"
158
+ >
159
+ {displayableErrors.map(({ errorMessage }) => errorMessage)}
160
+ </span>
161
+ )}
162
+ </div>
163
+ </div>
164
+
165
+ {AfterField && <AfterField attribute={attribute} />}
166
+ </Fragment>
167
+ );
168
+ })}
169
+ </>
170
+ );
171
+ }
172
+ );
@@ -25,7 +25,8 @@ export type KcContextBase =
25
25
  | KcContextBase.LoginIdpLinkEmail
26
26
  | KcContextBase.LoginPageExpired
27
27
  | KcContextBase.LoginConfigTotp
28
- | KcContextBase.LogoutConfirm;
28
+ | KcContextBase.LogoutConfirm
29
+ | KcContextBase.UpdateUserProfile;
29
30
 
30
31
  export declare namespace KcContextBase {
31
32
  export type Common = {
@@ -270,6 +271,15 @@ export declare namespace KcContextBase {
270
271
  skipLink?: boolean;
271
272
  };
272
273
  };
274
+
275
+ export type UpdateUserProfile = Common & {
276
+ pageId: "update-user-profile.ftl";
277
+ profile: {
278
+ context: "REGISTRATION_PROFILE";
279
+ attributes: Attribute[];
280
+ attributesByName: Record<string, Attribute>;
281
+ };
282
+ };
273
283
  }
274
284
 
275
285
  export type Attribute = {
@@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
7
7
 
8
8
  const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
9
9
 
10
+ const attributes: Attribute[] = [
11
+ {
12
+ "validators": {
13
+ "username-prohibited-characters": {
14
+ "ignore.empty.value": true
15
+ },
16
+ "up-username-has-value": {},
17
+ "length": {
18
+ "ignore.empty.value": true,
19
+ "min": "3",
20
+ "max": "255"
21
+ },
22
+ "up-duplicate-username": {},
23
+ "up-username-mutation": {}
24
+ },
25
+ "displayName": "${username}",
26
+ "annotations": {},
27
+ "required": true,
28
+ "groupAnnotations": {},
29
+ "autocomplete": "username",
30
+ "readOnly": false,
31
+ "name": "username",
32
+ "value": "xxxx"
33
+ },
34
+ {
35
+ "validators": {
36
+ "up-email-exists-as-username": {},
37
+ "length": {
38
+ "max": "255",
39
+ "ignore.empty.value": true
40
+ },
41
+ "up-blank-attribute-value": {
42
+ "error-message": "missingEmailMessage",
43
+ "fail-on-null": false
44
+ },
45
+ "up-duplicate-email": {},
46
+ "email": {
47
+ "ignore.empty.value": true
48
+ },
49
+ "pattern": {
50
+ "ignore.empty.value": true,
51
+ "pattern": "gmail\\.com$"
52
+ }
53
+ },
54
+ "displayName": "${email}",
55
+ "annotations": {},
56
+ "required": true,
57
+ "groupAnnotations": {},
58
+ "autocomplete": "email",
59
+ "readOnly": false,
60
+ "name": "email"
61
+ },
62
+ {
63
+ "validators": {
64
+ "length": {
65
+ "max": "255",
66
+ "ignore.empty.value": true
67
+ },
68
+ "person-name-prohibited-characters": {
69
+ "ignore.empty.value": true
70
+ },
71
+ "up-immutable-attribute": {},
72
+ "up-attribute-required-by-metadata-value": {}
73
+ },
74
+ "displayName": "${firstName}",
75
+ "annotations": {},
76
+ "required": true,
77
+ "groupAnnotations": {},
78
+ "readOnly": false,
79
+ "name": "firstName"
80
+ },
81
+ {
82
+ "validators": {
83
+ "length": {
84
+ "max": "255",
85
+ "ignore.empty.value": true
86
+ },
87
+ "person-name-prohibited-characters": {
88
+ "ignore.empty.value": true
89
+ },
90
+ "up-immutable-attribute": {},
91
+ "up-attribute-required-by-metadata-value": {}
92
+ },
93
+ "displayName": "${lastName}",
94
+ "annotations": {},
95
+ "required": true,
96
+ "groupAnnotations": {},
97
+ "readOnly": false,
98
+ "name": "lastName"
99
+ }
100
+ ];
101
+
102
+ const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
103
+
10
104
  export const kcContextCommonMock: KcContextBase.Common = {
11
105
  "url": {
12
106
  "loginAction": "#",
@@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
200
294
  ...registerCommon,
201
295
  "profile": {
202
296
  "context": "REGISTRATION_PROFILE" as const,
203
- ...(() => {
204
- const attributes: Attribute[] = [
205
- {
206
- "validators": {
207
- "username-prohibited-characters": {
208
- "ignore.empty.value": true
209
- },
210
- "up-username-has-value": {},
211
- "length": {
212
- "ignore.empty.value": true,
213
- "min": "3",
214
- "max": "255"
215
- },
216
- "up-duplicate-username": {},
217
- "up-username-mutation": {}
218
- },
219
- "displayName": "${username}",
220
- "annotations": {},
221
- "required": true,
222
- "groupAnnotations": {},
223
- "autocomplete": "username",
224
- "readOnly": false,
225
- "name": "username",
226
- "value": "xxxx"
227
- },
228
- {
229
- "validators": {
230
- "up-email-exists-as-username": {},
231
- "length": {
232
- "max": "255",
233
- "ignore.empty.value": true
234
- },
235
- "up-blank-attribute-value": {
236
- "error-message": "missingEmailMessage",
237
- "fail-on-null": false
238
- },
239
- "up-duplicate-email": {},
240
- "email": {
241
- "ignore.empty.value": true
242
- },
243
- "pattern": {
244
- "ignore.empty.value": true,
245
- "pattern": "gmail\\.com$"
246
- }
247
- },
248
- "displayName": "${email}",
249
- "annotations": {},
250
- "required": true,
251
- "groupAnnotations": {},
252
- "autocomplete": "email",
253
- "readOnly": false,
254
- "name": "email"
255
- },
256
- {
257
- "validators": {
258
- "length": {
259
- "max": "255",
260
- "ignore.empty.value": true
261
- },
262
- "person-name-prohibited-characters": {
263
- "ignore.empty.value": true
264
- },
265
- "up-immutable-attribute": {},
266
- "up-attribute-required-by-metadata-value": {}
267
- },
268
- "displayName": "${firstName}",
269
- "annotations": {},
270
- "required": true,
271
- "groupAnnotations": {},
272
- "readOnly": false,
273
- "name": "firstName"
274
- },
275
- {
276
- "validators": {
277
- "length": {
278
- "max": "255",
279
- "ignore.empty.value": true
280
- },
281
- "person-name-prohibited-characters": {
282
- "ignore.empty.value": true
283
- },
284
- "up-immutable-attribute": {},
285
- "up-attribute-required-by-metadata-value": {}
286
- },
287
- "displayName": "${lastName}",
288
- "annotations": {},
289
- "required": true,
290
- "groupAnnotations": {},
291
- "readOnly": false,
292
- "name": "lastName"
293
- }
294
- ];
295
-
296
- return {
297
- attributes,
298
- "attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
299
- } as any;
300
- })()
297
+ attributes,
298
+ attributesByName
301
299
  }
302
300
  })
303
301
  ];
@@ -423,5 +421,14 @@ export const kcContextMocks: KcContextBase[] = [
423
421
  "baseUrl": "#"
424
422
  },
425
423
  "logoutConfirm": { "code": "123", skipLink: false }
424
+ }),
425
+ id<KcContextBase.UpdateUserProfile>({
426
+ ...kcContextCommonMock,
427
+ "pageId": "update-user-profile.ftl",
428
+ "profile": {
429
+ "context": "REGISTRATION_PROFILE" as const,
430
+ attributes,
431
+ attributesByName
432
+ }
426
433
  })
427
434
  ];
@@ -83,8 +83,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
83
83
  return;
84
84
  }
85
85
 
86
- let isMounted = true;
87
-
88
86
  refHasStartedFetching.current = true;
89
87
 
90
88
  (async () => {
@@ -144,10 +142,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
144
142
  })()
145
143
  ]).then(modules => modules.map(module => module.default));
146
144
 
147
- if (!isMounted) {
148
- return;
149
- }
150
-
151
145
  setI18n({
152
146
  ...createI18nTranslationFunctions({
153
147
  "fallbackMessages": {
@@ -180,10 +174,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
180
174
  )
181
175
  });
182
176
  })();
183
-
184
- return () => {
185
- isMounted = false;
186
- };
187
177
  }, []);
188
178
 
189
179
  return i18n ?? null;
@@ -310,7 +310,7 @@ export function useFormValidationSlice(params: {
310
310
  profile: {
311
311
  attributes: Attribute[];
312
312
  };
313
- passwordRequired: boolean;
313
+ passwordRequired?: boolean;
314
314
  realm: { registrationEmailAsUsername: boolean };
315
315
  };
316
316
  /** NOTE: Try to avoid passing a new ref every render for better performances. */
@@ -14,6 +14,7 @@ generateKeycloakThemeResources({
14
14
  "extraPages": ["my-custom-page.ftl"],
15
15
  "extraThemeProperties": ["env=test"],
16
16
  "isStandalone": true,
17
- "urlPathname": "/keycloakify-demo-app/"
17
+ "urlPathname": "/keycloakify-demo-app/",
18
+ "isSilent": false
18
19
  }
19
20
  });
@@ -8,6 +8,7 @@ export function setupSampleReactProject() {
8
8
  downloadAndUnzip({
9
9
  "url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
10
10
  "destDirPath": sampleReactProjectDirPath,
11
- "cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache")
11
+ "cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
12
+ "isSilent": false
12
13
  });
13
14
  }