@keycloakify/angular 0.0.3 → 0.0.5

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 (182) hide show
  1. package/README.md +2 -5
  2. package/account/DefaultPage/DefaultPage.d.ts +2 -30
  3. package/esm2022/account/containers/template/template.component.mjs +6 -6
  4. package/esm2022/account/directives/kc-class/kc-class.directive.mjs +3 -3
  5. package/esm2022/account/pages/account/account.component.mjs +3 -3
  6. package/esm2022/account/pages/applications/applications.component.mjs +3 -3
  7. package/esm2022/account/pages/federatedIdentity/federatedIdentity.component.mjs +3 -3
  8. package/esm2022/account/pages/log/log.component.mjs +3 -3
  9. package/esm2022/account/pages/password/password.component.mjs +3 -3
  10. package/esm2022/account/pages/sessions/sessions.component.mjs +3 -3
  11. package/esm2022/account/pages/totp/totp.component.mjs +3 -3
  12. package/esm2022/account/services/account-resource-injector/account-resource-injector.service.mjs +3 -3
  13. package/esm2022/account/services/i18n/i18n.service.mjs +3 -3
  14. package/esm2022/lib/directives/attributes/attributes.directive.mjs +3 -3
  15. package/esm2022/lib/pipes/input-type/input-type.pipe.mjs +3 -3
  16. package/esm2022/lib/pipes/is-array-with-empty-object/is-array-with-empty-object.pipe.mjs +3 -3
  17. package/esm2022/lib/pipes/kc-sanitize/kc-sanitize.pipe.mjs +3 -3
  18. package/esm2022/lib/pipes/to-array/to-array.pipe.mjs +3 -3
  19. package/esm2022/lib/pipes/to-number/to-number.pipe.mjs +3 -3
  20. package/esm2022/lib/services/resource-injector/resource-injector.service.mjs +6 -5
  21. package/esm2022/login/components/add-remove-buttons-multi-valued-attribute/add-remove-buttons-multi-valued-attribute.component.mjs +7 -53
  22. package/esm2022/login/components/field-errors/field-errors.component.mjs +3 -3
  23. package/esm2022/login/components/group-label/group-label.component.mjs +3 -3
  24. package/esm2022/login/components/input-field-by-type/input-field-by-type.component.mjs +3 -3
  25. package/esm2022/login/components/input-tag/input-tag.component.mjs +3 -3
  26. package/esm2022/login/components/input-tag-selects/input-tag-selects.component.mjs +3 -3
  27. package/esm2022/login/components/logout-other-sessions/logout-other-sessions.component.mjs +3 -3
  28. package/esm2022/login/components/password-wrapper/password-wrapper.component.mjs +3 -3
  29. package/esm2022/login/components/select-tag/select-tag.component.mjs +3 -3
  30. package/esm2022/login/components/textarea-tag/textarea-tag.component.mjs +3 -3
  31. package/esm2022/login/components/user-profile-form-fields/user-profile-form-fields.component.mjs +15 -22
  32. package/esm2022/login/containers/template/template.component.mjs +6 -6
  33. package/esm2022/login/directives/kc-class/kc-class.directive.mjs +3 -3
  34. package/esm2022/login/pages/code/code.component.mjs +3 -3
  35. package/esm2022/login/pages/delete-account-confirm/delete-account-confirm.component.mjs +3 -3
  36. package/esm2022/login/pages/delete-credential/delete-credential.component.mjs +3 -3
  37. package/esm2022/login/pages/error/error.component.mjs +3 -3
  38. package/esm2022/login/pages/frontchannel-logout/frontchannel-logout.component.mjs +3 -3
  39. package/esm2022/login/pages/idp-review-user-profile/idp-review-user-profile.component.mjs +13 -15
  40. package/esm2022/login/pages/info/info.component.mjs +3 -3
  41. package/esm2022/login/pages/login/login.component.mjs +3 -3
  42. package/esm2022/login/pages/login-config-totp/login-config-totp.component.mjs +3 -3
  43. package/esm2022/login/pages/login-idp-link-confirm/login-idp-link-confirm.component.mjs +3 -3
  44. package/esm2022/login/pages/login-idp-link-confirm-override/login-idp-link-confirm-override.component.mjs +3 -3
  45. package/esm2022/login/pages/login-idp-link-email/login-idp-link-email.component.mjs +3 -3
  46. package/esm2022/login/pages/login-oauth-grant/login-oauth-grant.component.mjs +3 -3
  47. package/esm2022/login/pages/login-oauth2-device-verify-user-code/login-oauth2-device-verify-user-code.component.mjs +3 -3
  48. package/esm2022/login/pages/login-otp/login-otp.component.mjs +3 -3
  49. package/esm2022/login/pages/login-page-expired/login-page-expired.component.mjs +3 -3
  50. package/esm2022/login/pages/login-passkeys-conditional-authenticate/login-passkeys-conditional-authenticate.component.mjs +3 -3
  51. package/esm2022/login/pages/login-password/login-password.component.mjs +3 -3
  52. package/esm2022/login/pages/login-recovery-authn-code-config/login-recovery-authn-code-config.component.mjs +3 -3
  53. package/esm2022/login/pages/login-recovery-authn-code-input/login-recovery-authn-code-input.component.mjs +3 -3
  54. package/esm2022/login/pages/login-reset-otp/login-reset-otp.component.mjs +3 -3
  55. package/esm2022/login/pages/login-reset-password/login-reset-password.component.mjs +3 -3
  56. package/esm2022/login/pages/login-update-password/login-update-password.component.mjs +3 -3
  57. package/esm2022/login/pages/login-update-profile/login-update-profile.component.mjs +13 -15
  58. package/esm2022/login/pages/login-username/login-username.component.mjs +6 -6
  59. package/esm2022/login/pages/login-verify-email/login-verify-email.component.mjs +3 -3
  60. package/esm2022/login/pages/login-x509-info/login-x509-info.component.mjs +3 -3
  61. package/esm2022/login/pages/logout-confirm/logout-confirm.component.mjs +3 -3
  62. package/esm2022/login/pages/register/register.component.mjs +14 -16
  63. package/esm2022/login/pages/saml-post-form/saml-post-form.component.mjs +3 -3
  64. package/esm2022/login/pages/select-authenticator/select-authenticator.component.mjs +3 -3
  65. package/esm2022/login/pages/terms/terms.component.mjs +3 -3
  66. package/esm2022/login/pages/update-email/update-email.component.mjs +13 -15
  67. package/esm2022/login/pages/webauthn-authenticate/webauthn-authenticate.component.mjs +3 -3
  68. package/esm2022/login/pages/webauthn-error/webauthn-error.component.mjs +3 -3
  69. package/esm2022/login/pages/webauthn-register/webauthn-register.component.mjs +3 -3
  70. package/esm2022/login/services/i18n/i18n.service.mjs +3 -3
  71. package/esm2022/login/services/login-resource-injector/login-resource-injector.service.mjs +9 -8
  72. package/esm2022/login/services/user-profile-form/user-profile-form.service.mjs +56 -1004
  73. package/fesm2022/keycloakify-angular-account-containers-template.mjs +6 -6
  74. package/fesm2022/keycloakify-angular-account-directives-kc-class.mjs +3 -3
  75. package/fesm2022/keycloakify-angular-account-pages-account.mjs +3 -3
  76. package/fesm2022/keycloakify-angular-account-pages-applications.mjs +3 -3
  77. package/fesm2022/keycloakify-angular-account-pages-federatedIdentity.mjs +3 -3
  78. package/fesm2022/keycloakify-angular-account-pages-log.mjs +3 -3
  79. package/fesm2022/keycloakify-angular-account-pages-password.mjs +3 -3
  80. package/fesm2022/keycloakify-angular-account-pages-sessions.mjs +3 -3
  81. package/fesm2022/keycloakify-angular-account-pages-totp.mjs +3 -3
  82. package/fesm2022/keycloakify-angular-account-services-account-resource-injector.mjs +3 -3
  83. package/fesm2022/keycloakify-angular-account-services-i18n.mjs +3 -3
  84. package/fesm2022/keycloakify-angular-lib-directives-attributes.mjs +3 -3
  85. package/fesm2022/keycloakify-angular-lib-pipes-input-type.mjs +3 -3
  86. package/fesm2022/keycloakify-angular-lib-pipes-is-array-with-empty-object.mjs +3 -3
  87. package/fesm2022/keycloakify-angular-lib-pipes-kc-sanitize.mjs +3 -3
  88. package/fesm2022/keycloakify-angular-lib-pipes-to-array.mjs +3 -3
  89. package/fesm2022/keycloakify-angular-lib-pipes-to-number.mjs +3 -3
  90. package/fesm2022/keycloakify-angular-lib-services-resource-injector.mjs +5 -4
  91. package/fesm2022/keycloakify-angular-lib-services-resource-injector.mjs.map +1 -1
  92. package/fesm2022/keycloakify-angular-login-components-add-remove-buttons-multi-valued-attribute.mjs +6 -52
  93. package/fesm2022/keycloakify-angular-login-components-add-remove-buttons-multi-valued-attribute.mjs.map +1 -1
  94. package/fesm2022/keycloakify-angular-login-components-field-errors.mjs +3 -3
  95. package/fesm2022/keycloakify-angular-login-components-group-label.mjs +3 -3
  96. package/fesm2022/keycloakify-angular-login-components-input-field-by-type.mjs +3 -3
  97. package/fesm2022/keycloakify-angular-login-components-input-tag-selects.mjs +3 -3
  98. package/fesm2022/keycloakify-angular-login-components-input-tag.mjs +3 -3
  99. package/fesm2022/keycloakify-angular-login-components-logout-other-sessions.mjs +3 -3
  100. package/fesm2022/keycloakify-angular-login-components-password-wrapper.mjs +3 -3
  101. package/fesm2022/keycloakify-angular-login-components-select-tag.mjs +3 -3
  102. package/fesm2022/keycloakify-angular-login-components-textarea-tag.mjs +3 -3
  103. package/fesm2022/keycloakify-angular-login-components-user-profile-form-fields.mjs +14 -21
  104. package/fesm2022/keycloakify-angular-login-components-user-profile-form-fields.mjs.map +1 -1
  105. package/fesm2022/keycloakify-angular-login-containers-template.mjs +6 -6
  106. package/fesm2022/keycloakify-angular-login-directives-kc-class.mjs +3 -3
  107. package/fesm2022/keycloakify-angular-login-pages-code.mjs +3 -3
  108. package/fesm2022/keycloakify-angular-login-pages-delete-account-confirm.mjs +3 -3
  109. package/fesm2022/keycloakify-angular-login-pages-delete-credential.mjs +3 -3
  110. package/fesm2022/keycloakify-angular-login-pages-error.mjs +3 -3
  111. package/fesm2022/keycloakify-angular-login-pages-frontchannel-logout.mjs +3 -3
  112. package/fesm2022/keycloakify-angular-login-pages-idp-review-user-profile.mjs +12 -14
  113. package/fesm2022/keycloakify-angular-login-pages-idp-review-user-profile.mjs.map +1 -1
  114. package/fesm2022/keycloakify-angular-login-pages-info.mjs +3 -3
  115. package/fesm2022/keycloakify-angular-login-pages-login-config-totp.mjs +3 -3
  116. package/fesm2022/keycloakify-angular-login-pages-login-idp-link-confirm-override.mjs +3 -3
  117. package/fesm2022/keycloakify-angular-login-pages-login-idp-link-confirm.mjs +3 -3
  118. package/fesm2022/keycloakify-angular-login-pages-login-idp-link-email.mjs +3 -3
  119. package/fesm2022/keycloakify-angular-login-pages-login-oauth-grant.mjs +3 -3
  120. package/fesm2022/keycloakify-angular-login-pages-login-oauth2-device-verify-user-code.mjs +3 -3
  121. package/fesm2022/keycloakify-angular-login-pages-login-otp.mjs +3 -3
  122. package/fesm2022/keycloakify-angular-login-pages-login-page-expired.mjs +3 -3
  123. package/fesm2022/keycloakify-angular-login-pages-login-passkeys-conditional-authenticate.mjs +3 -3
  124. package/fesm2022/keycloakify-angular-login-pages-login-password.mjs +3 -3
  125. package/fesm2022/keycloakify-angular-login-pages-login-recovery-authn-code-config.mjs +3 -3
  126. package/fesm2022/keycloakify-angular-login-pages-login-recovery-authn-code-input.mjs +3 -3
  127. package/fesm2022/keycloakify-angular-login-pages-login-reset-otp.mjs +3 -3
  128. package/fesm2022/keycloakify-angular-login-pages-login-reset-password.mjs +3 -3
  129. package/fesm2022/keycloakify-angular-login-pages-login-update-password.mjs +3 -3
  130. package/fesm2022/keycloakify-angular-login-pages-login-update-profile.mjs +12 -14
  131. package/fesm2022/keycloakify-angular-login-pages-login-update-profile.mjs.map +1 -1
  132. package/fesm2022/keycloakify-angular-login-pages-login-username.mjs +5 -5
  133. package/fesm2022/keycloakify-angular-login-pages-login-username.mjs.map +1 -1
  134. package/fesm2022/keycloakify-angular-login-pages-login-verify-email.mjs +3 -3
  135. package/fesm2022/keycloakify-angular-login-pages-login-x509-info.mjs +3 -3
  136. package/fesm2022/keycloakify-angular-login-pages-login.mjs +3 -3
  137. package/fesm2022/keycloakify-angular-login-pages-logout-confirm.mjs +3 -3
  138. package/fesm2022/keycloakify-angular-login-pages-register.mjs +13 -15
  139. package/fesm2022/keycloakify-angular-login-pages-register.mjs.map +1 -1
  140. package/fesm2022/keycloakify-angular-login-pages-saml-post-form.mjs +3 -3
  141. package/fesm2022/keycloakify-angular-login-pages-select-authenticator.mjs +3 -3
  142. package/fesm2022/keycloakify-angular-login-pages-terms.mjs +3 -3
  143. package/fesm2022/keycloakify-angular-login-pages-update-email.mjs +12 -14
  144. package/fesm2022/keycloakify-angular-login-pages-update-email.mjs.map +1 -1
  145. package/fesm2022/keycloakify-angular-login-pages-webauthn-authenticate.mjs +3 -3
  146. package/fesm2022/keycloakify-angular-login-pages-webauthn-error.mjs +3 -3
  147. package/fesm2022/keycloakify-angular-login-pages-webauthn-register.mjs +3 -3
  148. package/fesm2022/keycloakify-angular-login-services-i18n.mjs +3 -3
  149. package/fesm2022/keycloakify-angular-login-services-login-resource-injector.mjs +8 -7
  150. package/fesm2022/keycloakify-angular-login-services-login-resource-injector.mjs.map +1 -1
  151. package/fesm2022/keycloakify-angular-login-services-user-profile-form.mjs +55 -1003
  152. package/fesm2022/keycloakify-angular-login-services-user-profile-form.mjs.map +1 -1
  153. package/login/components/add-remove-buttons-multi-valued-attribute/add-remove-buttons-multi-valued-attribute.component.d.ts +0 -1
  154. package/login/components/user-profile-form-fields/user-profile-form-fields.component.d.ts +3 -13
  155. package/login/pages/idp-review-user-profile/idp-review-user-profile.component.d.ts +1 -2
  156. package/login/pages/login-update-profile/login-update-profile.component.d.ts +1 -2
  157. package/login/pages/register/register.component.d.ts +1 -2
  158. package/login/pages/update-email/update-email.component.d.ts +1 -2
  159. package/login/services/user-profile-form/user-profile-form.service.d.ts +20 -41
  160. package/package.json +2 -8
  161. package/src/lib/services/resource-injector/resource-injector.service.ts +2 -1
  162. package/src/login/components/add-remove-buttons-multi-valued-attribute/add-remove-buttons-multi-valued-attribute.component.ts +3 -69
  163. package/src/login/components/user-profile-form-fields/user-profile-form-fields.component.html +71 -68
  164. package/src/login/components/user-profile-form-fields/user-profile-form-fields.component.ts +6 -21
  165. package/src/login/pages/idp-review-user-profile/idp-review-user-profile.component.ts +6 -13
  166. package/src/login/pages/login-update-profile/login-update-profile.component.ts +6 -12
  167. package/src/login/pages/login-username/login-username.component.html +0 -1
  168. package/src/login/pages/register/register.component.html +1 -4
  169. package/src/login/pages/register/register.component.ts +5 -10
  170. package/src/login/pages/update-email/update-email.component.ts +6 -12
  171. package/src/login/services/login-resource-injector/login-resource-injector.service.ts +5 -4
  172. package/src/login/services/user-profile-form/user-profile-form.service.ts +103 -1433
  173. package/esm2022/login/services/submit/keycloakify-angular-login-services-submit.mjs +0 -5
  174. package/esm2022/login/services/submit/public-api.mjs +0 -2
  175. package/esm2022/login/services/submit/submit.service.mjs +0 -20
  176. package/fesm2022/keycloakify-angular-login-services-submit.mjs +0 -27
  177. package/fesm2022/keycloakify-angular-login-services-submit.mjs.map +0 -1
  178. package/login/services/submit/index.d.ts +0 -5
  179. package/login/services/submit/public-api.d.ts +0 -1
  180. package/login/services/submit/submit.service.d.ts +0 -9
  181. package/src/login/services/submit/index.ts +0 -1
  182. package/src/login/services/submit/submit.service.ts +0 -12
