keycloakify 11.3.8 → 11.3.9-rc.1

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