Compare commits

..

44 Commits

Author SHA1 Message Date
da1dc0309b Release candidate 2024-06-12 08:57:59 +02:00
30f4e7d833 Add PasswordPolicies on every page where there's user profile 2024-06-12 08:57:40 +02:00
cf3a86fb9b Release candidate 2024-06-11 21:22:34 +02:00
e1633f43f4 Apply same strategy for UserProfileFormField than for TempateProps for extendability 2024-06-11 21:21:58 +02:00
5b64cfc23c Release candidate 2024-06-11 20:50:31 +02:00
19709cf085 Only types are capitalized 2024-06-11 20:50:11 +02:00
b8bb6c4f02 Fix build 2024-06-11 20:40:00 +02:00
b7a543f8cb Do not export PageProps in the index 2024-06-11 20:30:39 +02:00
04b4e19563 Release candidate 2024-06-11 20:27:53 +02:00
ffb27fc66d Extract Props from UserProfileFormFields so it's ejectable 2024-06-11 20:27:03 +02:00
8b5f7eefda Release candidate 2024-06-11 19:14:19 +02:00
c750bf4ee8 Export PageProps 2024-06-11 19:14:04 +02:00
aa74019ef6 Fix build 2024-06-11 19:08:36 +02:00
9be6d9f75f Release candidate 2024-06-11 17:27:40 +02:00
81ebb9b552 Prevent the jar to be corrupted when rebuild 2024-06-11 17:19:36 +02:00
5e13b8c41f Exclude Keycloak 22 from test panel 2024-06-11 17:12:12 +02:00
dd1ed948ec Update Keycloak 25 default realm config 2024-06-11 16:26:03 +02:00
8b93f701cf Add realms configurations for Keycloak majors 2024-06-11 16:19:54 +02:00
2f0084de5b Pass the input options translation to the kcContext 2024-06-11 16:10:54 +02:00
2ef9828625 Start with keycloak 18 for local container 2024-06-11 11:39:03 +02:00
89db8983a7 Fix exception in terms.ftl 2024-06-11 11:37:45 +02:00
287dd9bd31 Refactor + attributes with options rendered by default as select inputs 2024-06-11 09:22:50 +02:00
9a92054c1a Remove unused dependency 2024-06-10 21:06:02 +02:00
4189036213 Fix storybook 2024-06-10 21:05:17 +02:00
2c0a427ba5 Fix the script to export realm 2024-06-10 20:51:00 +02:00
77b488d624 Fix the formatNumber function 2024-06-10 20:14:14 +02:00
5249e05746 Release candidate 2024-06-10 19:36:11 +02:00
1e7a0dd7a6 Enable to add files to the jar with the post build options 2024-06-10 19:35:56 +02:00
fd67f2402a Release candidate 2024-06-10 17:30:20 +02:00
60a65ede2f Preserve ordering on user attributes 2024-06-10 17:30:00 +02:00
1fa659ce61 Release candidate 2024-06-10 16:01:56 +02:00
0ab903dbc7 Add new build target for Kc 25 https://github.com/p2-inc/keycloak-account-v1/pull/13 2024-06-10 15:29:08 +02:00
70b0a04793 Release candidate 2024-06-10 15:08:46 +02:00
c0df9aa939 Remove logs 2024-06-10 09:32:07 +02:00
60a1886942 Fix path error 2024-06-10 09:28:31 +02:00
1ebf97871b Fix logical error 2024-06-10 09:26:47 +02:00
72e321aa32 Fix update of the build process checkpoint 2024-06-10 09:24:16 +02:00
b0f602b565 Fix post build script 2024-06-10 09:12:24 +02:00
84c774503d Build rework checkpoint 2024-06-10 07:57:12 +02:00
9bbc7cc651 Release candidate 2024-06-09 15:04:47 +02:00
458083fb6d Prettier stable generated code 2024-06-09 15:04:31 +02:00
8dcfc840b4 Remove useless 'as const' 2024-06-09 14:34:41 +02:00
9d06a3a6ad Release candidate 2024-06-09 14:33:42 +02:00
86cd08b954 Add missing file to the NPM bundle 2024-06-09 14:33:29 +02:00
45 changed files with 11753 additions and 674 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.42", "version": "10.0.0-rc.54",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -44,6 +44,7 @@
"dist/bin/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/constants.d.ts", "dist/bin/shared/constants.d.ts",
"dist/bin/shared/constants.js.map", "dist/bin/shared/constants.js.map",
"dist/bin/shared/buildContext.d.ts",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"dist/vite-plugin/index.d.ts", "dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts", "dist/vite-plugin/vite-plugin.d.ts",
@ -109,7 +110,6 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"recast": "^0.23.3", "recast": "^0.23.3",
"run-exclusive": "^2.2.19", "run-exclusive": "^2.2.19",
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2", "storybook-dark-mode": "^1.1.2",
"termost": "^0.12.0", "termost": "^0.12.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",

View File