@@ -1,39 +1,35 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
- /* eslint-disable @typescript-eslint/ban-ts-comment */
3
-
4
- import {
5
- computed,
6
- inject,
7
- Injectable,
8
- signal,
9
- Signal,
10
- WritableSignal
11
- } from '@angular/core';
1
+ import { inject, Injectable, OnDestroy } from '@angular/core';
2
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
12
3
  import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
13
- import type { KcContext } from '@keycloakify/angular/login/KcContext';
14
4
  import type { I18n } from '@keycloakify/angular/login/i18n';
15
- import { LoginResourceInjectorService } from '@keycloakify/angular/login/services/login-resource-injector';
16
5
  import { LOGIN_I18N } from '@keycloakify/angular/login/tokens/i18n';
17
6
  import { KC_LOGIN_CONTEXT } from '@keycloakify/angular/login/tokens/kc-context';
18
7
  import { DO_MAKE_USER_CONFIRM_PASSWORD } from '@keycloakify/angular/login/tokens/make-user-confirm-password';
19
- import {
20
- type Attribute,
21
- type PasswordPolicies,
22
- type Validators
23
- } from 'keycloakify/login/KcContext';
24
8
  import type {
25
- KcContextLike as KcContextLike_i18n,
26
- MessageKey_defaultSet
27
- } from 'keycloakify/login/i18n/noJsx';
28
- import { emailRegexp } from 'keycloakify/tools/emailRegExp';
29
- import { formatNumber } from 'keycloakify/tools/formatNumber';
30
- import { structuredCloneButFunctions } from 'keycloakify/tools/structuredCloneButFunctions';
31
- import { assert, id } from 'tsafe';
9
+ Attribute,
10
+ PasswordPolicies,
11
+ Validators
12
+ } from 'keycloakify/login/KcContext';
13
+ import * as reactlessApi from 'keycloakify/login/lib/getUserProfileApi';
14
+ import { BehaviorSubject, map, Observable } from 'rxjs';
15
+ import { assert, type Equals } from 'tsafe/assert';
32
16
 
