Refactor and handle legacy login-update-profile.ftl

This commit is contained in:
Joseph Garrone 2024-05-08 16:04:12 +02:00
parent a6f7f8ff49
commit 027c8f38d8

View File

@ -70,7 +70,7 @@ export type KcContextLike = {
attributes: Attribute[];
html5DataAnnotations?: Record<string, string>;
};
passwordRequired: boolean;
passwordRequired?: boolean;
realm: { registrationEmailAsUsername: boolean };
passwordPolicies?: PasswordPolicies;
url: {
@ -122,8 +122,19 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
"documentTitle": undefined
});
const attributesWithPassword = useMemo(() => {
const attributesWithPassword: Attribute[] = [];
const { getErrors } = useGetErrors({
kcContext,
i18n
});
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: {
@ -131,8 +142,9 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break retrocompat_patch;
}
kcContext.profile = {
"attributes": (["firstName", "lastName", "email", "username"] as const)
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>({
@ -155,16 +167,41 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
})()
})
),
"html5DataAnnotations": {}
};
);
}
return kcContext.profile.attributes;
})();
if ("user" in kcContext && kcContext.user instanceof Object) {
//NOTE: Handle legacy login-update-profile.ftl
return (["username", "email", "firstName", "lastName"] as const)
.filter(name => (name !== "username" ? true : (kcContext as any).user.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;
}
})()
})
);
}
for (const attribute_pre_group_patch of attributes) {
const attribute = (() => {
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 & {
@ -186,9 +223,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
return attribute_pre_group_patch;
});
})();
attributesWithPassword.push(attribute);
for (const attribute of attributes) {
syntheticAttributes.push(attribute);
add_password_and_password_confirm: {
if (!kcContext.passwordRequired) {
@ -201,7 +240,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break add_password_and_password_confirm;
}
attributesWithPassword.push(
syntheticAttributes.push(
{
"name": "password",
"displayName": id<`\${${MessageKey}}`>("${password}"),
@ -230,16 +269,77 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
}
return attributesWithPassword;
}, []);
return syntheticAttributes;
})();
const { getErrors } = useGetErrors({
kcContext,
i18n
const initialFormFieldState = (() => {
const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
for (const attribute of syntheticAttributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
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("");
}
}
out.push({
attribute,
"valueOrValues": values
});
const [state, dispatchFormAction] = useReducer(
function reducer(state: internal.State, params: FormAction): internal.State {
continue;
}
out.push({
attribute,
"valueOrValues": attribute.value ?? ""
});
}
return out;
})();
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);
@ -302,75 +402,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
})();
return state;
},
useMemo(function getInitialState(): internal.State {
const initialFormFieldState = (() => {
const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
for (const attribute of attributesWithPassword) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
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("");
}
}
out.push({
attribute,
"valueOrValues": values
});
continue;
}
out.push({
attribute,
"valueOrValues": attribute.value ?? ""
});
}
return out;
})();
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;
}, [])
);
}, initialState);
const formState: FormState = useMemo(
() => ({