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[]; attributes: Attribute[];
html5DataAnnotations?: Record<string, string>; html5DataAnnotations?: Record<string, string>;
}; };
passwordRequired: boolean; passwordRequired?: boolean;
realm: { registrationEmailAsUsername: boolean }; realm: { registrationEmailAsUsername: boolean };
passwordPolicies?: PasswordPolicies; passwordPolicies?: PasswordPolicies;
url: { url: {
@ -122,8 +122,19 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
"documentTitle": undefined "documentTitle": undefined
}); });
const attributesWithPassword = useMemo(() => { const { getErrors } = useGetErrors({
const attributesWithPassword: Attribute[] = []; 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 = (() => { const attributes = (() => {
retrocompat_patch: { retrocompat_patch: {
@ -131,8 +142,9 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break retrocompat_patch; break retrocompat_patch;
} }
kcContext.profile = { if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
"attributes": (["firstName", "lastName", "email", "username"] as const) //NOTE: Handle legacy register.ftl page
return (["firstName", "lastName", "email", "username"] as const)
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername)) .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
.map(name => .map(name =>
id<Attribute>({ 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) { assert(false, "Unable to mock user profile from the current kcContext");
const attribute = (() => { }
return kcContext.profile.attributes.map(attribute_pre_group_patch => {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") { if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
attribute_pre_group_patch as Attribute & { attribute_pre_group_patch as Attribute & {
@ -186,9 +223,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
} }
return attribute_pre_group_patch; return attribute_pre_group_patch;
});
})(); })();
attributesWithPassword.push(attribute); for (const attribute of attributes) {
syntheticAttributes.push(attribute);
add_password_and_password_confirm: { add_password_and_password_confirm: {
if (!kcContext.passwordRequired) { if (!kcContext.passwordRequired) {
@ -201,7 +240,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break add_password_and_password_confirm; break add_password_and_password_confirm;
} }
attributesWithPassword.push( syntheticAttributes.push(
{ {
"name": "password", "name": "password",
"displayName": id<`\${${MessageKey}}`>("${password}"), "displayName": id<`\${${MessageKey}}`>("${password}"),
@ -230,16 +269,77 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
} }
} }
return attributesWithPassword; return syntheticAttributes;
}, []); })();
const { getErrors } = useGetErrors({ const initialFormFieldState = (() => {
kcContext, const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
i18n
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( continue;
function reducer(state: internal.State, params: FormAction): internal.State { }
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); const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === params.name);
assert(formFieldState !== undefined); assert(formFieldState !== undefined);
@ -302,75 +402,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
})(); })();
return state; return state;
}, }, initialState);
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;
}, [])
);
const formState: FormState = useMemo( const formState: FormState = useMemo(
() => ({ () => ({