33
- type KcContextLike_useGetErrors = KcContextLike_i18n & {
34
- messagesPerField: Pick<KcContext['messagesPerField'], 'existsError' | 'get'>;
35
- passwordPolicies?: PasswordPolicies;
17
+ export { getButtonToDisplayForMultivaluedAttributeField } from 'keycloakify/login/lib/getUserProfileApi';
18
+
19
+ export type FormFieldError = {
20
+ errorMessage: SafeHtml;
21
+ errorMessageStr: string;
22
+ source: FormFieldError.Source;
23
+ fieldIndex: number | undefined;
36
24
  };
25
+
26
+ {
27
+ type A = Omit<FormFieldError, 'errorMessage' | 'errorMessageStr'>;
28
+ type B = Omit<reactlessApi.FormFieldError, 'advancedMsgArgs'>;
29
+
30
+ assert<Equals<A, B>>();
31
+ }
32
+
37
33
  export namespace FormFieldError {
38
34
  export type Source =
39
35
  | Source.Validator
@@ -61,45 +57,38 @@ export namespace FormFieldError {
61
57
  }
62
58
  }
63
59
 
64
- export type KcContextLike = KcContextLike_i18n &
65
- KcContextLike_useGetErrors & {
66
- profile: {
67
- attributesByName: Record<string, Attribute>;
68
- html5DataAnnotations?: Record<string, string>;
69
- };
70
- passwordRequired?: boolean;
71
- realm: { registrationEmailAsUsername: boolean };
72
- url: {
73
- resourcesPath: string;
74
- };
75
- };
76
- export type FormFieldError = {
77
- errorMessage: SafeHtml; // this was jsx, be carefull
78
- errorMessageStr: string;
79
- source: FormFieldError.Source;
80
- fieldIndex: number | undefined;
81
- };
82
- namespace internal {
83
- export type FormFieldState = {
84
- attribute: Attribute;
85
- errors: FormFieldError[];
86
- hasLostFocusAtLeastOnce: boolean | boolean[];
87
- valueOrValues: string | string[];
88
- };
60
+ {
61
+ type A = FormFieldError.Source;
62
+ type B = reactlessApi.FormFieldError.Source;
89
63
 
90
- export type State = {
91
- formFieldStates: FormFieldState[];
92
- };
64
+ assert<Equals<A, B>>();
93
65
  }
94
- type FormFieldState = {
66
+
67
+ export type FormFieldState = {
95
68
  attribute: Attribute;
96
69
  displayableErrors: FormFieldError[];
97
70
  valueOrValues: string | string[];
98
71
  };
99
- type FormState = {
72
+
73
+ {
74
+ type A = Omit<FormFieldState, 'displayableErrors'>;
75
+ type B = Omit<reactlessApi.FormFieldState, 'displayableErrors'>;
76
+
77
+ assert<Equals<A, B>>();
78
+ }
79
+
80
+ export type FormState = {
100
81
  isFormSubmittable: boolean;
101
82
  formFieldStates: FormFieldState[];
102
83
  };
84
+
85
+ {
86
+ type A = Omit<FormState, 'formFieldStates'>;
87
+ type B = Omit<FormState, 'formFieldStates'>;
88
+
89
+ assert<Equals<A, B>>();
90
+ }
91
+
103
92
  export type FormAction =
104
93
  | {
105
94
  action: 'update';
@@ -114,1389 +103,70 @@ export type FormAction =
114
103
  fieldIndex: number | undefined;
115
104
  };
116
105
 
117
- @Injectable({ providedIn: 'root' })
118
- export class UserProfileFormService {
119
- private kcContext: KcContextLike =
120
- inject<Extract<KcContext, { pageId: 'register.ftl' }>>(KC_LOGIN_CONTEXT);
121
- private i18n = inject<I18n>(LOGIN_I18N);
122
- private doMakeUserConfirmPassword = inject(DO_MAKE_USER_CONFIRM_PASSWORD);
123
- private loginResourceInjectorService = inject(LoginResourceInjectorService);
124
- private sanitizer: DomSanitizer = inject(DomSanitizer);
125
- private initialState: internal.State = (() => {
126
- const attributes: Attribute[] = (() => {
127
- mock_user_profile_attributes_for_older_keycloak_versions: {
128
- if (
129
- 'profile' in this.kcContext &&
130
- 'attributesByName' in this.kcContext.profile &&
131
- Object.keys(this.kcContext.profile.attributesByName).length !== 0
132
- ) {
133
- break mock_user_profile_attributes_for_older_keycloak_versions;
134
- }
135
-
136
- if (
137
- 'register' in this.kcContext &&
138
- this.kcContext.register instanceof Object &&
139
- 'formData' in this.kcContext.register
140
- ) {
141
- //NOTE: Handle legacy register.ftl page
142
- return (['firstName', 'lastName', 'email', 'username'] as const)
143
- .filter(name =>
144
- name !== 'username'
145
- ? true
146
- : !this.kcContext.realm.registrationEmailAsUsername
147
- )
148
- .map(name =>
149
- id<Attribute>({
150
- name: name,
151
- displayName: id<`\${${MessageKey_defaultSet}}`>(
152
- `\${${name}}`
153
- ),
154
- required: true,
155
- value:
156
- (this.kcContext as any).register.formData[name] ?? '',
157
- html5DataAnnotations: {},
158
- readOnly: false,
159
- validators: {},
160
- annotations: {},
161
- autocomplete: (() => {
162
- switch (name) {
163
- case 'email':
164
- return 'email';
165
- case 'username':
166
- return 'username';
167
- default:
168
- return undefined;
169
- }
170
- })()
171
- })
172
- );
173
- }
174
-
175
- if ('user' in this.kcContext && this.kcContext.user instanceof Object) {
176
- //NOTE: Handle legacy login-update-profile.ftl
177
- return (['username', 'email', 'firstName', 'lastName'] as const)
178
- .filter(name =>
179
- name !== 'username'
180
- ? true
181
- : (this.kcContext as any).user.editUsernameAllowed
182
- )
183
- .map(name =>
184
- id<Attribute>({
185
- name: name,
186
- displayName: id<`\${${MessageKey_defaultSet}}`>(
187
- `\${${name}}`
188
- ),
189
- required: true,
190
- value: (this.kcContext as any).user[name] ?? '',
191
- html5DataAnnotations: {},
192
- readOnly: false,
193
- validators: {},
194
- annotations: {},
195
- autocomplete: (() => {
196
- switch (name) {
197
- case 'email':
198
- return 'email';
199
- case 'username':
200
- return 'username';
201
- default:
202
- return undefined;
203
- }
204
- })()
205
- })
206
- );
207
- }
208
-
209
- if ('email' in this.kcContext && this.kcContext.email instanceof Object) {
210
- //NOTE: Handle legacy update-email.ftl
211
- return [
212
- id<Attribute>({
213
- name: 'email',
214
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
215
- required: true,
216
- value: (this.kcContext.email as any).value ?? '',
217
- html5DataAnnotations: {},
218
- readOnly: false,
219
- validators: {},
220
- annotations: {},
221
- autocomplete: 'email'
222
- })
223
- ];
224
- }
225
-
226
- assert(false, 'Unable to mock user profile from the current kcContext');
227
- }
228
-
229
- return Object.values(this.kcContext.profile.attributesByName).map(
230
- structuredCloneButFunctions
231
- );
232
- })();
233
- // Retro-compatibility and consistency patches
234
- attributes.forEach(attribute => {
235
- patch_legacy_group: {
236
- if (typeof attribute.group !== 'string') {
237
- break patch_legacy_group;
238
- }
239
-
240
- const {
241
- group,
242
- groupDisplayHeader,
243
- groupDisplayDescription,
244
- groupAnnotations
245
- } = attribute as Attribute & {
246
- group: string;
247
- groupDisplayHeader?: string;
248
- groupDisplayDescription?: string;
249
- groupAnnotations: Record<string, string>;
250
- };
251
-
252
- delete attribute.group;
253
- // @ts-expect-error
254
- delete attribute.groupDisplayHeader;
255
- // @ts-expect-error
256
- delete attribute.groupDisplayDescription;
257
- // @ts-expect-error
258
- delete attribute.groupAnnotations;
259
-
260
- if (group === '') {
261
- break patch_legacy_group;
262
- }
263
-
264
- attribute.group = {
265
- name: group,
266
- displayHeader: groupDisplayHeader,
267
- displayDescription: groupDisplayDescription,
268
- annotations: groupAnnotations,
269
- html5DataAnnotations: {}
270
- };
271
- }
272
-
273
- // Attributes with options rendered by default as select inputs
274
- if (
275
- attribute.validators.options !== undefined &&
276
- attribute.annotations.inputType === undefined
277
- ) {
278
- attribute.annotations.inputType = 'select';
279
- }
280
-
281
- // Consistency patch on values/value property
282
- {
283
- if (this.getIsMultivaluedSingleField({ attribute })) {
284
- attribute.multivalued = true;
285
- }
286
-
287
- if (attribute.multivalued) {
288
- attribute.values ??=
289
- attribute.value !== undefined ? [attribute.value] : [];
290
- delete attribute.value;
291
- } else {
292
- attribute.value ??= attribute.values?.[0];
293
- delete attribute.values;
294
- }
295
- }
296
- });
297
- add_password_and_password_confirm: {
298
- if (!this.kcContext.passwordRequired) {
299
- break add_password_and_password_confirm;
300
- }
301
-
302
- attributes.forEach((attribute, i) => {
303
- if (
304
- attribute.name !==
305
- (this.kcContext.realm.registrationEmailAsUsername
306
- ? 'email'
307
- : 'username')
308
- ) {
309
- // NOTE: We want to add password and password-confirm after the field that identifies the user.
310
- // It's either email or username.
311
- return;
312
- }
313
-
314
- attributes.splice(
315
- i + 1,
316
- 0,
317
- {
318
- name: 'password',
319
- displayName: id<`\${${MessageKey_defaultSet}}`>('${password}'),
320
- required: true,
321
- readOnly: false,
322
- validators: {},
323
- annotations: {},
324
- autocomplete: 'new-password',
325
- html5DataAnnotations: {}
326
- },
327
- {
328
- name: 'password-confirm',
329
- displayName:
330
- id<`\${${MessageKey_defaultSet}}`>('${passwordConfirm}'),
331
- required: true,
332
- readOnly: false,
333
- validators: {},
334
- annotations: {},
335
- html5DataAnnotations: {},
336
- autocomplete: 'new-password'
337
- }
338
- );
339
- });
340
- }
341
- const initialFormFieldState: {
342
- attribute: Attribute;
343
- valueOrValues: string | string[];
344
- }[] = [];
345
-
346
- for (const attribute of attributes) {
347
- handle_multi_valued_attribute: {
348
- if (!attribute.multivalued) {
349
- break handle_multi_valued_attribute;
350
- }
351
-
352
- const values = attribute.values?.length ? attribute.values : [''];
353
-
354
- apply_validator_min_range: {
355
- if (this.getIsMultivaluedSingleField({ attribute })) {
356
- break apply_validator_min_range;
357
- }
106
+ {
107
+ type A = FormAction;
108
+ type B = reactlessApi.FormAction;
358
109
 
359
- const validator = attribute.validators.multivalued;
360
-
361
- if (validator === undefined) {
362
- break apply_validator_min_range;
363
- }
364
-
365
- const { min: minStr } = validator;
366
-
367
- if (!minStr) {
368
- break apply_validator_min_range;
369
- }
370
-
371
- const min = parseInt(`${minStr}`);
372
-
373
- for (let index = values.length; index < min; index++) {
374
- values.push('');
375
- }
376
- }
377
-
378
- initialFormFieldState.push({
379
- attribute,
380
- valueOrValues: values
381
- });
382
-
383
- continue;
384
- }
385
-
386
- initialFormFieldState.push({
387
- attribute,
388
- valueOrValues: attribute.value ?? ''
389
- });
390
- }
391
-
392
- const initialState: internal.State = {
393
- formFieldStates: initialFormFieldState.map(
394
- ({ attribute, valueOrValues }) => ({
395
- attribute,
396
- errors: this.getErrors({
397
- attributeName: attribute.name,
398
- formFieldStates: initialFormFieldState
399
- }),
400
- hasLostFocusAtLeastOnce:
401
- valueOrValues instanceof Array &&
402
- !this.getIsMultivaluedSingleField({ attribute })
403
- ? valueOrValues.map(() => false)
404
- : false,
405
- valueOrValues: valueOrValues
406
- })
407
- )
408
- };
409
- return initialState;
410
- })();
110
+ assert<Equals<A, B>>();
111
+ }
411
112
 
412
- private state: WritableSignal<internal.State> = signal(this.initialState);
113
+ export type KcContextLike = reactlessApi.KcContextLike;
413
114
 
414
- formState: Signal<FormState> = computed(() => {
415
- const state: internal.State = this.state();
416
- return {
417
- formFieldStates: state.formFieldStates.map(
418
- ({
419
- errors,
420
- hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr,
421
- attribute,
422
- ...valueOrValuesWrap
423
- }) => ({
424
- displayableErrors: errors.filter(error => {
425
- const hasLostFocusAtLeastOnce =
426
- typeof hasLostFocusAtLeastOnceOrArr === 'boolean'
427
- ? hasLostFocusAtLeastOnceOrArr
428
- : error.fieldIndex !== undefined
429
- ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
430
- : hasLostFocusAtLeastOnceOrArr[
431
- hasLostFocusAtLeastOnceOrArr.length - 1
432
- ];
433
- let value = false;
434
- switch (error.source.type) {
435
- case 'server':
436
- value = true;
437
- break;
438
- case 'other':
439
- switch (error.source.rule) {
440
- case 'requiredField':
441
- value = hasLostFocusAtLeastOnce;
442
- break;
443
- case 'passwordConfirmMatchesPassword':
444
- value = hasLostFocusAtLeastOnce;
445
- break;
446
- }
447
- // assert<Equals<typeof error.source.rule, never>>(false);
448
- break;
449
- case 'passwordPolicy':
450
- switch (error.source.name) {
451
- case 'length':
452
- value = hasLostFocusAtLeastOnce;
453
- break;
454
- case 'digits':
455
- value = hasLostFocusAtLeastOnce;
456
- break;
457
- case 'lowerCase':
458
- value = hasLostFocusAtLeastOnce;
459
- break;
460
- case 'upperCase':
461
- value = hasLostFocusAtLeastOnce;
462
- break;
463
- case 'specialChars':
464
- value = hasLostFocusAtLeastOnce;
465
- break;
466
- case 'notUsername':
467
- value = true;
468
- break;
469
- case 'notEmail':
470
- value = true;
471
- break;
472
- }
473
- // assert<Equals<typeof error.source, never>>(false);
474
- break;
475
- case 'validator':
476
- switch (error.source.name) {
477
- case 'length':
478
- value = hasLostFocusAtLeastOnce;
479
- break;
480
- case 'pattern':
481
- value = hasLostFocusAtLeastOnce;
482
- break;
483
- case 'email':
484
- value = hasLostFocusAtLeastOnce;
485
- break;
486
- case 'integer':
487
- value = hasLostFocusAtLeastOnce;
488
- break;
489
- case 'multivalued':
490
- value = hasLostFocusAtLeastOnce;
491
- break;
492
- case 'options':
493
- value = hasLostFocusAtLeastOnce;
494
- break;
495
- }
496
- // assert<Equals<typeof error.source, never>>(false);
497
- break;
498
- }
499
- return value;
500
- }),
501
- attribute,
502
- ...valueOrValuesWrap
503
- })
504
- ),
505
- isFormSubmittable: state.formFieldStates.every(
506
- ({ errors }) => errors.length === 0
507
- )
508
- };
509
- });
115
+ export type I18nLike = Pick<I18n, 'advancedMsgStr'>;
510
116
 
117
+ @Injectable({ providedIn: 'root' })
118
+ export class UserProfileFormService implements OnDestroy {
119
+ #kcContext = inject<KcContextLike>(KC_LOGIN_CONTEXT);
120
+ #i18n = inject<I18nLike>(LOGIN_I18N);
121
+ #doMakeUserConfirmPassword = inject(DO_MAKE_USER_CONFIRM_PASSWORD);
122
+ #domSanitizer = inject(DomSanitizer);
123
+ #internal_formState$: BehaviorSubject<reactlessApi.FormState>;
124
+ #unsubscribe: (() => void) | undefined;
125
+ public formState$: Observable<FormState>;
126
+ public dispatchFormAction: (action: FormAction) => void;
511
127
  constructor() {
512
- this.loginResourceInjectorService.insertAdditionalScripts(
513
- Object.keys(this.kcContext.profile?.html5DataAnnotations ?? {})
514
- .filter(key => key !== 'kcMultivalued' && key !== 'kcNumberFormat') // NOTE: Keycloakify handles it.
515
- .map(key => ({
516
- type: 'module',
517
- src: `${this.kcContext.url.resourcesPath}/js/${key}.js`,
518
- id: `${this.kcContext.url.resourcesPath}/js/${key}.js`
519
- }))
128
+ const api = reactlessApi.getUserProfileApi({
129
+ kcContext: this.#kcContext,
130
+ doMakeUserConfirmPassword: this.#doMakeUserConfirmPassword
131
+ });
132
+ this.#internal_formState$ = new BehaviorSubject<reactlessApi.FormState>(
133
+ api.getFormState()
520
134
  );
521
- }
522
-
523
- public dispatchFormAction(formAction: FormAction) {
524
- if (!formAction) return;
525
- const state = this.state();
526
- const formFieldState = state.formFieldStates.find(
527
- ({ attribute }) => attribute.name === formAction.name
135
+ this.#unsubscribe = api.subscribeToFormState(() => {
136
+ this.#internal_formState$.next(api.getFormState());
137
+ }).unsubscribe;
138
+ this.formState$ = this.#internal_formState$.asObservable().pipe(
139
+ takeUntilDestroyed(),
140
+ map(formState_reactless => ({
141
+ isFormSubmittable: formState_reactless.isFormSubmittable,
142
+ formFieldStates: formState_reactless.formFieldStates.map(
143
+ formFieldState_reactless => ({
144
+ attribute: formFieldState_reactless.attribute,
145
+ valueOrValues: formFieldState_reactless.valueOrValues,
146
+ displayableErrors: formFieldState_reactless.displayableErrors.map(
147
+ (formFieldError_reactless, i) => ({
148
+ errorMessage: this.#domSanitizer.bypassSecurityTrustHtml(
149
+ this.#i18n.advancedMsgStr(
150
+ ...formFieldError_reactless.advancedMsgArgs
151
+ )
152
+ ),
153
+ errorMessageStr: this.#i18n.advancedMsgStr(
154
+ ...formFieldError_reactless.advancedMsgArgs
155
+ ),
156
+ source: formFieldError_reactless.source,
157
+ fieldIndex: formFieldError_reactless.fieldIndex
158
+ })
159
+ )
160
+ })
161
+ )
162
+ }))
528
163
  );
529
- assert(formFieldState !== undefined);
530
- switch (formAction.action) {
531
- case 'update':
532
- formFieldState.valueOrValues = formAction.valueOrValues;
533
-
534
- apply_formatters: {
535
- const { attribute } = formFieldState;
536
-
537
- const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
538
-
539
- if (!kcNumberFormat) {
540
- break apply_formatters;
541
- }
542
-
543
- if (formFieldState.valueOrValues instanceof Array) {
544
- formFieldState.valueOrValues = formFieldState.valueOrValues.map(
545
- value => formatNumber(value, kcNumberFormat)
546
- );
547
- } else {
548
- formFieldState.valueOrValues = formatNumber(
549
- formFieldState.valueOrValues,
550
- kcNumberFormat
551
- );
552
- }
553
- }
554
-
555
- formFieldState.errors = this.getErrors({
556
- attributeName: formAction.name,
557
- formFieldStates: state.formFieldStates
558
- });
559
-
560
- simulate_focus_lost: {
561
- const { displayErrorsImmediately = false } = formAction;
562
-
563
- if (!displayErrorsImmediately) {
564
- break simulate_focus_lost;
565
- }
566
-
567
- for (const fieldIndex of formAction.valueOrValues instanceof Array
568
- ? formAction.valueOrValues.map((...[, index]) => index)
569
- : [undefined]) {
570
- this.dispatchFormAction({
571
- action: 'focus lost',
572
- name: formAction.name,
573
- fieldIndex
574
- });
575
- }
576
- }
577
-
578
- update_password_confirm: {
579
- if (this.doMakeUserConfirmPassword) {
580
- break update_password_confirm;
581
- }
582
-
583
- if (formAction.name !== 'password') {
584
- break update_password_confirm;
585
- }
586
-
587
- this.dispatchFormAction({
588
- action: 'update',
589
- name: 'password-confirm',
590
- valueOrValues: formAction.valueOrValues,
591
- displayErrorsImmediately: formAction.displayErrorsImmediately
592
- });
593
- }
594
-
595
- trigger_password_confirm_validation_on_password_change: {
596
- if (!this.doMakeUserConfirmPassword) {
597
- break trigger_password_confirm_validation_on_password_change;
598
- }
599
-
600
- if (formAction.name !== 'password') {
601
- break trigger_password_confirm_validation_on_password_change;
602
- }
603
-
604
- this.dispatchFormAction({
605
- action: 'update',
606
- name: 'password-confirm',
607
- valueOrValues: (() => {
608
- const formFieldState = state.formFieldStates.find(
609
- ({ attribute }) => attribute.name === 'password-confirm'
610
- );
611
-
612
- assert(formFieldState !== undefined);
613
-
614
- return formFieldState.valueOrValues;
615
- })(),
616
- displayErrorsImmediately: formAction.displayErrorsImmediately
617
- });
618
- }
619
-
620
- break;
621
- case 'focus lost':
622
- if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
623
- const { fieldIndex } = formAction;
624
- assert(fieldIndex !== undefined);
625
- formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
626
- break;
627
- }
628
-
629
- formFieldState.hasLostFocusAtLeastOnce = true;
630
- break;
631
- }
632
- this.state.update(state => ({
633
- ...state,
634
- formFieldStates: state.formFieldStates.map(f => {
635
- if (f.attribute === formFieldState.attribute) return formFieldState;
636
- return f;
637
- })
638
- }));
164
+ this.dispatchFormAction = api.dispatchFormAction;
639
165
  }
640
166
 
641
- private getIsMultivaluedSingleField(params: { attribute: Attribute }) {
642
- const { attribute } = params;
643
-
644
- return attribute.annotations.inputType?.startsWith('multiselect') ?? false;
645
- }
646
-
647
- private getErrors(params: {
648
- attributeName: string;
649
- formFieldStates: {
650
- attribute: Attribute;
651
- valueOrValues: string | string[];
652
- }[];
653
- }): FormFieldError[] {
654
- const { messagesPerField, passwordPolicies } = this.kcContext;
655
-
656
- const { msgStr, advancedMsgStr } = this.i18n;
657
- const { attributeName, formFieldStates } = params;
658
-
659
- const formFieldState = formFieldStates.find(
660
- ({ attribute }) => attribute.name === attributeName
661
- );
662
-
663
- assert(formFieldState !== undefined);
664
-
665
- const { attribute } = formFieldState;
666
-
667
- const valueOrValues = (() => {
668
- let { valueOrValues } = formFieldState;
669
-
670
- unFormat_number: {
671
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
672
-
673
- if (!kcNumberUnFormat) {
674
- break unFormat_number;
675
- }
676
-
677
- if (valueOrValues instanceof Array) {
678
- valueOrValues = valueOrValues.map(value =>
679
- formatNumber(value, kcNumberUnFormat)
680
- );
681
- } else {
682
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
683
- }
684
- }
685
-
686
- return valueOrValues;
687
- })();
688
-
689
- assert(attribute !== undefined);
690
-
691
- server_side_error: {
692
- if (attribute.multivalued) {
693
- const defaultValues = attribute.values?.length ? attribute.values : [''];
694
-
695
- assert(valueOrValues instanceof Array);
696
-
697
- const values = valueOrValues;
698
-
699
- if (
700
- JSON.stringify(defaultValues) !==
701
- JSON.stringify(values.slice(0, defaultValues.length))
702
- ) {
703
- break server_side_error;
704
- }
705
- } else {
706
- const defaultValue = attribute.value ?? '';
707
-
708
- assert(typeof valueOrValues === 'string');
709
-
710
- const value = valueOrValues;
711
-
712
- if (defaultValue !== value) {
713
- break server_side_error;
714
- }
715
- }
716
-
717
- let doesErrorExist: boolean;
718
-
719
- try {
720
- doesErrorExist = messagesPerField.existsError(attributeName);
721
- } catch {
722
- break server_side_error;
723
- }
724
-
725
- if (!doesErrorExist) {
726
- break server_side_error;
727
- }
728
-
729
- const errorMessageStr = messagesPerField.get(attributeName);
730
-
731
- return [
732
- {
733
- errorMessageStr,
734
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
735
- `<span>${errorMessageStr}</span>`
736
- ),
737
- fieldIndex: undefined,
738
- source: {
739
- type: 'server'
740
- }
741
- }
742
- ];
743
- }
744
-
745
- handle_multi_valued_multi_fields: {
746
- if (!attribute.multivalued) {
747
- break handle_multi_valued_multi_fields;
748
- }
749
-
750
- if (this.getIsMultivaluedSingleField({ attribute })) {
751
- break handle_multi_valued_multi_fields;
752
- }
753
-
754
- assert(valueOrValues instanceof Array);
755
-
756
- const values = valueOrValues;
757
-
758
- const errors = values
759
- .map((...[, index]) => {
760
- const specificValueErrors = this.getErrors({
761
- attributeName,
762
- formFieldStates: formFieldStates.map(formFieldState => {
763
- if (formFieldState.attribute.name === attributeName) {
764
- assert(formFieldState.valueOrValues instanceof Array);
765
- return {
766
- attribute: {
767
- ...attribute,
768
- annotations: {
769
- ...attribute.annotations,
770
- inputType: undefined
771
- },
772
- multivalued: false
773
- },
774
- valueOrValues: formFieldState.valueOrValues[index]
775
- };
776
- }
777
-
778
- return formFieldState;
779
- })
780
- });
781
-
782
- return specificValueErrors
783
- .filter(error => {
784
- if (
785
- error.source.type === 'other' &&
786
- error.source.rule === 'requiredField'
787
- ) {
788
- return false;
789
- }
790
-
791
- return true;
792
- })
793
- .map(
794
- (error): FormFieldError => ({
795
- ...error,
796
- fieldIndex: index
797
- })
798
- );
799
- })
800
- .reduce((acc, errors) => [...acc, ...errors], []);
801
-
802
- required_field: {
803
- if (!attribute.required) {
804
- break required_field;
805
- }
806
-
807
- if (values.every(value => value !== '')) {
808
- break required_field;
809
- }
810
-
811
- const msgArgs = ['error-user-attribute-required'] as const;
812
-
813
- errors.push({
814
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
815
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
816
- ),
817
- errorMessageStr: msgStr(...msgArgs),
818
- fieldIndex: undefined,
819
- source: {
820
- type: 'other',
821
- rule: 'requiredField'
822
- }
823
- });
824
- }
825
-
826
- return errors;
827
- }
828
-
829
- handle_multi_valued_single_field: {
830
- if (!attribute.multivalued) {
831
- break handle_multi_valued_single_field;
832
- }
833
-
834
- if (!this.getIsMultivaluedSingleField({ attribute })) {
835
- break handle_multi_valued_single_field;
836
- }
837
-
838
- const validatorName = 'multivalued';
839
-
840
- const validator = attribute.validators[validatorName];
841
-
842
- if (validator === undefined) {
843
- return [];
844
- }
845
-
846
- const { min: minStr } = validator;
847
-
848
- const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
849
-
850
- assert(!isNaN(min));
851
-
852
- const { max: maxStr } = validator;
853
-
854
- const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
855
-
856
- assert(!isNaN(max));
857
-
858
- assert(valueOrValues instanceof Array);
859
-
860
- const values = valueOrValues;
861
-
862
- if (min <= values.length && values.length <= max) {
863
- return [];
864
- }
865
-
866
- const msgArgs = [
867
- 'error-invalid-multivalued-size',
868
- `${min}`,
869
- `${max}`
870
- ] as const;
871
-
872
- return [
873
- {
874
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
875
- `<span data-key="0">${msgStr(...msgArgs)}</span>`
876
- ),
877
- errorMessageStr: msgStr(...msgArgs),
878
- fieldIndex: undefined,
879
- source: {
880
- type: 'validator',
881
- name: validatorName
882
- }
883
- }
884
- ];
885
- }
886
-
887
- assert(typeof valueOrValues === 'string');
888
-
889
- const value = valueOrValues;
890
-
891
- const errors: FormFieldError[] = [];
892
-
893
- check_password_policies: {
894
- if (attributeName !== 'password') {
895
- break check_password_policies;
896
- }
897
-
898
- if (passwordPolicies === undefined) {
899
- break check_password_policies;
900
- }
901
-
902
- check_password_policy_x: {
903
- const policyName = 'length';
904
-
905
- const policy = passwordPolicies[policyName];
906
-
907
- if (!policy) {
908
- break check_password_policy_x;
909
- }
910
-
911
- const minLength = policy;
912
-
913
- if (value.length >= minLength) {
914
- break check_password_policy_x;
915
- }
916
-
917
- const msgArgs = [
918
- 'invalidPasswordMinLengthMessage',
919
- `${minLength}`
920
- ] as const;
921
-
922
- errors.push({
923
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
924
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
925
- ),
926
- errorMessageStr: msgStr(...msgArgs),
927
- fieldIndex: undefined,
928
- source: {
929
- type: 'passwordPolicy',
930
- name: policyName
931
- }
932
- });
933
- }
934
-
935
- check_password_policy_x: {
936
- const policyName = 'digits';
937
-
938
- const policy = passwordPolicies[policyName];
939
-
940
- if (!policy) {
941
- break check_password_policy_x;
942
- }
943
-
944
- const minNumberOfDigits = policy;
945
-
946
- if (
947
- value.split('').filter(char => !isNaN(parseInt(char))).length >=
948
- minNumberOfDigits
949
- ) {
950
- break check_password_policy_x;
951
- }
952
-
953
- const msgArgs = [
954
- 'invalidPasswordMinDigitsMessage',
955
- `${minNumberOfDigits}`
956
- ] as const;
957
-
958
- errors.push({
959
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
960
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
961
- ),
962
- errorMessageStr: msgStr(...msgArgs),
963
- fieldIndex: undefined,
964
- source: {
965
- type: 'passwordPolicy',
966
- name: policyName
967
- }
968
- });
969
- }
970
-
971
- check_password_policy_x: {
972
- const policyName = 'lowerCase';
973
-
974
- const policy = passwordPolicies[policyName];
975
-
976
- if (!policy) {
977
- break check_password_policy_x;
978
- }
979
-
980
- const minNumberOfLowerCaseChar = policy;
981
-
982
- if (
983
- value
984
- .split('')
985
- .filter(
986
- char =>
987
- char === char.toLowerCase() && char !== char.toUpperCase()
988
- ).length >= minNumberOfLowerCaseChar
989
- ) {
990
- break check_password_policy_x;
991
- }
992
-
993
- const msgArgs = [
994
- 'invalidPasswordMinLowerCaseCharsMessage',
995
- `${minNumberOfLowerCaseChar}`
996
- ] as const;
997
-
998
- errors.push({
999
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1000
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1001
- ),
1002
- errorMessageStr: msgStr(...msgArgs),
1003
- fieldIndex: undefined,
1004
- source: {
1005
- type: 'passwordPolicy',
1006
- name: policyName
1007
- }
1008
- });
1009
- }
1010
-
1011
- check_password_policy_x: {
1012
- const policyName = 'upperCase';
1013
-
1014
- const policy = passwordPolicies[policyName];
1015
-
1016
- if (!policy) {
1017
- break check_password_policy_x;
1018
- }
1019
-
1020
- const minNumberOfUpperCaseChar = policy;
1021
-
1022
- if (
1023
- value
1024
- .split('')
1025
- .filter(
1026
- char =>
1027
- char === char.toUpperCase() && char !== char.toLowerCase()
1028
- ).length >= minNumberOfUpperCaseChar
1029
- ) {
1030
- break check_password_policy_x;
1031
- }
1032
-
1033
- const msgArgs = [
1034
- 'invalidPasswordMinUpperCaseCharsMessage',
1035
- `${minNumberOfUpperCaseChar}`
1036
- ] as const;
1037
-
1038
- errors.push({
1039
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1040
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1041
- ),
1042
- errorMessageStr: msgStr(...msgArgs),
1043
- fieldIndex: undefined,
1044
- source: {
1045
- type: 'passwordPolicy',
1046
- name: policyName
1047
- }
1048
- });
1049
- }
1050
-
1051
- check_password_policy_x: {
1052
- const policyName = 'specialChars';
1053
-
1054
- const policy = passwordPolicies[policyName];
1055
-
1056
- if (!policy) {
1057
- break check_password_policy_x;
1058
- }
1059
-
1060
- const minNumberOfSpecialChar = policy;
1061
-
1062
- if (
1063
- value.split('').filter(char => !char.match(/[a-zA-Z0-9]/)).length >=
1064
- minNumberOfSpecialChar
1065
- ) {
1066
- break check_password_policy_x;
1067
- }
1068
-
1069
- const msgArgs = [
1070
- 'invalidPasswordMinSpecialCharsMessage',
1071
- `${minNumberOfSpecialChar}`
1072
- ] as const;
1073
-
1074
- errors.push({
1075
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1076
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1077
- ),
1078
- errorMessageStr: msgStr(...msgArgs),
1079
- fieldIndex: undefined,
1080
- source: {
1081
- type: 'passwordPolicy',
1082
- name: policyName
1083
- }
1084
- });
1085
- }
1086
-
1087
- check_password_policy_x: {
1088
- const policyName = 'notUsername';
1089
-
1090
- const notUsername = passwordPolicies[policyName];
1091
-
1092
- if (!notUsername) {
1093
- break check_password_policy_x;
1094
- }
1095
-
1096
- const usernameFormFieldState = formFieldStates.find(
1097
- formFieldState => formFieldState.attribute.name === 'username'
1098
- );
1099
-
1100
- if (!usernameFormFieldState) {
1101
- break check_password_policy_x;
1102
- }
1103
-
1104
- const usernameValue = (() => {
1105
- let { valueOrValues } = usernameFormFieldState;
1106
-
1107
- assert(typeof valueOrValues === 'string');
1108
-
1109
- unFormat_number: {
1110
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
1111
-
1112
- if (!kcNumberUnFormat) {
1113
- break unFormat_number;
1114
- }
1115
-
1116
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
1117
- }
1118
-
1119
- return valueOrValues;
1120
- })();
1121
-
1122
- if (usernameValue === '') {
1123
- break check_password_policy_x;
1124
- }
1125
-
1126
- if (value !== usernameValue) {
1127
- break check_password_policy_x;
1128
- }
1129
-
1130
- const msgArgs = ['invalidPasswordNotUsernameMessage'] as const;
1131
-
1132
- errors.push({
1133
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1134
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1135
- ),
1136
- errorMessageStr: msgStr(...msgArgs),
1137
- fieldIndex: undefined,
1138
- source: {
1139
- type: 'passwordPolicy',
1140
- name: policyName
1141
- }
1142
- });
1143
- }
1144
-
1145
- check_password_policy_x: {
1146
- const policyName = 'notEmail';
1147
-
1148
- const notEmail = passwordPolicies[policyName];
1149
-
1150
- if (!notEmail) {
1151
- break check_password_policy_x;
1152
- }
1153
-
1154
- const emailFormFieldState = formFieldStates.find(
1155
- formFieldState => formFieldState.attribute.name === 'email'
1156
- );
1157
-
1158
- if (!emailFormFieldState) {
1159
- break check_password_policy_x;
1160
- }
1161
-
1162
- assert(typeof emailFormFieldState.valueOrValues === 'string');
1163
-
1164
- {
1165
- const emailValue = emailFormFieldState.valueOrValues;
1166
-
1167
- if (emailValue === '') {
1168
- break check_password_policy_x;
1169
- }
1170
-
1171
- if (value !== emailValue) {
1172
- break check_password_policy_x;
1173
- }
1174
- }
1175
-
1176
- const msgArgs = ['invalidPasswordNotEmailMessage'] as const;
1177
-
1178
- errors.push({
1179
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1180
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1181
- ),
1182
- errorMessageStr: msgStr(...msgArgs),
1183
- fieldIndex: undefined,
1184
- source: {
1185
- type: 'passwordPolicy',
1186
- name: policyName
1187
- }
1188
- });
1189
- }
1190
- }
1191
-
1192
- password_confirm_matches_password: {
1193
- if (attributeName !== 'password-confirm') {
1194
- break password_confirm_matches_password;
1195
- }
1196
-
1197
- const passwordFormFieldState = formFieldStates.find(
1198
- formFieldState => formFieldState.attribute.name === 'password'
1199
- );
1200
-
1201
- assert(passwordFormFieldState !== undefined);
1202
-
1203
- assert(typeof passwordFormFieldState.valueOrValues === 'string');
1204
-
1205
- {
1206
- const passwordValue = passwordFormFieldState.valueOrValues;
1207
-
1208
- if (value === passwordValue) {
1209
- break password_confirm_matches_password;
1210
- }
1211
- }
1212
-
1213
- const msgArgs = ['invalidPasswordConfirmMessage'] as const;
1214
-
1215
- errors.push({
1216
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1217
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1218
- ),
1219
- errorMessageStr: msgStr(...msgArgs),
1220
- fieldIndex: undefined,
1221
- source: {
1222
- type: 'other',
1223
- rule: 'passwordConfirmMatchesPassword'
1224
- }
1225
- });
1226
- }
1227
-
1228
- const { validators } = attribute;
1229
-
1230
- required_field: {
1231
- if (!attribute.required) {
1232
- break required_field;
1233
- }
1234
-
1235
- if (value !== '') {
1236
- break required_field;
1237
- }
1238
-
1239
- const msgArgs = ['error-user-attribute-required'] as const;
1240
-
1241
- errors.push({
1242
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1243
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1244
- ),
1245
- errorMessageStr: msgStr(...msgArgs),
1246
- fieldIndex: undefined,
1247
- source: {
1248
- type: 'other',
1249
- rule: 'requiredField'
1250
- }
1251
- });
1252
- }
1253
-
1254
- validator_x: {
1255
- const validatorName = 'length';
1256
-
1257
- const validator = validators[validatorName];
1258
-
1259
- if (!validator) {
1260
- break validator_x;
1261
- }
1262
-
1263
- const {
1264
- 'ignore.empty.value': ignoreEmptyValue = false,
1265
- max,
1266
- min
1267
- } = validator;
1268
-
1269
- if (ignoreEmptyValue && value === '') {
1270
- break validator_x;
1271
- }
1272
-
1273
- const source: FormFieldError.Source = {
1274
- type: 'validator',
1275
- name: validatorName
1276
- };
1277
-
1278
- if (max && value.length > parseInt(`${max}`)) {
1279
- const msgArgs = ['error-invalid-length-too-long', `${max}`] as const;
1280
-
1281
- errors.push({
1282
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1283
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1284
- ),
1285
- errorMessageStr: msgStr(...msgArgs),
1286
- fieldIndex: undefined,
1287
- source
1288
- });
1289
- }
1290
-
1291
- if (min && value.length < parseInt(`${min}`)) {
1292
- const msgArgs = ['error-invalid-length-too-short', `${min}`] as const;
1293
-
1294
- errors.push({
1295
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1296
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1297
- ),
1298
- errorMessageStr: msgStr(...msgArgs),
1299
- fieldIndex: undefined,
1300
- source
1301
- });
1302
- }
1303
- }
1304
-
1305
- validator_x: {
1306
- const validatorName = 'pattern';
1307
-
1308
- const validator = validators[validatorName];
1309
-
1310
- if (validator === undefined) {
1311
- break validator_x;
1312
- }
1313
-
1314
- const {
1315
- 'ignore.empty.value': ignoreEmptyValue = false,
1316
- pattern,
1317
- 'error-message': errorMessageKey
1318
- } = validator;
1319
-
1320
- if (ignoreEmptyValue && value === '') {
1321
- break validator_x;
1322
- }
1323
-
1324
- if (new RegExp(pattern).test(value)) {
1325
- break validator_x;
1326
- }
1327
-
1328
- const msgArgs = [
1329
- errorMessageKey ?? id<MessageKey_defaultSet>('shouldMatchPattern'),
1330
- pattern
1331
- ] as const;
1332
-
1333
- errors.push({
1334
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1335
- `<span data-key="${attributeName}-${errors.length}">${advancedMsgStr(...msgArgs)}</span>`
1336
- ),
1337
- errorMessageStr: advancedMsgStr(...msgArgs),
1338
- fieldIndex: undefined,
1339
- source: {
1340
- type: 'validator',
1341
- name: validatorName
1342
- }
1343
- });
1344
- }
1345
-
1346
- validator_x: {
1347
- {
1348
- const lastError = errors[errors.length - 1];
1349
- if (
1350
- lastError !== undefined &&
1351
- lastError.source.type === 'validator' &&
1352
- lastError.source.name === 'pattern'
1353
- ) {
1354
- break validator_x;
1355
- }
1356
- }
1357
-
1358
- const validatorName = 'email';
1359
-
1360
- const validator = validators[validatorName];
1361
-
1362
- if (validator === undefined) {
1363
- break validator_x;
1364
- }
1365
-
1366
- const { 'ignore.empty.value': ignoreEmptyValue = false } = validator;
1367
-
1368
- if (ignoreEmptyValue && value === '') {
1369
- break validator_x;
1370
- }
1371
-
1372
- if (emailRegexp.test(value)) {
1373
- break validator_x;
1374
- }
1375
-
1376
- const msgArgs = [id<MessageKey_defaultSet>('invalidEmailMessage')] as const;
1377
-
1378
- errors.push({
1379
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1380
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1381
- ),
1382
- errorMessageStr: msgStr(...msgArgs),
1383
- fieldIndex: undefined,
1384
- source: {
1385
- type: 'validator',
1386
- name: validatorName
1387
- }
1388
- });
1389
- }
1390
-
1391
- validator_x: {
1392
- const validatorName = 'integer';
1393
-
1394
- const validator = validators[validatorName];
1395
-
1396
- if (validator === undefined) {
1397
- break validator_x;
1398
- }
1399
-
1400
- const {
1401
- 'ignore.empty.value': ignoreEmptyValue = false,
1402
- max,
1403
- min
1404
- } = validator;
1405
-
1406
- if (ignoreEmptyValue && value === '') {
1407
- break validator_x;
1408
- }
1409
-
1410
- const intValue = parseInt(value);
1411
-
1412
- const source: FormFieldError.Source = {
1413
- type: 'validator',
1414
- name: validatorName
1415
- };
1416
-
1417
- if (isNaN(intValue)) {
1418
- const msgArgs = ['mustBeAnInteger'] as const;
1419
-
1420
- errors.push({
1421
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1422
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1423
- ),
1424
- errorMessageStr: msgStr(...msgArgs),
1425
- fieldIndex: undefined,
1426
- source
1427
- });
1428
-
1429
- break validator_x;
1430
- }
1431
-
1432
- if (max && intValue > parseInt(`${max}`)) {
1433
- const msgArgs = ['error-number-out-of-range-too-big', `${max}`] as const;
1434
-
1435
- errors.push({
1436
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1437
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1438
- ),
1439
- errorMessageStr: msgStr(...msgArgs),
1440
- fieldIndex: undefined,
1441
- source
1442
- });
1443
-
1444
- break validator_x;
1445
- }
1446
-
1447
- if (min && intValue < parseInt(`${min}`)) {
1448
- const msgArgs = [
1449
- 'error-number-out-of-range-too-small',
1450
- `${min}`
1451
- ] as const;
1452
-
1453
- errors.push({
1454
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1455
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1456
- ),
1457
- errorMessageStr: msgStr(...msgArgs),
1458
- fieldIndex: undefined,
1459
- source
1460
- });
1461
-
1462
- break validator_x;
1463
- }
1464
- }
1465
-
1466
- validator_x: {
1467
- const validatorName = 'options';
1468
-
1469
- const validator = validators[validatorName];
1470
-
1471
- if (validator === undefined) {
1472
- break validator_x;
1473
- }
1474
-
1475
- if (value === '') {
1476
- break validator_x;
1477
- }
1478
-
1479
- if (validator.options.indexOf(value) >= 0) {
1480
- break validator_x;
1481
- }
1482
-
1483
- const msgArgs = [id<MessageKey_defaultSet>('notAValidOption')] as const;
1484
-
1485
- errors.push({
1486
- errorMessage: this.sanitizer.bypassSecurityTrustHtml(
1487
- `<span data-key="${attributeName}-${errors.length}">${msgStr(...msgArgs)}</span>`
1488
- ),
1489
- errorMessageStr: msgStr(...msgArgs),
1490
- fieldIndex: undefined,
1491
- source: {
1492
- type: 'validator',
1493
- name: validatorName
1494
- }
1495
- });
1496
- }
1497
-
1498
- //TODO: Implement missing validators. See Validators type definition.
1499
-
1500
- return errors;
167
+ ngOnDestroy(): void {
168
+ this.#internal_formState$.complete();
169
+ this.#internal_formState$.unsubscribe();
170
+ if (this.#unsubscribe) this.#unsubscribe();
1501
171
  }
1502
172
  }