Compare commits
26 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
da1dc0309b | |||
30f4e7d833 | |||
cf3a86fb9b | |||
e1633f43f4 | |||
5b64cfc23c | |||
19709cf085 | |||
b8bb6c4f02 | |||
b7a543f8cb | |||
04b4e19563 | |||
ffb27fc66d | |||
8b5f7eefda | |||
c750bf4ee8 | |||
aa74019ef6 | |||
9be6d9f75f | |||
81ebb9b552 | |||
5e13b8c41f | |||
dd1ed948ec | |||
8b93f701cf | |||
2f0084de5b | |||
2ef9828625 | |||
89db8983a7 | |||
287dd9bd31 | |||
9a92054c1a | |||
4189036213 | |||
2c0a427ba5 | |||
77b488d624 |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.48",
|
||||
"version": "10.0.0-rc.54",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -110,7 +110,6 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"recast": "^0.23.3",
|
||||
"run-exclusive": "^2.2.19",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"storybook-dark-mode": "^1.1.2",
|
||||
"termost": "^0.12.0",
|
||||
"ts-node": "^10.9.2",
|
||||
|
@ -3,40 +3,83 @@ import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import chalk from "chalk";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
run(
|
||||
[
|
||||
`docker exec -it ${containerName}`,
|
||||
`/opt/keycloak/bin/kc.sh export`,
|
||||
`--dir /tmp`,
|
||||
`--realm myrealm`,
|
||||
`--users realm_file`
|
||||
].join(" ")
|
||||
);
|
||||
(async () => {
|
||||
{
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
.split(":")[1]
|
||||
).major;
|
||||
const child = child_process.spawn("docker", [
|
||||
...["exec", containerName],
|
||||
...["/opt/keycloak/bin/kc.sh", "export"],
|
||||
...["--dir", "/tmp"],
|
||||
...["--realm", "myrealm"],
|
||||
...["--users", "realm_file"]
|
||||
]);
|
||||
|
||||
const targetFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
)
|
||||
);
|
||||
let output = "";
|
||||
|
||||
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
||||
const onExit = (code: number | null) => {
|
||||
dCompleted.reject(new Error(`Exited with code ${code}`));
|
||||
};
|
||||
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
child.on("exit", onExit);
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
const outputStr = data.toString("utf8");
|
||||
|
||||
if (outputStr.includes("Export finished successfully")) {
|
||||
child.removeListener("exit", onExit);
|
||||
|
||||
child.kill();
|
||||
|
||||
dCompleted.resolve();
|
||||
}
|
||||
|
||||
output += outputStr;
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
||||
|
||||
try {
|
||||
await dCompleted.pr;
|
||||
} catch (error) {
|
||||
assert(is<Error>(error));
|
||||
|
||||
console.log(chalk.red(error.message));
|
||||
|
||||
console.log(output);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`)
|
||||
.toString("utf8")
|
||||
.trim()
|
||||
.split(":")[1]
|
||||
).major;
|
||||
|
||||
const targetFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
)
|
||||
);
|
||||
|
||||
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
||||
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
})();
|
||||
|
||||
function run(command: string) {
|
||||
console.log(chalk.grey(`$ ${command}`));
|
||||
|
@ -34,6 +34,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
cacheDirPath: buildContext.cacheDirPath
|
||||
});
|
||||
|
||||
|
@ -180,6 +180,16 @@ try {
|
||||
<#if attribute.annotations.inputTypePlaceholder??>
|
||||
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
|
||||
</#if>
|
||||
<!-- Loop through the options that are in attribute.validators.options.options -->
|
||||
<#if (
|
||||
attribute.annotations.inputOptionLabelsI18nPrefix?? &&
|
||||
attribute.validators?? &&
|
||||
attribute.validators.options??
|
||||
)>
|
||||
<#list attribute.validators.options.options as option>
|
||||
"${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}": decodeHtmlEntities("${msg(attribute.annotations.inputOptionLabelsI18nPrefix + "." + option)?js_string}"),
|
||||
</#list>
|
||||
</#if>
|
||||
</#list>
|
||||
};
|
||||
</#if>
|
||||
@ -326,7 +336,7 @@ function decodeHtmlEntities(htmlStr){
|
||||
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
key == "attemptedUsername" && are_same_path(path, ["auth"])
|
||||
)>
|
||||
<#attempt>
|
||||
|
@ -46,7 +46,7 @@ export async function generateKcGenTs(params: {
|
||||
``,
|
||||
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
|
||||
``,
|
||||
`export const KcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
|
||||
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
|
||||
``,
|
||||
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
|
@ -9,9 +9,10 @@ import { id } from "tsafe/id";
|
||||
|
||||
export async function promptKeycloakVersion(params: {
|
||||
startingFromMajor: number | undefined;
|
||||
excludeMajorVersions: number[];
|
||||
cacheDirPath: string;
|
||||
}) {
|
||||
const { startingFromMajor, cacheDirPath } = params;
|
||||
const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params;
|
||||
|
||||
const { getLatestsSemVersionedTag } = (() => {
|
||||
const { octokit } = (() => {
|
||||
@ -95,6 +96,10 @@ export async function promptKeycloakVersion(params: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSemVersionedTag = semVersionedTagByMajor.get(
|
||||
semVersionedTag.version.major
|
||||
);
|
||||
|
2155
src/bin/start-keycloak/myrealm-realm-18.json
Normal file
2155
src/bin/start-keycloak/myrealm-realm-18.json
Normal file
File diff suppressed because it is too large
Load Diff
2186
src/bin/start-keycloak/myrealm-realm-19.json
Normal file
2186
src/bin/start-keycloak/myrealm-realm-19.json
Normal file
File diff suppressed because it is too large
Load Diff
2197
src/bin/start-keycloak/myrealm-realm-20.json
Normal file
2197
src/bin/start-keycloak/myrealm-realm-20.json
Normal file
File diff suppressed because it is too large
Load Diff
2201
src/bin/start-keycloak/myrealm-realm-21.json
Normal file
2201
src/bin/start-keycloak/myrealm-realm-21.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -587,7 +587,9 @@
|
||||
"publicClient": true,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@ -619,7 +621,9 @@
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@ -695,7 +699,9 @@
|
||||
"publicClient": false,
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {},
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
@ -783,6 +789,7 @@
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"multivalued": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"user.attribute": "foo",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
@ -827,7 +834,8 @@
|
||||
"config": {
|
||||
"id.token.claim": "true",
|
||||
"introspection.token.claim": "true",
|
||||
"access.token.claim": "true"
|
||||
"access.token.claim": "true",
|
||||
"userinfo.token.claim": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1348,10 +1356,10 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
@ -1423,13 +1431,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
@ -2043,7 +2051,7 @@
|
||||
"name": "Terms and Conditions",
|
||||
"providerId": "TERMS_AND_CONDITIONS",
|
||||
"enabled": true,
|
||||
"defaultAction": false,
|
||||
"defaultAction": true,
|
||||
"priority": 20,
|
||||
"config": {}
|
||||
},
|
||||
@ -2122,8 +2130,8 @@
|
||||
"cibaExpiresIn": "120",
|
||||
"cibaAuthRequestedUserHint": "login_hint",
|
||||
"oauth2DeviceCodeLifespan": "600",
|
||||
"oauth2DevicePollingInterval": "5",
|
||||
"clientOfflineSessionMaxLifespan": "0",
|
||||
"oauth2DevicePollingInterval": "5",
|
||||
"clientSessionIdleTimeout": "0",
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0",
|
||||
|
@ -1501,14 +1501,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"saml-user-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1541,13 +1541,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -2200,7 +2200,7 @@
|
||||
"name": "Terms and Conditions",
|
||||
"providerId": "TERMS_AND_CONDITIONS",
|
||||
"enabled": true,
|
||||
"defaultAction": false,
|
||||
"defaultAction": true,
|
||||
"priority": 20,
|
||||
"config": {}
|
||||
},
|
||||
@ -2307,7 +2307,7 @@
|
||||
"cibaInterval": "5",
|
||||
"realmReusableOtpCode": "false"
|
||||
},
|
||||
"keycloakVersion": "24.0.4",
|
||||
"keycloakVersion": "24.0.5",
|
||||
"userManagedAccessAllowed": false,
|
||||
"clientProfiles": {
|
||||
"profiles": []
|
||||
|
2400
src/bin/start-keycloak/myrealm-realm-25.json
Normal file
2400
src/bin/start-keycloak/myrealm-realm-25.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -159,7 +159,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
);
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
startingFromMajor: 17,
|
||||
startingFromMajor: 18,
|
||||
excludeMajorVersions: [22],
|
||||
cacheDirPath: buildContext.cacheDirPath
|
||||
});
|
||||
|
||||
@ -231,6 +232,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
const realmJsonFilePath = await (async () => {
|
||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||
if (cliCommandOptions.realmJsonFilePath === "none") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
|
||||
@ -312,6 +317,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
await extractThemeResourcesFromJar();
|
||||
|
||||
const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename);
|
||||
|
||||
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
|
||||
|
||||
try {
|
||||
child_process.execSync(`docker rm --force ${containerName}`, {
|
||||
stdio: "ignore"
|
||||
@ -332,7 +341,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
"-v",
|
||||
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
|
||||
]),
|
||||
...["-v", `${jarFilePath}:/opt/keycloak/providers/keycloak-theme.jar`],
|
||||
...[
|
||||
"-v",
|
||||
`${jarFilePath_cacheDir}:/opt/keycloak/providers/keycloak-theme.jar`
|
||||
],
|
||||
...(keycloakMajorVersionNumber <= 20
|
||||
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
||||
: []),
|
||||
|
@ -4,7 +4,7 @@ import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { I18n } from "keycloakify/login/i18n";
|
||||
import type { KcContext } from "keycloakify/login/KcContext";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
|
||||
const Login = lazy(() => import("keycloakify/login/pages/Login"));
|
||||
const Register = lazy(() => import("keycloakify/login/pages/Register"));
|
||||
|
@ -209,17 +209,13 @@ export declare namespace KcContext {
|
||||
export type Register = Common & {
|
||||
pageId: "register.ftl";
|
||||
profile: UserProfile;
|
||||
passwordPolicies?: PasswordPolicies;
|
||||
url: {
|
||||
registrationAction: string;
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
recaptchaRequired: boolean;
|
||||
recaptchaSiteKey?: string;
|
||||
/**
|
||||
* Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension
|
||||
* A Keycloak Java extension used as dependency in Keycloakify.
|
||||
*/
|
||||
passwordPolicies?: PasswordPolicies;
|
||||
termsAcceptanceRequired?: boolean;
|
||||
};
|
||||
|
||||
@ -479,16 +475,19 @@ export declare namespace KcContext {
|
||||
export type LoginUpdateProfile = Common & {
|
||||
pageId: "login-update-profile.ftl";
|
||||
profile: UserProfile;
|
||||
passwordPolicies?: PasswordPolicies;
|
||||
};
|
||||
|
||||
export type IdpReviewUserProfile = Common & {
|
||||
pageId: "idp-review-user-profile.ftl";
|
||||
profile: UserProfile;
|
||||
passwordPolicies?: PasswordPolicies;
|
||||
};
|
||||
|
||||
export type UpdateEmail = Common & {
|
||||
pageId: "update-email.ftl";
|
||||
profile: UserProfile;
|
||||
passwordPolicies?: PasswordPolicies;
|
||||
};
|
||||
|
||||
export type SelectAuthenticator = Common & {
|
||||
@ -752,6 +751,10 @@ export declare namespace Validators {
|
||||
assert<Equals<OnlyInExpected, never>>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension
|
||||
* A Keycloak Java extension used as dependency in Keycloakify.
|
||||
*/
|
||||
export type PasswordPolicies = {
|
||||
/** The minimum length of the password */
|
||||
length?: number;
|
||||
|
@ -4,33 +4,15 @@ import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import {
|
||||
useUserProfileForm,
|
||||
getButtonToDisplayForMultivaluedAttributeField,
|
||||
type KcContextLike,
|
||||
type FormAction,
|
||||
type FormFieldError
|
||||
} from "keycloakify/login/lib/useUserProfileForm";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { Attribute } from "keycloakify/login/KcContext";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export type UserProfileFormFieldsProps = {
|
||||
kcContext: KcContextLike;
|
||||
i18n: I18n;
|
||||
kcClsx: KcClsx;
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
doMakeUserConfirmPassword: boolean;
|
||||
BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||
AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
type BeforeAfterFieldProps = {
|
||||
attribute: Attribute;
|
||||
dispatchFormAction: React.Dispatch<FormAction>;
|
||||
displayableErrors: FormFieldError[];
|
||||
valueOrValues: string | string[];
|
||||
kcClsx: KcClsx;
|
||||
i18n: I18n;
|
||||
};
|
||||
|
||||
export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
||||
export default function UserProfileFormFields(props: UserProfileFormFieldsProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
22
src/login/UserProfileFormFieldsProps.tsx
Normal file
22
src/login/UserProfileFormFieldsProps.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
|
||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { Attribute } from "keycloakify/login/KcContext";
|
||||
|
||||
export type UserProfileFormFieldsProps<KcContext = any, I18n = any> = {
|
||||
kcContext: Extract<KcContext, { profile: unknown }>;
|
||||
i18n: I18n;
|
||||
kcClsx: KcClsx;
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
doMakeUserConfirmPassword: boolean;
|
||||
BeforeField?: (props: BeforeAfterFieldProps<I18n>) => JSX.Element | null;
|
||||
AfterField?: (props: BeforeAfterFieldProps<I18n>) => JSX.Element | null;
|
||||
};
|
||||
|
||||
type BeforeAfterFieldProps<I18n> = {
|
||||
attribute: Attribute;
|
||||
dispatchFormAction: React.Dispatch<FormAction>;
|
||||
displayableErrors: FormFieldError[];
|
||||
valueOrValues: string | string[];
|
||||
kcClsx: KcClsx;
|
||||
i18n: I18n;
|
||||
};
|
@ -79,9 +79,9 @@ export type KcContextLike = KcContextLike_i18n &
|
||||
};
|
||||
};
|
||||
|
||||
assert<Extract<KcContext.Register, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
|
||||
assert<Extract<Extract<KcContext, { profile: unknown }>, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
|
||||
|
||||
export type ParamsOfUseUserProfileForm = {
|
||||
export type UseUserProfileFormParams = {
|
||||
kcContext: KcContextLike;
|
||||
i18n: I18n;
|
||||
doMakeUserConfirmPassword: boolean;
|
||||
@ -105,7 +105,7 @@ namespace internal {
|
||||
};
|
||||
}
|
||||
|
||||
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
|
||||
export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
|
||||
const { kcContext, i18n, doMakeUserConfirmPassword } = params;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
@ -130,168 +130,137 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
||||
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[] = [];
|
||||
// We also want to apply some retro-compatibility and consistency patches.
|
||||
const attributes: Attribute[] = (() => {
|
||||
mock_user_profile_attributes_for_older_keycloak_versions: {
|
||||
if (
|
||||
"profile" in kcContext &&
|
||||
"attributesByName" in kcContext.profile &&
|
||||
Object.keys(kcContext.profile.attributesByName).length !== 0
|
||||
) {
|
||||
break mock_user_profile_attributes_for_older_keycloak_versions;
|
||||
}
|
||||
|
||||
const attributes = (() => {
|
||||
retrocompat_patch: {
|
||||
if (
|
||||
"profile" in kcContext &&
|
||||
"attributesByName" in kcContext.profile &&
|
||||
Object.keys(kcContext.profile.attributesByName).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,
|
||||
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)
|
||||
.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;
|
||||
}
|
||||
})()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if ("email" in kcContext && kcContext.email instanceof Object) {
|
||||
//NOTE: Handle legacy update-email.ftl
|
||||
return [
|
||||
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: "email",
|
||||
displayName: id<`\${${MessageKey}}`>(`\${email}`),
|
||||
name: name,
|
||||
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
|
||||
required: true,
|
||||
value: (kcContext.email as any).value ?? "",
|
||||
value: (kcContext.register as any).formData[name] ?? "",
|
||||
html5DataAnnotations: {},
|
||||
readOnly: false,
|
||||
validators: {},
|
||||
annotations: {},
|
||||
autocomplete: "email"
|
||||
autocomplete: (() => {
|
||||
switch (name) {
|
||||
case "email":
|
||||
return "email";
|
||||
case "username":
|
||||
return "username";
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})()
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
assert(false, "Unable to mock user profile from the current kcContext");
|
||||
);
|
||||
}
|
||||
|
||||
return Object.values(kcContext.profile.attributesByName).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>;
|
||||
};
|
||||
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.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;
|
||||
}
|
||||
})()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return id<Attribute>({
|
||||
...rest,
|
||||
group: {
|
||||
name: group,
|
||||
displayHeader: groupDisplayHeader,
|
||||
displayDescription: groupDisplayDescription,
|
||||
html5DataAnnotations: {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return attribute_pre_group_patch;
|
||||
});
|
||||
})();
|
||||
|
||||
for (const attribute of attributes) {
|
||||
syntheticAttributes.push(structuredCloneButFunctions(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;
|
||||
}
|
||||
|
||||
syntheticAttributes.push(
|
||||
{
|
||||
name: "password",
|
||||
displayName: id<`\${${MessageKey}}`>("${password}"),
|
||||
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: "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 {})
|
||||
}
|
||||
);
|
||||
autocomplete: "email"
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
assert(false, "Unable to mock user profile from the current kcContext");
|
||||
}
|
||||
|
||||
// NOTE: Consistency patch
|
||||
syntheticAttributes.forEach(attribute => {
|
||||
return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
|
||||
})();
|
||||
|
||||
// Retro-compatibility and consistency patches
|
||||
attributes.forEach(attribute => {
|
||||
patch_legacy_group: {
|
||||
if (typeof attribute.group !== "string") {
|
||||
break patch_legacy_group;
|
||||
}
|
||||
|
||||
const { group, groupDisplayHeader, groupDisplayDescription /*, groupAnnotations*/ } = attribute as Attribute & {
|
||||
group: string;
|
||||
groupDisplayHeader?: string;
|
||||
groupDisplayDescription?: string;
|
||||
groupAnnotations: Record<string, string>;
|
||||
};
|
||||
|
||||
delete attribute.group;
|
||||
// @ts-expect-error
|
||||
delete attribute.groupDisplayHeader;
|
||||
// @ts-expect-error
|
||||
delete attribute.groupDisplayDescription;
|
||||
// @ts-expect-error
|
||||
delete attribute.groupAnnotations;
|
||||
|
||||
if (group === "") {
|
||||
break patch_legacy_group;
|
||||
}
|
||||
|
||||
attribute.group = {
|
||||
name: group,
|
||||
displayHeader: groupDisplayHeader,
|
||||
displayDescription: groupDisplayDescription,
|
||||
html5DataAnnotations: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Attributes with options rendered by default as select inputs
|
||||
if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
|
||||
attribute.annotations.inputType = "select";
|
||||
}
|
||||
|
||||
// Consistency patch on values/value property
|
||||
{
|
||||
if (getIsMultivaluedSingleField({ attribute })) {
|
||||
attribute.multivalued = true;
|
||||
}
|
||||
@ -303,65 +272,98 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
|
||||
attribute.value ??= attribute.values?.[0];
|
||||
delete attribute.values;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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?.length ? attribute.values : [""];
|
||||
|
||||
apply_validator_min_range: {
|
||||
if (getIsMultivaluedSingleField({ attribute })) {
|
||||
break apply_validator_min_range;
|
||||
}
|
||||
|
||||
const validator = attribute.validators.multivalued;
|
||||
|
||||
if (validator === undefined) {
|
||||
break apply_validator_min_range;
|
||||
}
|
||||
|
||||
const { min: minStr } = validator;
|
||||
|
||||
if (!minStr) {
|
||||
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 ?? ""
|
||||
});
|
||||
add_password_and_password_confirm: {
|
||||
if (!kcContext.passwordRequired) {
|
||||
break add_password_and_password_confirm;
|
||||
}
|
||||
|
||||
return out;
|
||||
})();
|
||||
attributes.forEach((attribute, i) => {
|
||||
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.
|
||||
return;
|
||||
}
|
||||
|
||||
attributes.splice(
|
||||
i + 1,
|
||||
0,
|
||||
{
|
||||
name: "password",
|
||||
displayName: id<`\${${MessageKey}}`>("${password}"),
|
||||
required: true,
|
||||
readOnly: false,
|
||||
validators: {},
|
||||
annotations: {},
|
||||
autocomplete: "new-password",
|
||||
html5DataAnnotations: {}
|
||||
},
|
||||
{
|
||||
name: "password-confirm",
|
||||
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
||||
required: true,
|
||||
readOnly: false,
|
||||
validators: {},
|
||||
annotations: {},
|
||||
html5DataAnnotations: {},
|
||||
autocomplete: "new-password"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const initialFormFieldState: {
|
||||
attribute: Attribute;
|
||||
valueOrValues: string | string[];
|
||||
}[] = [];
|
||||
|
||||
for (const attribute of attributes) {
|
||||
handle_multi_valued_attribute: {
|
||||
if (!attribute.multivalued) {
|
||||
break handle_multi_valued_attribute;
|
||||
}
|
||||
|
||||
const values = attribute.values?.length ? attribute.values : [""];
|
||||
|
||||
apply_validator_min_range: {
|
||||
if (getIsMultivaluedSingleField({ attribute })) {
|
||||
break apply_validator_min_range;
|
||||
}
|
||||
|
||||
const validator = attribute.validators.multivalued;
|
||||
|
||||
if (validator === undefined) {
|
||||
break apply_validator_min_range;
|
||||
}
|
||||
|
||||
const { min: minStr } = validator;
|
||||
|
||||
if (!minStr) {
|
||||
break apply_validator_min_range;
|
||||
}
|
||||
|
||||
const min = parseInt(`${minStr}`);
|
||||
|
||||
for (let index = values.length; index < min; index++) {
|
||||
values.push("");
|
||||
}
|
||||
}
|
||||
|
||||
initialFormFieldState.push({
|
||||
attribute,
|
||||
valueOrValues: values
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
initialFormFieldState.push({
|
||||
attribute,
|
||||
valueOrValues: attribute.value ?? ""
|
||||
});
|
||||
}
|
||||
|
||||
const initialState: internal.State = {
|
||||
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
|
||||
|
@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
@ -3,7 +3,7 @@ import { Markdown } from "keycloakify/tools/Markdown";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
@ -1,4 +1,4 @@
|
||||
export const formatNumber = (input: string, format: string): string => {
|
||||
export const formatNumber = (input: string, format: string) => {
|
||||
if (!input) {
|
||||
return "";
|
||||
}
|
||||
@ -20,7 +20,8 @@ export const formatNumber = (input: string, format: string): string => {
|
||||
let rawValue = input.replace(/\D+/g, "");
|
||||
|
||||
// make sure the value is a number
|
||||
if (`${parseInt(rawValue)}` !== rawValue) {
|
||||
// @ts-expect-error: It's Keycloak's code, we trust it.
|
||||
if (parseInt(rawValue) != rawValue) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import DefaultPage from "../../dist/account/Fallback";
|
||||
import DefaultPage from "../../dist/account/DefaultPage";
|
||||
import { useI18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import Template from "../../dist/account/Template";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import DefaultPage from "../../dist/login/Fallback";
|
||||
import DefaultPage from "../../dist/login/DefaultPage";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms";
|
||||
|
@ -11144,11 +11144,6 @@ schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2:
|
||||
ajv "^6.12.5"
|
||||
ajv-keywords "^3.5.2"
|
||||
|
||||
scripting-tools@^0.19.13:
|
||||
version "0.19.14"
|
||||
resolved "https://registry.yarnpkg.com/scripting-tools/-/scripting-tools-0.19.14.tgz#d46cdea3dcf042b103b1712103b007e72c4901d5"
|
||||
integrity sha512-KGRES70dEmcaCdpx3R88bLWmfA4mQ/EGikCQy0FGTZwx3y9F5yYkzEhwp02+ZTgpvF25JcNOhDBbOqL6z92kwg==
|
||||
|
||||
semver-compare@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"
|
||||
|
Reference in New Issue
Block a user