keycloak_theme/src/login/lib/useUserProfileForm.tsx

1238 lines
47 KiB
TypeScript
Raw Normal View History

2023-03-18 06:14:05 +01:00
import "keycloakify/tools/Array.prototype.every";
import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
2023-03-18 06:14:05 +01:00
import { id } from "tsafe/id";
2023-03-19 23:12:45 +01:00
import type { MessageKey } from "keycloakify/login/i18n/i18n";
import type { Attribute, Validators } from "keycloakify/login/kcContext/KcContext";
2023-03-18 06:14:05 +01:00
import { useConstCallback } from "keycloakify/tools/useConstCallback";
import { emailRegexp } from "keycloakify/tools/emailRegExp";
import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/KcContext";
2024-04-21 08:12:25 +02:00
import { assert, type Equals } from "tsafe/assert";
import { formatNumber } from "keycloakify/tools/formatNumber";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { I18n } from "../i18n";
2024-04-21 08:12:25 +02:00
export type FormFieldError = {
errorMessage: JSX.Element;
errorMessageStr: string;
source: FormFieldError.Source;
fieldIndex: number | undefined;
2024-04-21 08:12:25 +02:00
};
export namespace FormFieldError {
export type Source = Source.Validator | Source.PasswordPolicy | Source.Server | Source.Other;
export namespace Source {
export type Validator = {
type: "validator";
name: keyof Validators;
};
export type PasswordPolicy = {
type: "passwordPolicy";
name: keyof PasswordPolicies;
};
export type Server = {
type: "server";
};
export type Other = {
type: "other";
rule: "passwordConfirmMatchesPassword" | "requiredField";
};
}
}
export type FormFieldState = {
attribute: Attribute;
displayableErrors: FormFieldError[];
valueOrValues: string | string[];
};
2024-04-21 08:12:25 +02:00
export type FormState = {
isFormSubmittable: boolean;
formFieldStates: FormFieldState[];
};
export type FormAction =
| {
action: "update";
2024-04-21 08:12:25 +02:00
name: string;
valueOrValues: string | string[];
2024-04-21 08:12:25 +02:00
}
| {
action: "focus lost";
2024-04-21 08:12:25 +02:00
name: string;
fieldIndex: number | undefined;
2024-04-21 08:12:25 +02:00
};
export type KcContextLike = {
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
profile: {
attributes: Attribute[];
html5DataAnnotations?: Record<string, string>;
2023-03-18 06:14:05 +01:00
};
passwordRequired?: boolean;
realm: { registrationEmailAsUsername: boolean };
passwordPolicies?: PasswordPolicies;
url: {
resourcesPath: string;
};
};
export type ParamsOfUseUserProfileForm = {
kcContext: KcContextLike;
2023-03-18 06:14:05 +01:00
i18n: I18n;
2024-04-27 19:09:22 +02:00
doMakeUserConfirmPassword: boolean;
2024-04-21 08:12:25 +02:00
};
2023-03-18 06:14:05 +01:00
export type ReturnTypeOfUseUserProfileForm = {
2024-04-21 08:12:25 +02:00
formState: FormState;
dispatchFormAction: Dispatch<FormAction>;
};
namespace internal {
export type FormFieldState = {
attribute: Attribute;
errors: FormFieldError[];
hasLostFocusAtLeastOnce: boolean | boolean[];
valueOrValues: string | string[];
};
export type State = {
formFieldStates: FormFieldState[];
};
}
const { useInsertScriptTags } = createUseInsertScriptTags();
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
2024-04-27 19:09:22 +02:00
const { kcContext, i18n, doMakeUserConfirmPassword } = params;
2024-04-21 08:12:25 +02:00
const { insertScriptTags } = useInsertScriptTags({
"scriptTags": Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
.map(key => ({
"type": "module",
"src": `${kcContext.url.resourcesPath}/js/${key}.js`
}))
});
useEffect(() => {
insertScriptTags();
}, []);
const { getErrors } = useGetErrors({
kcContext,
i18n
});
2024-04-21 08:12:25 +02:00
const initialState = useMemo((): internal.State => {
// NOTE: We don't use te kcContext.profile.attributes directly because
// they don't includes the password and password confirm fields and we want to add them.
// Also, we want to polyfill the attributes for older Keycloak version before User Profile was introduced.
// Finally we want to patch the changes made by Keycloak on the attributes format so we have an homogeneous
// attributes format to work with.
const syntheticAttributes = (() => {
const syntheticAttributes: Attribute[] = [];
const attributes = (() => {
retrocompat_patch: {
if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
break retrocompat_patch;
}
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
//NOTE: Handle legacy register.ftl page
return (["firstName", "lastName", "email", "username"] as const)
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
.map(name =>
id<Attribute>({
"name": name,
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
"required": true,
2024-05-08 19:24:18 +02:00
"value": (kcContext.register as any).formData[name] ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
if ("user" in kcContext && kcContext.user instanceof Object) {
//NOTE: Handle legacy login-update-profile.ftl
return (["username", "email", "firstName", "lastName"] as const)
2024-05-08 19:24:18 +02:00
.filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
.map(name =>
id<Attribute>({
"name": name,
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
"required": true,
"value": (kcContext as any).user[name] ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
2024-05-08 19:24:18 +02:00
if ("email" in kcContext && kcContext.email instanceof Object) {
//NOTE: Handle legacy update-email.ftl
return [
id<Attribute>({
"name": "email",
"displayName": id<`\${${MessageKey}}`>(`\${email}`),
"required": true,
"value": (kcContext.email as any).value ?? "",
"html5DataAnnotations": {},
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": "email"
})
];
}
assert(false, "Unable to mock user profile from the current kcContext");
}
return kcContext.profile.attributes.map(attribute_pre_group_patch => {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
attribute_pre_group_patch as Attribute & {
group: string;
groupDisplayHeader?: string;
groupDisplayDescription?: string;
groupAnnotations: Record<string, string>;
};
return id<Attribute>({
...rest,
"group": {
"name": group,
"displayHeader": groupDisplayHeader,
"displayDescription": groupDisplayDescription,
"html5DataAnnotations": {}
}
});
}
2024-04-21 08:12:25 +02:00
return attribute_pre_group_patch;
});
})();
for (const attribute of attributes) {
syntheticAttributes.push(attribute);
2024-04-21 08:12:25 +02:00
add_password_and_password_confirm: {
if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
2024-04-21 08:12:25 +02:00
}
if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
// NOTE: We want to add password and password-confirm after the field that identifies the user.
// It's either email or username.
break add_password_and_password_confirm;
}
2024-04-21 08:12:25 +02:00
syntheticAttributes.push(
{
"name": "password",
"displayName": id<`\${${MessageKey}}`>("${password}"),
"required": true,
"readOnly": false,
"validators": {},
"annotations": {},
"autocomplete": "new-password",
"html5DataAnnotations": {},
// NOTE: Compat with Keycloak version prior to 24
...({ "groupAnnotations": {} } as {})
},
{
"name": "password-confirm",
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"),
"required": true,
"readOnly": false,
"validators": {},
"annotations": {},
"html5DataAnnotations": {},
"autocomplete": "new-password",
// NOTE: Compat with Keycloak version prior to 24
...({ "groupAnnotations": {} } as {})
}
);
}
}
2023-03-18 06:14:05 +01:00
return syntheticAttributes;
})();
2024-04-21 08:12:25 +02:00
const initialFormFieldState = (() => {
const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
2024-04-21 08:12:25 +02:00
for (const attribute of syntheticAttributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
2024-05-04 20:36:54 +02:00
const values = attribute.values ?? [""];
apply_validator_min_range: {
if (attribute.annotations.inputType?.startsWith("multiselect")) {
break apply_validator_min_range;
}
const validator = attribute.validators.multivalued;
if (validator === undefined) {
break apply_validator_min_range;
}
const { min: minStr } = validator;
if (minStr === undefined) {
break apply_validator_min_range;
}
const min = parseInt(minStr);
for (let index = values.length; index < min; index++) {
values.push("");
2024-05-04 20:36:54 +02:00
}
}
2024-05-04 20:36:54 +02:00
out.push({
attribute,
"valueOrValues": values
});
continue;
2024-05-04 20:36:54 +02:00
}
2024-04-21 08:12:25 +02:00
out.push({
attribute,
"valueOrValues": attribute.value ?? ""
});
}
2024-04-21 08:12:25 +02:00
return out;
})();
2023-03-18 06:14:05 +01:00
const initialState: internal.State = {
"formFieldStates": initialFormFieldState.map(({ attribute, valueOrValues }) => ({
attribute,
"errors": getErrors({
"attributeName": attribute.name,
"formFieldStates": initialFormFieldState
}),
"hasLostFocusAtLeastOnce": valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
"valueOrValues": valueOrValues
}))
};
return initialState;
}, []);
const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, params: FormAction): internal.State {
const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === params.name);
assert(formFieldState !== undefined);
(() => {
switch (params.action) {
case "update":
formFieldState.valueOrValues = params.valueOrValues;
apply_formatters: {
const { attribute } = formFieldState;
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
if (kcNumberFormat === undefined) {
break apply_formatters;
}
2024-04-21 08:12:25 +02:00
if (formFieldState.valueOrValues instanceof Array) {
formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
} else {
formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
}
2023-03-18 06:14:05 +01:00
}
2024-04-21 08:12:25 +02:00
formFieldState.errors = getErrors({
"attributeName": params.name,
"formFieldStates": state.formFieldStates
2024-04-21 08:12:25 +02:00
});
update_password_confirm: {
if (doMakeUserConfirmPassword) {
break update_password_confirm;
}
2024-04-21 08:12:25 +02:00
if (params.name !== "password") {
break update_password_confirm;
}
state = reducer(state, {
"action": "update",
"name": "password-confirm",
"valueOrValues": params.valueOrValues
});
}
return;
case "focus lost":
if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
const { fieldIndex } = params;
assert(fieldIndex !== undefined);
formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
return;
}
formFieldState.hasLostFocusAtLeastOnce = true;
return;
}
assert<Equals<typeof params, never>>(false);
})();
return state;
}, initialState);
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const formState: FormState = useMemo(
2023-03-18 06:14:05 +01:00
() => ({
"formFieldStates": state.formFieldStates.map(
({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
"displayableErrors": errors.filter(error => {
const hasLostFocusAtLeastOnce =
typeof hasLostFocusAtLeastOnceOrArr === "boolean"
? hasLostFocusAtLeastOnceOrArr
: error.fieldIndex !== undefined
? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
: hasLostFocusAtLeastOnceOrArr[hasLostFocusAtLeastOnceOrArr.length - 1];
switch (error.source.type) {
case "server":
return true;
case "other":
switch (error.source.rule) {
case "requiredField":
return hasLostFocusAtLeastOnce;
case "passwordConfirmMatchesPassword":
return hasLostFocusAtLeastOnce;
}
assert<Equals<typeof error.source.rule, never>>(false);
case "passwordPolicy":
switch (error.source.name) {
case "length":
return hasLostFocusAtLeastOnce;
case "digits":
return hasLostFocusAtLeastOnce;
case "lowerCase":
return hasLostFocusAtLeastOnce;
case "upperCase":
return hasLostFocusAtLeastOnce;
case "specialChars":
return hasLostFocusAtLeastOnce;
case "notUsername":
return true;
case "notEmail":
return true;
}
assert<Equals<typeof error.source, never>>(false);
case "validator":
switch (error.source.name) {
case "length":
return hasLostFocusAtLeastOnce;
case "pattern":
return hasLostFocusAtLeastOnce;
case "email":
return hasLostFocusAtLeastOnce;
case "integer":
return hasLostFocusAtLeastOnce;
case "multivalued":
return hasLostFocusAtLeastOnce;
case "options":
return hasLostFocusAtLeastOnce;
}
assert<Equals<typeof error.source, never>>(false);
}
}),
attribute,
...valueOrValuesWrap
})
),
2024-04-27 19:09:22 +02:00
"isFormSubmittable": state.formFieldStates.every(({ errors }) => errors.length === 0)
2023-03-18 06:14:05 +01:00
}),
2024-04-21 08:12:25 +02:00
[state]
2023-03-18 06:14:05 +01:00
);
return {
2024-04-21 08:12:25 +02:00
formState,
2024-04-22 06:53:08 +02:00
dispatchFormAction
2023-03-18 06:14:05 +01:00
};
}
function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField" | "passwordPolicies">; i18n: I18n }) {
const { kcContext, i18n } = params;
2023-03-18 06:14:05 +01:00
2024-04-22 06:34:50 +02:00
const { messagesPerField, passwordPolicies } = kcContext;
2023-03-18 06:14:05 +01:00
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
2024-04-21 08:12:25 +02:00
const getErrors = useConstCallback(
(params: { attributeName: string; formFieldStates: { attribute: Attribute; valueOrValues: string | string[] }[] }): FormFieldError[] => {
const { attributeName, formFieldStates } = params;
2023-03-18 06:14:05 +01:00
const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
2023-03-18 06:14:05 +01:00
assert(formFieldState !== undefined);
2023-03-18 06:14:05 +01:00
const { attribute } = formFieldState;
2023-03-18 06:14:05 +01:00
const valueOrValues = (() => {
let { valueOrValues } = formFieldState;
unFormat_number: {
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
if (kcNumberUnFormat === undefined) {
break unFormat_number;
}
if (valueOrValues instanceof Array) {
valueOrValues = valueOrValues.map(value => formatNumber(value, kcNumberUnFormat));
} else {
valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
}
}
return valueOrValues;
})();
2024-04-21 08:12:25 +02:00
assert(attribute !== undefined);
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
server_side_error: {
if (attribute.multivalued) {
const defaultValues = attribute.values ?? [""];
2023-03-18 06:14:05 +01:00
assert(valueOrValues instanceof Array);
const values = valueOrValues;
if (JSON.stringify(defaultValues) !== JSON.stringify(values.slice(0, defaultValues.length))) {
break server_side_error;
}
} else {
const defaultValue = attribute.value ?? "";
assert(typeof valueOrValues === "string");
const value = valueOrValues;
if (defaultValue !== value) {
break server_side_error;
}
2023-03-18 06:14:05 +01:00
}
2024-04-21 08:12:25 +02:00
let doesErrorExist: boolean;
try {
doesErrorExist = messagesPerField.existsError(attributeName);
2024-04-21 08:12:25 +02:00
} catch {
break server_side_error;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (!doesErrorExist) {
break server_side_error;
}
2023-03-18 06:14:05 +01:00
const errorMessageStr = messagesPerField.get(attributeName);
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
return [
{
errorMessageStr,
"errorMessage": <span key={0}>{errorMessageStr}</span>,
"fieldIndex": undefined,
"source": {
"type": "server"
}
2024-04-21 08:12:25 +02:00
}
];
2023-03-18 06:14:05 +01:00
}
handle_multi_valued_multi_fields: {
if (!attribute.multivalued) {
break handle_multi_valued_multi_fields;
}
if (attribute.annotations.inputType?.startsWith("multiselect")) {
break handle_multi_valued_multi_fields;
}
assert(valueOrValues instanceof Array);
const values = valueOrValues;
const errors = values
.map((...[, index]) => {
const specificValueErrors = getErrors({
attributeName,
"formFieldStates": formFieldStates.map(formFieldState => {
if (formFieldState.attribute.name === attributeName) {
assert(formFieldState.valueOrValues instanceof Array);
return {
"attribute": {
...attribute,
"annotations": {
...attribute.annotations,
"inputType": undefined
},
"multivalued": false
},
"valueOrValues": formFieldState.valueOrValues[index]
};
}
return formFieldState;
})
});
return specificValueErrors
.filter(error => {
if (error.source.type === "other" && error.source.rule === "requiredField") {
return false;
}
return true;
})
.map((error): FormFieldError => ({ ...error, "fieldIndex": index }));
})
.reduce((acc, errors) => [...acc, ...errors], []);
required_field: {
if (!attribute.required) {
break required_field;
}
2024-05-04 20:40:45 +02:00
if (values.every(value => value !== "")) {
break required_field;
}
const msgArgs = ["error-user-attribute-required"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "requiredField"
}
});
}
return errors;
}
handle_multi_valued_single_field: {
if (!attribute.multivalued) {
break handle_multi_valued_single_field;
}
if (!attribute.annotations.inputType?.startsWith("multiselect")) {
break handle_multi_valued_single_field;
}
const validatorName = "multivalued";
const validator = attribute.validators[validatorName];
if (validator === undefined) {
return [];
}
const { min: minStr } = validator;
const min = minStr !== undefined ? parseInt(minStr) : attribute.required ? 1 : 0;
assert(!isNaN(min));
const { max: maxStr } = validator;
const max = maxStr === undefined ? Infinity : parseInt(maxStr);
assert(!isNaN(max));
assert(valueOrValues instanceof Array);
const values = valueOrValues;
if (min <= values.length && values.length <= max) {
return [];
}
const msgArgs = ["error-invalid-multivalued-size", `${min}`, `${max}`] as const;
return [
{
"errorMessage": <Fragment key={0}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
}
}
];
}
assert(typeof valueOrValues === "string");
const value = valueOrValues;
2024-04-21 08:12:25 +02:00
const errors: FormFieldError[] = [];
2023-03-18 06:14:05 +01:00
2024-04-22 06:34:50 +02:00
check_password_policies: {
if (attributeName !== "password") {
2024-04-22 06:34:50 +02:00
break check_password_policies;
}
if (passwordPolicies === undefined) {
break check_password_policies;
}
check_password_policy_x: {
const policyName = "length";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minLength = parseInt(policy);
assert(!isNaN(minLength));
if (value.length >= minLength) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "digits";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfDigits = parseInt(policy);
assert(!isNaN(minNumberOfDigits));
if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "lowerCase";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfLowerCaseChar = parseInt(policy);
assert(!isNaN(minNumberOfLowerCaseChar));
if (
value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "upperCase";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfUpperCaseChar = parseInt(policy);
assert(!isNaN(minNumberOfUpperCaseChar));
if (
value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "specialChars";
const policy = passwordPolicies[policyName];
if (policy === undefined) {
break check_password_policy_x;
}
const minNumberOfSpecialChar = parseInt(policy);
assert(!isNaN(minNumberOfSpecialChar));
if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
break check_password_policy_x;
}
const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "notUsername";
const notUsername = passwordPolicies[policyName];
if (!notUsername) {
break check_password_policy_x;
}
const usernameFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "username");
2024-04-22 06:34:50 +02:00
if (usernameFormFieldState === undefined) {
2024-04-22 06:34:50 +02:00
break check_password_policy_x;
}
const usernameValue = (() => {
let { valueOrValues } = usernameFormFieldState;
assert(typeof valueOrValues === "string");
unFormat_number: {
const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
if (kcNumberUnFormat === undefined) {
break unFormat_number;
}
valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
}
return valueOrValues;
})();
if (value !== usernameValue) {
break check_password_policy_x;
2024-04-22 06:34:50 +02:00
}
const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
check_password_policy_x: {
const policyName = "notEmail";
const notEmail = passwordPolicies[policyName];
if (!notEmail) {
break check_password_policy_x;
}
const emailFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "email");
2024-04-22 06:34:50 +02:00
if (emailFormFieldState === undefined) {
2024-04-22 06:34:50 +02:00
break check_password_policy_x;
}
assert(typeof emailFormFieldState.valueOrValues === "string");
{
const emailValue = emailFormFieldState.valueOrValues;
if (value !== emailValue) {
break check_password_policy_x;
}
2024-04-22 06:34:50 +02:00
}
const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "passwordPolicy",
"name": policyName
}
2024-04-22 06:34:50 +02:00
});
}
}
password_confirm_matches_password: {
if (attributeName !== "password-confirm") {
break password_confirm_matches_password;
}
const passwordFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "password");
assert(passwordFormFieldState !== undefined);
assert(typeof passwordFormFieldState.valueOrValues === "string");
{
const passwordValue = passwordFormFieldState.valueOrValues;
if (value === passwordValue) {
break password_confirm_matches_password;
}
}
const msgArgs = ["invalidPasswordConfirmMessage"] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "passwordConfirmMatchesPassword"
}
});
}
2024-04-21 08:12:25 +02:00
const { validators } = attribute;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
required_field: {
if (!attribute.required) {
break required_field;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (value !== "") {
break required_field;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const msgArgs = ["error-user-attribute-required"] as const;
2023-03-18 06:14:05 +01:00
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "other",
"rule": "requiredField"
}
2023-03-18 06:14:05 +01:00
});
}
2024-04-21 08:12:25 +02:00
validator_x: {
const validatorName = "length";
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validator = validators[validatorName];
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (validator === undefined) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (ignoreEmptyValue && value === "") {
break validator_x;
}
2023-03-18 06:14:05 +01:00
const source: FormFieldError.Source = {
"type": "validator",
"name": validatorName
};
2024-04-21 08:12:25 +02:00
if (max !== undefined && value.length > parseInt(max)) {
const msgArgs = ["error-invalid-length-too-long", max] as const;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
2024-04-21 08:12:25 +02:00
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
source
2024-04-21 08:12:25 +02:00
});
2023-03-18 06:14:05 +01:00
}
2024-04-21 08:12:25 +02:00
if (min !== undefined && value.length < parseInt(min)) {
const msgArgs = ["error-invalid-length-too-short", min] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
2024-04-21 08:12:25 +02:00
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
source
2024-04-21 08:12:25 +02:00
});
}
2023-03-18 06:14:05 +01:00
}
2024-04-21 08:12:25 +02:00
validator_x: {
const validatorName = "pattern";
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validator = validators[validatorName];
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (validator === undefined) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (ignoreEmptyValue && value === "") {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (new RegExp(pattern).test(value)) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
"errorMessageStr": advancedMsgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
}
2024-04-21 08:12:25 +02:00
});
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
validator_x: {
{
const lastError = errors[errors.length - 1];
if (lastError !== undefined && lastError.source.type === "validator" && lastError.source.name === "pattern") {
break validator_x;
}
2024-04-21 08:12:25 +02:00
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validatorName = "email";
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validator = validators[validatorName];
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (validator === undefined) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (ignoreEmptyValue && value === "") {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (emailRegexp.test(value)) {
break validator_x;
}
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
2023-03-18 06:14:05 +01:00
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
}
2023-03-18 06:14:05 +01:00
});
}
2024-04-21 08:12:25 +02:00
validator_x: {
const validatorName = "integer";
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validator = validators[validatorName];
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (validator === undefined) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (ignoreEmptyValue && value === "") {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const intValue = parseInt(value);
2023-03-18 06:14:05 +01:00
const source: FormFieldError.Source = {
"type": "validator",
"name": validatorName
};
2024-04-21 08:12:25 +02:00
if (isNaN(intValue)) {
const msgArgs = ["mustBeAnInteger"] as const;
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
source
2024-04-21 08:12:25 +02:00
});
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (max !== undefined && intValue > parseInt(max)) {
const msgArgs = ["error-number-out-of-range-too-big", max] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
source
2024-04-21 08:12:25 +02:00
});
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (min !== undefined && intValue < parseInt(min)) {
const msgArgs = ["error-number-out-of-range-too-small", min] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{msg(...msgArgs)}</Fragment>,
"errorMessageStr": msgStr(...msgArgs),
"fieldIndex": undefined,
source
2024-04-21 08:12:25 +02:00
});
break validator_x;
}
2023-03-18 06:14:05 +01:00
}
2024-04-21 08:12:25 +02:00
validator_x: {
const validatorName = "options";
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
const validator = validators[validatorName];
if (validator === undefined) {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (value === "") {
break validator_x;
}
2023-03-18 06:14:05 +01:00
2024-04-21 08:12:25 +02:00
if (validator.options.indexOf(value) >= 0) {
break validator_x;
}
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
errors.push({
"errorMessage": <Fragment key={`${attributeName}-${errors.length}`}>{advancedMsg(...msgArgs)}</Fragment>,
"errorMessageStr": advancedMsgStr(...msgArgs),
"fieldIndex": undefined,
"source": {
"type": "validator",
"name": validatorName
}
2024-04-21 08:12:25 +02:00
});
}
//TODO: Implement missing validators. See Validators type definition.
2024-04-21 08:12:25 +02:00
return errors;
}
);
2023-03-18 06:14:05 +01:00
return { getErrors };
}