@ -3,40 +3,83 @@ import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer"; import { SemVer } from "../src/bin/tools/SemVer";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import chalk from "chalk"; import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
run( (async () => {
[ {
`docker exec -it ${containerName}`, const dCompleted = new Deferred<void>();
`/opt/keycloak/bin/kc.sh export`,
`--dir /tmp`,
`--realm myrealm`,
`--users realm_file`
].join(" ")
);
const keycloakMajorVersionNumber = SemVer.parse( const child = child_process.spawn("docker", [
child_process ...["exec", containerName],
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`) ...["/opt/keycloak/bin/kc.sh", "export"],
.toString("utf8") ...["--dir", "/tmp"],
.trim() ...["--realm", "myrealm"],
.split(":")[1] ...["--users", "realm_file"]
).major; ]);
const targetFilePath = pathRelative( let output = "";
process.cwd(),
pathJoin(
__dirname,
"..",
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
)
);
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) { function run(command: string) {
console.log(chalk.grey(`$ ${command}`)); console.log(chalk.grey(`$ ${command}`));

View File

@ -1,63 +0,0 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext";
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "./tools/transformCodebase";
import type { CliCommandOptions } from "./main";
import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
console.log(
chalk.cyan(
"Select the Keycloak version from which you want to download the builtins theme:"
)
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: undefined,
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
const destDirPath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
console.log(
[
`Downloading builtins theme of Keycloak ${keycloakVersion} here:`,
`- ${chalk.bold(
`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`
)}`,
`- ${chalk.bold(
`.${pathSep}${pathJoin(
pathRelative(process.cwd(), destDirPath),
"keycloak"
)}`
)}`
].join("\n")
);
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion,
buildContext
});
transformCodebase({
srcDirPath: defaultThemeDirPath,
destDirPath
});
console.log(chalk.green(`✓ done`));
}

View File

@ -34,6 +34,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const { keycloakVersion } = await promptKeycloakVersion({ const { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary // NOTE: This is arbitrary
startingFromMajor: 17, startingFromMajor: 17,
excludeMajorVersions: [],
cacheDirPath: buildContext.cacheDirPath cacheDirPath: buildContext.cacheDirPath
}); });

View File

@ -32,12 +32,14 @@ export async function buildJar(params: {
jarFileBasename: string; jarFileBasename: string;
keycloakAccountV1Version: KeycloakAccountV1Version; keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion; keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
resourcesDirPath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { const {
jarFileBasename, jarFileBasename,
keycloakAccountV1Version, keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext buildContext
} = params; } = params;
@ -48,35 +50,10 @@ export async function buildJar(params: {
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true }); rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
{ transformCodebase({
const transformCodebase_common = (params: { srcDirPath: resourcesDirPath,
fileRelativePath: string; destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"),
sourceCode: Buffer; transformSourceCode:
}): { modifiedSourceCode: Buffer } | undefined => {
const { fileRelativePath, sourceCode } = params;
if (
fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath: "." })
) {
return { modifiedSourceCode: sourceCode };
}
for (const themeName of [...buildContext.themeNames, accountV1ThemeName]) {
if (
isInside({
dirPath: pathJoin("src", "main", "resources", "theme", themeName),
filePath: fileRelativePath
})
) {
return { modifiedSourceCode: sourceCode };
}
}
return undefined;
};
const transformCodebase_patchForUsingBuiltinAccountV1 =
keycloakAccountV1Version !== null keycloakAccountV1Version !== null
? undefined ? undefined
: (params: { : (params: {
@ -87,13 +64,7 @@ export async function buildJar(params: {
if ( if (
isInside({ isInside({
dirPath: pathJoin( dirPath: pathJoin("theme", accountV1ThemeName),
"src",
"main",
"resources",
"theme",
accountV1ThemeName
),
filePath: fileRelativePath filePath: fileRelativePath
}) })
) { ) {
@ -103,7 +74,7 @@ export async function buildJar(params: {
if ( if (
fileRelativePath === fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({ getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath: "." resourcesDirPath: "."
}) })
) { ) {
const keycloakThemesJsonParsed = JSON.parse( const keycloakThemesJsonParsed = JSON.parse(
@ -128,15 +99,7 @@ export async function buildJar(params: {
for (const themeName of buildContext.themeNames) { for (const themeName of buildContext.themeNames) {
if ( if (
fileRelativePath === fileRelativePath ===
pathJoin( pathJoin("theme", themeName, "account", "theme.properties")
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
)
) { ) {
const modifiedSourceCode = Buffer.from( const modifiedSourceCode = Buffer.from(
sourceCode sourceCode
@ -157,31 +120,8 @@ export async function buildJar(params: {
} }
return { modifiedSourceCode: sourceCode }; return { modifiedSourceCode: sourceCode };
}; }
});
transformCodebase({
srcDirPath: buildContext.keycloakifyBuildDirPath,
destDirPath: keycloakifyBuildTmpDirPath,
transformSourceCode: params => {
const resultCommon = transformCodebase_common(params);
if (transformCodebase_patchForUsingBuiltinAccountV1 === undefined) {
return resultCommon;
}
if (resultCommon === undefined) {
return undefined;
}
const { modifiedSourceCode } = resultCommon;
return transformCodebase_patchForUsingBuiltinAccountV1({
...params,
sourceCode: modifiedSourceCode
});
}
});
}
route_legacy_pages: { route_legacy_pages: {
// NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create // NOTE: If there's no account theme there is no special target for keycloak 24 and up so we create

View File

@ -8,7 +8,7 @@ import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar"; import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename"; import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes"; import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants"; import { accountV1ThemeName } from "../../shared/constants";
export type BuildContextLike = BuildContextLike_buildJar & { export type BuildContextLike = BuildContextLike_buildJar & {
@ -18,12 +18,14 @@ export type BuildContextLike = BuildContextLike_buildJar & {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: { export async function buildJars(params: {
resourcesDirPath: string;
onlyBuildJarFileBasename: string | undefined;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes({ const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath resourcesDirPath
}).themes.some(({ name }) => name === accountV1ThemeName); }).themes.some(({ name }) => name === accountV1ThemeName);
await Promise.all( await Promise.all(
@ -56,12 +58,20 @@ export async function buildJars(params: {
keycloakVersionRange keycloakVersionRange
}); });
if (
onlyBuildJarFileBasename !== undefined &&
onlyBuildJarFileBasename !== jarFileBasename
) {
return undefined;
}
return { return {
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename jarFileBasename
}; };
} }
) )
.filter(exclude(undefined))
.map( .map(
({ ({
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
@ -71,6 +81,7 @@ export async function buildJars(params: {
jarFileBasename, jarFileBasename,
keycloakAccountV1Version, keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext buildContext
}) })
) )

View File

@ -1,5 +1,5 @@
// NOTE: v0.5 is a dummy version. // NOTE: v0.5 is a dummy version.
export const keycloakAccountV1Versions = [null, "0.3", "0.4"] as const; export const keycloakAccountV1Versions = [null, "0.3", "0.4", "0.6"] as const;
/** /**
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1 * https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1

View File

@ -44,12 +44,20 @@ export function getKeycloakVersionRangeForJar(params: {
case null: case null:
return undefined; return undefined;
case "1.1.5": case "1.1.5":
return "24-and-above" as const; return "24" as const;
} }
assert< assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never> Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false); >(false);
case "0.6":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "25-and-above" as const;
}
} }
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
})(); })();
assert< assert<
@ -65,7 +73,6 @@ export function getKeycloakVersionRangeForJar(params: {
if (keycloakAccountV1Version !== null) { if (keycloakAccountV1Version !== null) {
return undefined; return undefined;
} }
switch (keycloakThemeAdditionalInfoExtensionVersion) { switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null: case null:
return "21-and-below"; return "21-and-below";

View File

@ -180,10 +180,42 @@ try {
<#if attribute.annotations.inputTypePlaceholder??> <#if attribute.annotations.inputTypePlaceholder??>
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"), "${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
</#if> </#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> </#list>
}; };
</#if> </#if>
attributes_to_attributesByName: {
if( !out["profile"] ){
break attributes_to_attributesByName;
}
if( !out["profile"]["attributes"] ){
break attributes_to_attributesByName;
}
var attributes = out["profile"]["attributes"];
delete out["profile"]["attributes"];
out["profile"]["attributesByName"] = {};
attributes.forEach(function(attribute){
out["profile"]["attributesByName"][attribute.name] = attribute;
});
}
return out; return out;
function decodeHtmlEntities(htmlStr){ function decodeHtmlEntities(htmlStr){
@ -287,8 +319,8 @@ function decodeHtmlEntities(htmlStr){
key == "realmAttributes" && key == "realmAttributes" &&
are_same_path(path, []) are_same_path(path, [])
) || ( ) || (
<#-- attributesByName adds a lot of noise to the output and is not needed --> <#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
key == "attributes" && key == "attributesByName" &&
are_same_path(path, ["profile"]) are_same_path(path, ["profile"])
) || ( ) || (
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object --> <#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
@ -304,7 +336,7 @@ function decodeHtmlEntities(htmlStr){
<#-- https://github.com/keycloakify/keycloakify/discussions/406 --> <#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if ( <#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"]) key == "attemptedUsername" && are_same_path(path, ["auth"])
)> )>
<#attempt> <#attempt>

View File

@ -13,13 +13,15 @@ import { transformCodebase } from "../../tools/transformCodebase";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; npmWorkspaceRootDirPath: string;
keycloakifyBuildDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function bringInAccountV1(params: { buildContext: BuildContextLike }) { export async function bringInAccountV1(params: {
const { buildContext } = params; resourcesDirPath: string;
buildContext: BuildContextLike;
}) {
const { resourcesDirPath, buildContext } = params;
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion: lastKeycloakVersionWithAccountV1, keycloakVersion: lastKeycloakVersionWithAccountV1,
@ -27,10 +29,7 @@ export async function bringInAccountV1(params: { buildContext: BuildContextLike
}); });
const accountV1DirPath = pathJoin( const accountV1DirPath = pathJoin(
buildContext.keycloakifyBuildDirPath, resourcesDirPath,
"src",
"main",
"resources",
"theme", "theme",
accountV1ThemeName, accountV1ThemeName,
"account" "account"

View File

@ -5,6 +5,8 @@ import {
type BuildContextLike as BuildContextLike_generateSrcMainResourcesForMainTheme type BuildContextLike as BuildContextLike_generateSrcMainResourcesForMainTheme
} from "./generateSrcMainResourcesForMainTheme"; } from "./generateSrcMainResourcesForMainTheme";
import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant"; import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant";
import fs from "fs";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildContextLike = BuildContextLike_generateSrcMainResourcesForMainTheme & { export type BuildContextLike = BuildContextLike_generateSrcMainResourcesForMainTheme & {
themeNames: string[]; themeNames: string[];
@ -14,21 +16,27 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateSrcMainResources(params: { export async function generateSrcMainResources(params: {
buildContext: BuildContextLike; buildContext: BuildContextLike;
resourcesDirPath: string;
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { resourcesDirPath, buildContext } = params;
const [themeName, ...themeVariantNames] = buildContext.themeNames; const [themeName, ...themeVariantNames] = buildContext.themeNames;
if (fs.existsSync(resourcesDirPath)) {
rmSync(resourcesDirPath, { recursive: true });
}
await generateSrcMainResourcesForMainTheme({ await generateSrcMainResourcesForMainTheme({
resourcesDirPath,
themeName, themeName,
buildContext buildContext
}); });
for (const themeVariantName of themeVariantNames) { for (const themeVariantName of themeVariantNames) {
generateSrcMainResourcesForThemeVariant({ generateSrcMainResourcesForThemeVariant({
resourcesDirPath,
themeName, themeName,
themeVariantName, themeVariantName
buildContext
}); });
} }
} }

View File

@ -43,14 +43,10 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources & BuildContextLike_downloadKeycloakStaticResources &
BuildContextLike_bringInAccountV1 & { BuildContextLike_bringInAccountV1 & {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string; loginThemeResourcesFromKeycloakVersion: string;
projectBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
}; };
@ -58,9 +54,10 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateSrcMainResourcesForMainTheme(params: { export async function generateSrcMainResourcesForMainTheme(params: {
themeName: string; themeName: string;
resourcesDirPath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { themeName, buildContext } = params; const { themeName, resourcesDirPath, buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath projectDirPath: buildContext.projectDirPath
@ -68,15 +65,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params; const { themeType } = params;
return pathJoin( return pathJoin(resourcesDirPath, "theme", themeName, themeType);
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
}; };
const cssGlobalsToDefine: Record<string, string> = {}; const cssGlobalsToDefine: Record<string, string> = {};
@ -207,8 +196,6 @@ export async function generateSrcMainResourcesForMainTheme(params: {
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeTypeDirPath, { recursive: true });
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId), pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8") Buffer.from(ftlCode, "utf8")
@ -291,6 +278,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
if (implementedThemeTypes.account) { if (implementedThemeTypes.account) {
await bringInAccountV1({ await bringInAccountV1({
resourcesDirPath,
buildContext buildContext
}); });
} }
@ -313,7 +301,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
} }
writeMetaInfKeycloakThemes({ writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath, resourcesDirPath,
metaInfKeycloakThemes metaInfKeycloakThemes
}); });
} }

View File

@ -2,7 +2,7 @@ import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { import {
readMetaInfKeycloakThemes, readMetaInfKeycloakThemes_fromResourcesDirPath,
writeMetaInfKeycloakThemes writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes"; } from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -14,20 +14,13 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export function generateSrcMainResourcesForThemeVariant(params: { export function generateSrcMainResourcesForThemeVariant(params: {
resourcesDirPath: string;
themeName: string; themeName: string;
themeVariantName: string; themeVariantName: string;
buildContext: BuildContextLike;
}) { }) {
const { themeName, themeVariantName, buildContext } = params; const { resourcesDirPath, themeName, themeVariantName } = params;
const mainThemeDirPath = pathJoin( const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName
);
transformCodebase({ transformCodebase({
srcDirPath: mainThemeDirPath, srcDirPath: mainThemeDirPath,
@ -57,9 +50,10 @@ export function generateSrcMainResourcesForThemeVariant(params: {
}); });
{ {
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({ const updatedMetaInfKeycloakThemes =
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath readMetaInfKeycloakThemes_fromResourcesDirPath({
}); resourcesDirPath
});
updatedMetaInfKeycloakThemes.themes.push({ updatedMetaInfKeycloakThemes.themes.push({
name: themeVariantName, name: themeVariantName,
@ -73,7 +67,7 @@ export function generateSrcMainResourcesForThemeVariant(params: {
}); });
writeMetaInfKeycloakThemes({ writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath, resourcesDirPath,
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
}); });
} }

View File

@ -1,74 +0,0 @@
import * as fs from "fs";
import {
join as pathJoin,
relative as pathRelative,
basename as pathBasename
} from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import { accountV1ThemeName } from "../shared/constants";
export type BuildContextLike = {
keycloakifyBuildDirPath: string;
themeNames: string[];
};
assert<BuildContext extends BuildContextLike ? true : false>();
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container";
const keycloakVersion = "24.0.4";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: {
jarFilePath: string;
doesImplementAccountTheme: boolean;
buildContext: BuildContextLike;
}) {
const { jarFilePath, doesImplementAccountTheme, buildContext } = params;
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
fs.writeFileSync(
pathJoin(
buildContext.keycloakifyBuildDirPath,
generateStartKeycloakTestingContainer.basename
),
Buffer.from(
[
"#!/usr/bin/env bash",
"",
`docker rm ${containerName} || true`,
"",
`cd "${buildContext.keycloakifyBuildDirPath}"`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
` -v "${pathJoin(
"$(pwd)",
pathRelative(buildContext.keycloakifyBuildDirPath, jarFilePath)
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
[
...(doesImplementAccountTheme ? [accountV1ThemeName] : []),
...buildContext.themeNames
].map(
themeName =>
` -v "${pathJoin(
"$(pwd)",
themeRelativeDirPath,
themeName
).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
""
].join("\n"),
"utf8"
),
{ mode: 0o755 }
);
}

View File

@ -3,12 +3,16 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { vitePluginSubScriptEnvNames, skipBuildJarsEnvName } from "../shared/constants"; import {
vitePluginSubScriptEnvNames,
onlyBuildJarFileBasenameEnvName
} from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os"; import * as os from "os";
import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
exit_if_maven_not_installed: { exit_if_maven_not_installed: {
@ -76,7 +80,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
); );
} }
await generateSrcMainResources({ buildContext }); const resourcesDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "resources");
await generateSrcMainResources({
resourcesDirPath,
buildContext
});
run_post_build_script: { run_post_build_script: {
if (buildContext.bundler !== "vite") { if (buildContext.bundler !== "vite") {
@ -93,15 +102,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
} }
build_jars: { await buildJars({
if (process.env[skipBuildJarsEnvName]) { resourcesDirPath,
break build_jars; buildContext,
} onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName]
});
await buildJars({ buildContext }); rmSync(resourcesDirPath, { recursive: true });
}
console.log( console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`) chalk.green(
`✓ keycloak theme built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
)
); );
} }

View File

@ -134,20 +134,6 @@ program
} }
}); });
program
.command({
name: "download-keycloak-default-theme",
description: "Download the built-in Keycloak theme."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./download-keycloak-default-theme");
await command({ cliCommandOptions });
}
});
program program
.command({ .command({
name: "eject-page", name: "eject-page",

View File

@ -5,5 +5,5 @@ export type KeycloakVersionRange =
export namespace KeycloakVersionRange { export namespace KeycloakVersionRange {
export type WithoutAccountTheme = "21-and-below" | "22-and-above"; export type WithoutAccountTheme = "21-and-below" | "22-and-above";
export type WithAccountTheme = "21-and-below" | "23" | "24-and-above"; export type WithAccountTheme = "21-and-below" | "23" | "24" | "25-and-above";
} }

View File

@ -12,7 +12,7 @@ import { vitePluginSubScriptEnvNames } from "./constants";
export type BuildContext = { export type BuildContext = {
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
themeVersion: string; themeVersion: string;
themeNames: string[]; themeNames: [string, ...string[]];
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
groupId: string; groupId: string;
artifactId: string; artifactId: string;
@ -147,7 +147,7 @@ export function getBuildContext(params: {
...resolvedViteConfig?.buildOptions ...resolvedViteConfig?.buildOptions
}; };
const themeNames = (() => { const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return [
parsedPackageJson.name parsedPackageJson.name
@ -161,7 +161,11 @@ export function getBuildContext(params: {
return [buildOptions.themeName]; return [buildOptions.themeName];
} }
return buildOptions.themeName; const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
assert(mainThemeName !== undefined);
return [mainThemeName, ...themeVariantNames];
})(); })();
const projectBuildDirPath = (() => { const projectBuildDirPath = (() => {

View File

@ -16,7 +16,7 @@ export const vitePluginSubScriptEnvNames = {
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG" resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const; } as const;
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR"; export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME";
export const loginThemePageIds = [ export const loginThemePageIds = [
"login.ftl", "login.ftl",

View File

@ -46,7 +46,7 @@ export async function generateKcGenTs(params: {
``, ``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`, `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( `export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries( Object.fromEntries(
@ -58,7 +58,8 @@ export async function generateKcGenTs(params: {
2 2
)};`, )};`,
``, ``,
`/* prettier-ignore-end */` `/* prettier-ignore-end */`,
``
].join("\n"), ].join("\n"),
"utf8" "utf8"
); );

View File

@ -1,50 +1,73 @@
import { join as pathJoin, dirname as pathDirname } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants"; import type { ThemeType } from "./constants";
import * as fs from "fs"; import * as fs from "fs";
import { assert } from "tsafe/assert";
import { extractArchive } from "../tools/extractArchive";
export type MetaInfKeycloakTheme = { export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[]; themes: { name: string; types: (ThemeType | "email")[] }[];
}; };
export function getMetaInfKeycloakThemesJsonFilePath(params: { export function getMetaInfKeycloakThemesJsonFilePath(params: {
keycloakifyBuildDirPath: string; resourcesDirPath: string;
}) { }) {
const { keycloakifyBuildDirPath } = params; const { resourcesDirPath } = params;
return pathJoin( return pathJoin(
keycloakifyBuildDirPath === "." ? "" : keycloakifyBuildDirPath, resourcesDirPath === "." ? "" : resourcesDirPath,
"src",
"main",
"resources",
"META-INF", "META-INF",
"keycloak-themes.json" "keycloak-themes.json"
); );
} }
export function readMetaInfKeycloakThemes(params: { export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
keycloakifyBuildDirPath: string; resourcesDirPath: string;
}): MetaInfKeycloakTheme { }) {
const { keycloakifyBuildDirPath } = params; const { resourcesDirPath } = params;
return JSON.parse( return JSON.parse(
fs fs
.readFileSync( .readFileSync(
getMetaInfKeycloakThemesJsonFilePath({ getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath resourcesDirPath
}) })
) )
.toString("utf8") .toString("utf8")
) as MetaInfKeycloakTheme; ) as MetaInfKeycloakTheme;
} }
export async function readMetaInfKeycloakThemes_fromJar(params: {
jarFilePath: string;
}): Promise<MetaInfKeycloakTheme> {
const { jarFilePath } = params;
let metaInfKeycloakThemes: MetaInfKeycloakTheme | undefined = undefined;
await extractArchive({
archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
if (
relativeFilePathInArchive ===
getMetaInfKeycloakThemesJsonFilePath({ resourcesDirPath: "." })
) {
metaInfKeycloakThemes = JSON.parse((await readFile()).toString("utf8"));
earlyExit();
}
}
});
assert(metaInfKeycloakThemes !== undefined);
return metaInfKeycloakThemes;
}
export function writeMetaInfKeycloakThemes(params: { export function writeMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string; resourcesDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme; metaInfKeycloakThemes: MetaInfKeycloakTheme;
}) { }) {
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params; const { resourcesDirPath, metaInfKeycloakThemes } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({ const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath resourcesDirPath
}); });
{ {

View File

@ -9,9 +9,10 @@ import { id } from "tsafe/id";
export async function promptKeycloakVersion(params: { export async function promptKeycloakVersion(params: {
startingFromMajor: number | undefined; startingFromMajor: number | undefined;
excludeMajorVersions: number[];
cacheDirPath: string; cacheDirPath: string;
}) { }) {
const { startingFromMajor, cacheDirPath } = params; const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params;
const { getLatestsSemVersionedTag } = (() => { const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => { const { octokit } = (() => {
@ -95,6 +96,10 @@ export async function promptKeycloakVersion(params: {
return; return;
} }
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
return;
}
const currentSemVersionedTag = semVersionedTagByMajor.get( const currentSemVersionedTag = semVersionedTagByMajor.get(
semVersionedTag.version.major semVersionedTag.version.major
); );

View File

@ -1,4 +1,4 @@
import { skipBuildJarsEnvName } from "../shared/constants"; import { onlyBuildJarFileBasenameEnvName } from "../shared/constants";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -14,10 +14,10 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function keycloakifyBuild(params: { export async function keycloakifyBuild(params: {
doSkipBuildJars: boolean; onlyBuildJarFileBasename: string | undefined;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> { }): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
const { buildContext, doSkipBuildJars } = params; const { buildContext, onlyBuildJarFileBasename } = params;
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
@ -25,7 +25,7 @@ export async function keycloakifyBuild(params: {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
...(doSkipBuildJars ? { [skipBuildJarsEnvName]: "true" } : {}) [onlyBuildJarFileBasenameEnvName]: onlyBuildJarFileBasename
} }
}); });

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -587,7 +587,9 @@
"publicClient": true, "publicClient": true,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -619,7 +621,9 @@
"publicClient": false, "publicClient": false,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -695,7 +699,9 @@
"publicClient": false, "publicClient": false,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -783,6 +789,7 @@
"config": { "config": {
"introspection.token.claim": "true", "introspection.token.claim": "true",
"multivalued": "true", "multivalued": "true",
"userinfo.token.claim": "true",
"user.attribute": "foo", "user.attribute": "foo",
"id.token.claim": "true", "id.token.claim": "true",
"access.token.claim": "true", "access.token.claim": "true",
@ -827,7 +834,8 @@
"config": { "config": {
"id.token.claim": "true", "id.token.claim": "true",
"introspection.token.claim": "true", "introspection.token.claim": "true",
"access.token.claim": "true" "access.token.claim": "true",
"userinfo.token.claim": "true"
} }
} }
] ]
@ -1348,10 +1356,10 @@
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
@ -1423,13 +1431,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-full-name-mapper", "oidc-address-mapper",
"saml-user-property-mapper", "oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
} }
@ -2043,7 +2051,7 @@
"name": "Terms and Conditions", "name": "Terms and Conditions",
"providerId": "TERMS_AND_CONDITIONS", "providerId": "TERMS_AND_CONDITIONS",
"enabled": true, "enabled": true,
"defaultAction": false, "defaultAction": true,
"priority": 20, "priority": 20,
"config": {} "config": {}
}, },
@ -2122,8 +2130,8 @@
"cibaExpiresIn": "120", "cibaExpiresIn": "120",
"cibaAuthRequestedUserHint": "login_hint", "cibaAuthRequestedUserHint": "login_hint",
"oauth2DeviceCodeLifespan": "600", "oauth2DeviceCodeLifespan": "600",
"oauth2DevicePollingInterval": "5",
"clientOfflineSessionMaxLifespan": "0", "clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5",
"clientSessionIdleTimeout": "0", "clientSessionIdleTimeout": "0",
"parRequestUriLifespan": "60", "parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0", "clientSessionMaxLifespan": "0",

View File

@ -1501,14 +1501,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"saml-user-property-mapper", "saml-user-property-mapper"
"oidc-usermodel-attribute-mapper"
] ]
} }
}, },
@ -1541,13 +1541,13 @@
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-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-role-list-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper"
"oidc-full-name-mapper"
] ]
} }
}, },
@ -2200,7 +2200,7 @@
"name": "Terms and Conditions", "name": "Terms and Conditions",
"providerId": "TERMS_AND_CONDITIONS", "providerId": "TERMS_AND_CONDITIONS",
"enabled": true, "enabled": true,
"defaultAction": false, "defaultAction": true,
"priority": 20, "priority": 20,
"config": {} "config": {}
}, },
@ -2307,7 +2307,7 @@
"cibaInterval": "5", "cibaInterval": "5",
"realmReusableOtpCode": "false" "realmReusableOtpCode": "false"
}, },
"keycloakVersion": "24.0.4", "keycloakVersion": "24.0.5",
"userManagedAccessAllowed": false, "userManagedAccessAllowed": false,
"clientProfiles": { "clientProfiles": {
"profiles": [] "profiles": []

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes"; import { readMetaInfKeycloakThemes_fromJar } from "../shared/metaInfKeycloakThemes";
import { accountV1ThemeName, containerName } from "../shared/constants"; import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange"; import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
@ -21,6 +21,9 @@ import * as runExclusive from "run-exclusive";
import { extractArchive } from "../tools/extractArchive"; import { extractArchive } from "../tools/extractArchive";
import { appBuild } from "./appBuild"; import { appBuild } from "./appBuild";
import { keycloakifyBuild } from "./keycloakifyBuild"; import { keycloakifyBuild } from "./keycloakifyBuild";
import { isInside } from "../tools/isInside";
import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
export type CliCommandOptions = CliCommandOptions_common & { export type CliCommandOptions = CliCommandOptions_common & {
port: number; port: number;
@ -98,7 +101,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: false, onlyBuildJarFileBasename: undefined,
buildContext buildContext
}); });
@ -112,13 +115,31 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
} }
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({ const { doesImplementAccountTheme } = await (async () => {
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath const latestJarFilePath = fs
}); .readdirSync(buildContext.keycloakifyBuildDirPath)
.filter(fileBasename => fileBasename.endsWith(".jar"))
.map(fileBasename =>
pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename)
)
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some( assert(latestJarFilePath !== undefined);
({ name }) => name === accountV1ThemeName
); const metaInfKeycloakThemes = await readMetaInfKeycloakThemes_fromJar({
jarFilePath: latestJarFilePath
});
const mainThemeEntry = metaInfKeycloakThemes.themes.find(
({ name }) => name === buildContext.themeNames[0]
);
assert(mainThemeEntry !== undefined);
const doesImplementAccountTheme = mainThemeEntry.types.includes("account");
return { doesImplementAccountTheme };
})();
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } = const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } =
await (async function getKeycloakMajor(): Promise<{ await (async function getKeycloakMajor(): Promise<{
@ -138,7 +159,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
); );
const { keycloakVersion } = await promptKeycloakVersion({ const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 17, startingFromMajor: 18,
excludeMajorVersions: [22],
cacheDirPath: buildContext.cacheDirPath cacheDirPath: buildContext.cacheDirPath
}); });
@ -172,7 +194,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return "23" as const; return "23" as const;
} }
return "24-and-above" as const; if (keycloakMajorVersionNumber === 24) {
return "24" as const;
}
return "25-and-above" as const;
})(); })();
assert< assert<
@ -206,6 +232,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const realmJsonFilePath = await (async () => { const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) { if (cliCommandOptions.realmJsonFilePath !== undefined) {
if (cliCommandOptions.realmJsonFilePath === "none") {
return undefined;
}
console.log( console.log(
chalk.green( chalk.green(
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}` `Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
@ -262,65 +292,34 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename); const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename);
const { doUseBuiltInAccountV1Theme } = await (async () => { async function extractThemeResourcesFromJar() {
let doUseBuiltInAccountV1Theme = false;
await extractArchive({ await extractArchive({
archiveFilePath: jarFilePath, archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => { onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
for (const themeName of buildContext.themeNames) { if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
if ( await writeFile({
relativeFilePathInArchive === filePath: pathJoin(
pathJoin("theme", themeName, "account", "theme.properties") buildContext.keycloakifyBuildDirPath,
) { relativeFilePathInArchive
if ( )
(await readFile()) });
.toString("utf8")
.includes("parent=keycloak")
) {
doUseBuiltInAccountV1Theme = true;
}
earlyExit();
}
} }
} }
}); });
}
return { doUseBuiltInAccountV1Theme }; {
})(); const destDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "theme");
if (await existsAsync(destDirPath)) {
await rm(destDirPath, { recursive: true });
}
}
const accountThemePropertyPatch = !doUseBuiltInAccountV1Theme await extractThemeResourcesFromJar();
? undefined
: () => {
for (const themeName of buildContext.themeNames) {
const filePath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
);
const sourceCode = fs.readFileSync(filePath); const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename);
const modifiedSourceCode = Buffer.from( fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
sourceCode
.toString("utf8")
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
);
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
fs.writeFileSync(filePath, modifiedSourceCode);
}
};
accountThemePropertyPatch?.();
try { try {
child_process.execSync(`docker rm --force ${containerName}`, { child_process.execSync(`docker rm --force ${containerName}`, {
@ -342,20 +341,28 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
"-v", "-v",
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json` `${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 ...(keycloakMajorVersionNumber <= 20
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
: []), : []),
...[ ...[
...buildContext.themeNames, ...buildContext.themeNames,
...(doUseBuiltInAccountV1Theme ? [] : [accountV1ThemeName]) ...(fs.existsSync(
pathJoin(
buildContext.keycloakifyBuildDirPath,
"theme",
accountV1ThemeName
)
)
? [accountV1ThemeName]
: [])
] ]
.map(themeName => ({ .map(themeName => ({
localDirPath: pathJoin( localDirPath: pathJoin(
buildContext.keycloakifyBuildDirPath, buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme", "theme",
themeName themeName
), ),
@ -451,7 +458,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: true, onlyBuildJarFileBasename: jarFileBasename,
buildContext buildContext
}); });
@ -459,7 +466,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return; return;
} }
accountThemePropertyPatch?.(); await extractThemeResourcesFromJar();
console.log(chalk.green("Theme rebuilt and updated in Keycloak.")); console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
}); });

View File

@ -4,7 +4,7 @@ import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { I18n } from "keycloakify/login/i18n"; import type { I18n } from "keycloakify/login/i18n";
import type { KcContext } from "keycloakify/login/KcContext"; 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 Login = lazy(() => import("keycloakify/login/pages/Login"));
const Register = lazy(() => import("keycloakify/login/pages/Register")); const Register = lazy(() => import("keycloakify/login/pages/Register"));

View File

@ -209,17 +209,13 @@ export declare namespace KcContext {
export type Register = Common & { export type Register = Common & {
pageId: "register.ftl"; pageId: "register.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
url: { url: {
registrationAction: string; registrationAction: string;
}; };
passwordRequired: boolean; passwordRequired: boolean;
recaptchaRequired: boolean; recaptchaRequired: boolean;
recaptchaSiteKey?: string; 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; termsAcceptanceRequired?: boolean;
}; };
@ -479,16 +475,19 @@ export declare namespace KcContext {
export type LoginUpdateProfile = Common & { export type LoginUpdateProfile = Common & {
pageId: "login-update-profile.ftl"; pageId: "login-update-profile.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type IdpReviewUserProfile = Common & { export type IdpReviewUserProfile = Common & {
pageId: "idp-review-user-profile.ftl"; pageId: "idp-review-user-profile.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type UpdateEmail = Common & { export type UpdateEmail = Common & {
pageId: "update-email.ftl"; pageId: "update-email.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type SelectAuthenticator = Common & { export type SelectAuthenticator = Common & {
@ -752,6 +751,10 @@ export declare namespace Validators {
assert<Equals<OnlyInExpected, never>>(); 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 = { export type PasswordPolicies = {
/** The minimum length of the password */ /** The minimum length of the password */
length?: number; length?: number;

View File

@ -4,33 +4,15 @@ import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import { import {
useUserProfileForm, useUserProfileForm,
getButtonToDisplayForMultivaluedAttributeField, getButtonToDisplayForMultivaluedAttributeField,
type KcContextLike,
type FormAction, type FormAction,
type FormFieldError type FormFieldError
} from "keycloakify/login/lib/useUserProfileForm"; } from "keycloakify/login/lib/useUserProfileForm";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
import type { Attribute } from "keycloakify/login/KcContext"; import type { Attribute } from "keycloakify/login/KcContext";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
export type UserProfileFormFieldsProps = { export default function UserProfileFormFields(props: UserProfileFormFieldsProps<KcContext, I18n>) {
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) {
const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props; const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props;
const { advancedMsg } = i18n; const { advancedMsg } = i18n;

View 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;
};

View File

@ -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; kcContext: KcContextLike;
i18n: I18n; i18n: I18n;
doMakeUserConfirmPassword: boolean; 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 { kcContext, i18n, doMakeUserConfirmPassword } = params;
const { insertScriptTags } = useInsertScriptTags({ const { insertScriptTags } = useInsertScriptTags({
@ -130,168 +130,137 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const initialState = useMemo((): internal.State => { const initialState = useMemo((): internal.State => {
// NOTE: We don't use te kcContext.profile.attributes directly because // 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. // 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. // We also want to apply some retro-compatibility and consistency patches.
// Finally we want to patch the changes made by Keycloak on the attributes format so we have an homogeneous const attributes: Attribute[] = (() => {
// attributes format to work with. mock_user_profile_attributes_for_older_keycloak_versions: {
const syntheticAttributes = (() => { if (
const syntheticAttributes: Attribute[] = []; "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 = (() => { if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
retrocompat_patch: { //NOTE: Handle legacy register.ftl page
if ( return (["firstName", "lastName", "email", "username"] as const)
"profile" in kcContext && .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
"attributesByName" in kcContext.profile && .map(name =>
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 [
id<Attribute>({ id<Attribute>({
name: "email", name: name,
displayName: id<`\${${MessageKey}}`>(`\${email}`), displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true, required: true,
value: (kcContext.email as any).value ?? "", value: (kcContext.register as any).formData[name] ?? "",
html5DataAnnotations: {}, html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, 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 ("user" in kcContext && kcContext.user instanceof Object) {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") { //NOTE: Handle legacy login-update-profile.ftl
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = return (["username", "email", "firstName", "lastName"] as const)
attribute_pre_group_patch as Attribute & { .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
group: string; .map(name =>
groupDisplayHeader?: string; id<Attribute>({
groupDisplayDescription?: string; name: name,
groupAnnotations: Record<string, string>; 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>({ if ("email" in kcContext && kcContext.email instanceof Object) {
...rest, //NOTE: Handle legacy update-email.ftl
group: { return [
name: group, id<Attribute>({
displayHeader: groupDisplayHeader, name: "email",
displayDescription: groupDisplayDescription, displayName: id<`\${${MessageKey}}`>(`\${email}`),
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}"),
required: true, required: true,
value: (kcContext.email as any).value ?? "",
html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, annotations: {},
autocomplete: "new-password", autocomplete: "email"
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 {})
}
);
} }
assert(false, "Unable to mock user profile from the current kcContext");
} }
// NOTE: Consistency patch return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
syntheticAttributes.forEach(attribute => { })();
// 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 })) { if (getIsMultivaluedSingleField({ attribute })) {
attribute.multivalued = true; attribute.multivalued = true;
} }
@ -303,65 +272,98 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
attribute.value ??= attribute.values?.[0]; attribute.value ??= attribute.values?.[0];
delete attribute.values; delete attribute.values;
} }
}); }
});
return syntheticAttributes; add_password_and_password_confirm: {
})(); if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
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 ?? ""
});
} }
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 = { const initialState: internal.State = {
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({

View File

@ -2,7 +2,7 @@ import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; 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 { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; 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 { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";

View File

@ -3,7 +3,7 @@ import { Markdown } from "keycloakify/tools/Markdown";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms"; import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; 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 { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";

View File

@ -1,7 +1,7 @@
import { useState } from "react"; import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; 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 { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";

View File

@ -1,4 +1,4 @@
export const formatNumber = (input: string, format: string): string => { export const formatNumber = (input: string, format: string) => {
if (!input) { if (!input) {
return ""; return "";
} }
@ -20,7 +20,8 @@ export const formatNumber = (input: string, format: string): string => {
let rawValue = input.replace(/\D+/g, ""); let rawValue = input.replace(/\D+/g, "");
// make sure the value is a number // 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 ""; return "";
} }

View File

@ -33,7 +33,7 @@ export function keycloakify(params?: Params) {
let shouldGenerateSourcemap: boolean | undefined = undefined; let shouldGenerateSourcemap: boolean | undefined = undefined;
const plugin = { const plugin = {
name: "keycloakify" as const, name: "keycloakify",
configResolved: async resolvedConfig => { configResolved: async resolvedConfig => {
shouldGenerateSourcemap = resolvedConfig.build.sourcemap !== false; shouldGenerateSourcemap = resolvedConfig.build.sourcemap !== false;
@ -47,6 +47,10 @@ export function keycloakify(params?: Params) {
const buildContext = JSON.parse(envValue) as BuildContext; const buildContext = JSON.parse(envValue) as BuildContext;
process.chdir(
pathJoin(buildContext.keycloakifyBuildDirPath, "resources")
);
await postBuild?.(buildContext); await postBuild?.(buildContext);
process.exit(0); process.exit(0);

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import DefaultPage from "../../dist/account/Fallback"; import DefaultPage from "../../dist/account/DefaultPage";
import { useI18n } from "./i18n"; import { useI18n } from "./i18n";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import Template from "../../dist/account/Template"; import Template from "../../dist/account/Template";

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import DefaultPage from "../../dist/login/Fallback"; import DefaultPage from "../../dist/login/DefaultPage";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n"; import { useI18n } from "./i18n";
import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms"; import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms";

View File

@ -11144,11 +11144,6 @@ schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.1.2:
ajv "^6.12.5" ajv "^6.12.5"
ajv-keywords "^3.5.2" 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: semver-compare@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc"