Refactor and handle legacy login-update-profile.ftl
This commit is contained in:
parent
a6f7f8ff49
commit
027c8f38d8
@ -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,255 +122,287 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
|||||||
"documentTitle": undefined
|
"documentTitle": undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
const attributesWithPassword = useMemo(() => {
|
|
||||||
const attributesWithPassword: Attribute[] = [];
|
|
||||||
|
|
||||||
const attributes = (() => {
|
|
||||||
retrocompat_patch: {
|
|
||||||
if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
|
|
||||||
break retrocompat_patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
kcContext.profile = {
|
|
||||||
"attributes": (["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,
|
|
||||||
"value": (kcContext as any).register.formData[name] ?? "",
|
|
||||||
"html5DataAnnotations": {},
|
|
||||||
"readOnly": false,
|
|
||||||
"validators": {},
|
|
||||||
"annotations": {},
|
|
||||||
"autocomplete": (() => {
|
|
||||||
switch (name) {
|
|
||||||
case "email":
|
|
||||||
return "email";
|
|
||||||
case "username":
|
|
||||||
return "username";
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})()
|
|
||||||
})
|
|
||||||
),
|
|
||||||
"html5DataAnnotations": {}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return kcContext.profile.attributes;
|
|
||||||
})();
|
|
||||||
|
|
||||||
for (const attribute_pre_group_patch of attributes) {
|
|
||||||
const attribute = (() => {
|
|
||||||
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": {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return attribute_pre_group_patch;
|
|
||||||
})();
|
|
||||||
|
|
||||||
attributesWithPassword.push(attribute);
|
|
||||||
|
|
||||||
add_password_and_password_confirm: {
|
|
||||||
if (!kcContext.passwordRequired) {
|
|
||||||
break add_password_and_password_confirm;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
attributesWithPassword.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 {})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return attributesWithPassword;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { getErrors } = useGetErrors({
|
const { getErrors } = useGetErrors({
|
||||||
kcContext,
|
kcContext,
|
||||||
i18n
|
i18n
|
||||||
});
|
});
|
||||||
|
|
||||||
const [state, dispatchFormAction] = useReducer(
|
const initialState = useMemo((): internal.State => {
|
||||||
function reducer(state: internal.State, params: FormAction): internal.State {
|
// NOTE: We don't use te kcContext.profile.attributes directly because
|
||||||
const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === params.name);
|
// 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[] = [];
|
||||||
|
|
||||||
assert(formFieldState !== undefined);
|
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) {
|
||||||
switch (params.action) {
|
//NOTE: Handle legacy register.ftl page
|
||||||
case "update":
|
return (["firstName", "lastName", "email", "username"] as const)
|
||||||
formFieldState.valueOrValues = params.valueOrValues;
|
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
|
||||||
|
.map(name =>
|
||||||
|
id<Attribute>({
|
||||||
|
"name": name,
|
||||||
|
"displayName": id<`\${${MessageKey}}`>(`\${${name}}`),
|
||||||
|
"required": true,
|
||||||
|
"value": (kcContext as any).register.formData[name] ?? "",
|
||||||
|
"html5DataAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"validators": {},
|
||||||
|
"annotations": {},
|
||||||
|
"autocomplete": (() => {
|
||||||
|
switch (name) {
|
||||||
|
case "email":
|
||||||
|
return "email";
|
||||||
|
case "username":
|
||||||
|
return "username";
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
apply_formatters: {
|
if ("user" in kcContext && kcContext.user instanceof Object) {
|
||||||
const { attribute } = formFieldState;
|
//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;
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
|
assert(false, "Unable to mock user profile from the current kcContext");
|
||||||
|
|
||||||
if (kcNumberFormat === undefined) {
|
|
||||||
break apply_formatters;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (formFieldState.valueOrValues instanceof Array) {
|
|
||||||
formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
|
|
||||||
} else {
|
|
||||||
formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
formFieldState.errors = getErrors({
|
|
||||||
"attributeName": params.name,
|
|
||||||
"formFieldStates": state.formFieldStates
|
|
||||||
});
|
|
||||||
|
|
||||||
update_password_confirm: {
|
|
||||||
if (doMakeUserConfirmPassword) {
|
|
||||||
break update_password_confirm;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 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": {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attribute_pre_group_patch;
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return state;
|
for (const attribute of attributes) {
|
||||||
},
|
syntheticAttributes.push(attribute);
|
||||||
useMemo(function getInitialState(): internal.State {
|
|
||||||
const initialFormFieldState = (() => {
|
|
||||||
const out: { attribute: Attribute; valueOrValues: string | string[] }[] = [];
|
|
||||||
|
|
||||||
for (const attribute of attributesWithPassword) {
|
add_password_and_password_confirm: {
|
||||||
handle_multi_valued_attribute: {
|
if (!kcContext.passwordRequired) {
|
||||||
if (!attribute.multivalued) {
|
break add_password_and_password_confirm;
|
||||||
break handle_multi_valued_attribute;
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return syntheticAttributes;
|
||||||
|
})();
|
||||||
|
|
||||||
|
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 values = attribute.values ?? [""];
|
const validator = attribute.validators.multivalued;
|
||||||
|
|
||||||
apply_validator_min_range: {
|
if (validator === undefined) {
|
||||||
if (attribute.annotations.inputType?.startsWith("multiselect")) {
|
break apply_validator_min_range;
|
||||||
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({
|
const { min: minStr } = validator;
|
||||||
attribute,
|
|
||||||
"valueOrValues": values
|
|
||||||
});
|
|
||||||
|
|
||||||
continue;
|
if (minStr === undefined) {
|
||||||
|
break apply_validator_min_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
const min = parseInt(minStr);
|
||||||
|
|
||||||
|
for (let index = values.length; index < min; index++) {
|
||||||
|
values.push("");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push({
|
out.push({
|
||||||
attribute,
|
attribute,
|
||||||
"valueOrValues": attribute.value ?? ""
|
"valueOrValues": values
|
||||||
});
|
});
|
||||||
|
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
return out;
|
out.push({
|
||||||
})();
|
|
||||||
|
|
||||||
const initialState: internal.State = {
|
|
||||||
"formFieldStates": initialFormFieldState.map(({ attribute, valueOrValues }) => ({
|
|
||||||
attribute,
|
attribute,
|
||||||
"errors": getErrors({
|
"valueOrValues": attribute.value ?? ""
|
||||||
"attributeName": attribute.name,
|
});
|
||||||
"formFieldStates": initialFormFieldState
|
}
|
||||||
}),
|
|
||||||
"hasLostFocusAtLeastOnce": valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
|
|
||||||
"valueOrValues": valueOrValues
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
|
|
||||||
return initialState;
|
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);
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
switch (params.action) {
|
||||||
|
case "update":
|
||||||
|
formFieldState.valueOrValues = params.valueOrValues;
|
||||||
|
|
||||||
|
apply_formatters: {
|
||||||
|
const { attribute } = formFieldState;
|
||||||
|
|
||||||
|
const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
|
||||||
|
|
||||||
|
if (kcNumberFormat === undefined) {
|
||||||
|
break apply_formatters;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formFieldState.valueOrValues instanceof Array) {
|
||||||
|
formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
|
||||||
|
} else {
|
||||||
|
formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formFieldState.errors = getErrors({
|
||||||
|
"attributeName": params.name,
|
||||||
|
"formFieldStates": state.formFieldStates
|
||||||
|
});
|
||||||
|
|
||||||
|
update_password_confirm: {
|
||||||
|
if (doMakeUserConfirmPassword) {
|
||||||
|
break update_password_confirm;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
const formState: FormState = useMemo(
|
const formState: FormState = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user