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",
"version": "10.0.0-rc.42",
"version": "10.0.0-rc.54",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -44,6 +44,7 @@
"dist/bin/shared/constants.js",
"dist/bin/shared/constants.d.ts",
"dist/bin/shared/constants.js.map",
"dist/bin/shared/buildContext.d.ts",
"!dist/vite-plugin/",
"dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts",
@ -109,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",

View File

@ -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}`));

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({
// NOTE: This is arbitrary
startingFromMajor: 17,
excludeMajorVersions: [],
cacheDirPath: buildContext.cacheDirPath
});

View File

@ -32,12 +32,14 @@ export async function buildJar(params: {
jarFileBasename: string;
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
resourcesDirPath: string;
buildContext: BuildContextLike;
}): Promise<void> {
const {
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext
} = params;
@ -48,35 +50,10 @@ export async function buildJar(params: {
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
{
const transformCodebase_common = (params: {
fileRelativePath: string;
sourceCode: Buffer;
}): { 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 =
transformCodebase({
srcDirPath: resourcesDirPath,
destDirPath: pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources"),
transformSourceCode:
keycloakAccountV1Version !== null
? undefined
: (params: {
@ -87,13 +64,7 @@ export async function buildJar(params: {
if (
isInside({
dirPath: pathJoin(
"src",
"main",
"resources",
"theme",
accountV1ThemeName
),
dirPath: pathJoin("theme", accountV1ThemeName),
filePath: fileRelativePath
})
) {
@ -103,7 +74,7 @@ export async function buildJar(params: {
if (
fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath: "."
resourcesDirPath: "."
})
) {
const keycloakThemesJsonParsed = JSON.parse(
@ -128,15 +99,7 @@ export async function buildJar(params: {
for (const themeName of buildContext.themeNames) {
if (
fileRelativePath ===
pathJoin(
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
)
pathJoin("theme", themeName, "account", "theme.properties")
) {
const modifiedSourceCode = Buffer.from(
sourceCode
@ -157,31 +120,8 @@ export async function buildJar(params: {
}
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: {
// 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 type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { readMetaInfKeycloakThemes_fromResourcesDirPath } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants";
export type BuildContextLike = BuildContextLike_buildJar & {
@ -18,12 +18,14 @@ export type BuildContextLike = BuildContextLike_buildJar & {
assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: {
resourcesDirPath: string;
onlyBuildJarFileBasename: string | undefined;
buildContext: BuildContextLike;
}): Promise<void> {
const { buildContext } = params;
const { onlyBuildJarFileBasename, resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath
const doesImplementAccountTheme = readMetaInfKeycloakThemes_fromResourcesDirPath({
resourcesDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
await Promise.all(
@ -56,12 +58,20 @@ export async function buildJars(params: {
keycloakVersionRange
});
if (
onlyBuildJarFileBasename !== undefined &&
onlyBuildJarFileBasename !== jarFileBasename
) {
return undefined;
}
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
};
}
)
.filter(exclude(undefined))
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
@ -71,6 +81,7 @@ export async function buildJars(params: {
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext
})
)

View File

@ -1,5 +1,5 @@
// 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

View File

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

View File

@ -180,10 +180,42 @@ 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>
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;
function decodeHtmlEntities(htmlStr){
@ -287,8 +319,8 @@ function decodeHtmlEntities(htmlStr){
key == "realmAttributes" &&
are_same_path(path, [])
) || (
<#-- attributesByName adds a lot of noise to the output and is not needed -->
key == "attributes" &&
<#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
key == "attributesByName" &&
are_same_path(path, ["profile"])
) || (
<#-- 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 -->
<#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>

View File

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

View File

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

View File

@ -43,14 +43,10 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources &
BuildContextLike_bringInAccountV1 & {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string;
projectBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
projectDirPath: string;
keycloakifyBuildDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
};
@ -58,9 +54,10 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateSrcMainResourcesForMainTheme(params: {
themeName: string;
resourcesDirPath: string;
buildContext: BuildContextLike;
}): Promise<void> {
const { themeName, buildContext } = params;
const { themeName, resourcesDirPath, buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
@ -68,15 +65,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
return pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
return pathJoin(resourcesDirPath, "theme", themeName, themeType);
};
const cssGlobalsToDefine: Record<string, string> = {};
@ -207,8 +196,6 @@ export async function generateSrcMainResourcesForMainTheme(params: {
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeTypeDirPath, { recursive: true });
fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8")
@ -291,6 +278,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
if (implementedThemeTypes.account) {
await bringInAccountV1({
resourcesDirPath,
buildContext
});
}
@ -313,7 +301,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
}
writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath,
resourcesDirPath,
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 type { BuildContext } from "../../shared/buildContext";
import {
readMetaInfKeycloakThemes,
readMetaInfKeycloakThemes_fromResourcesDirPath,
writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
@ -14,20 +14,13 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateSrcMainResourcesForThemeVariant(params: {
resourcesDirPath: string;
themeName: string;
themeVariantName: string;
buildContext: BuildContextLike;
}) {
const { themeName, themeVariantName, buildContext } = params;
const { resourcesDirPath, themeName, themeVariantName } = params;
const mainThemeDirPath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName
);
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
transformCodebase({
srcDirPath: mainThemeDirPath,
@ -57,9 +50,10 @@ export function generateSrcMainResourcesForThemeVariant(params: {
});
{
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath
});
const updatedMetaInfKeycloakThemes =
readMetaInfKeycloakThemes_fromResourcesDirPath({
resourcesDirPath
});
updatedMetaInfKeycloakThemes.themes.push({
name: themeVariantName,
@ -73,7 +67,7 @@ export function generateSrcMainResourcesForThemeVariant(params: {
});
writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath,
resourcesDirPath,
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 fs from "fs";
import { getBuildContext } from "../shared/buildContext";
import { vitePluginSubScriptEnvNames, skipBuildJarsEnvName } from "../shared/constants";
import {
vitePluginSubScriptEnvNames,
onlyBuildJarFileBasenameEnvName
} from "../shared/constants";
import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main";
import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os";
import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
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: {
if (buildContext.bundler !== "vite") {
@ -93,15 +102,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
});
}
build_jars: {
if (process.env[skipBuildJarsEnvName]) {
break build_jars;
}
await buildJars({
resourcesDirPath,
buildContext,
onlyBuildJarFileBasename: process.env[onlyBuildJarFileBasenameEnvName]
});
await buildJars({ buildContext });
}
rmSync(resourcesDirPath, { recursive: true });
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
.command({
name: "eject-page",

View File

@ -5,5 +5,5 @@ export type KeycloakVersionRange =
export namespace KeycloakVersionRange {
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 = {
bundler: "vite" | "webpack";
themeVersion: string;
themeNames: string[];
themeNames: [string, ...string[]];
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
@ -147,7 +147,7 @@ export function getBuildContext(params: {
...resolvedViteConfig?.buildOptions
};
const themeNames = (() => {
const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) {
return [
parsedPackageJson.name
@ -161,7 +161,11 @@ export function getBuildContext(params: {
return [buildOptions.themeName];
}
return buildOptions.themeName;
const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
assert(mainThemeName !== undefined);
return [mainThemeName, ...themeVariantNames];
})();
const projectBuildDirPath = (() => {

View File

@ -16,7 +16,7 @@ export const vitePluginSubScriptEnvNames = {
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const;
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR";
export const onlyBuildJarFileBasenameEnvName = "KEYCLOAKIFY_ONLY_BUILD_JAR_FILE_BASENAME";
export const loginThemePageIds = [
"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 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(
@ -58,7 +58,8 @@ export async function generateKcGenTs(params: {
2
)};`,
``,
`/* prettier-ignore-end */`
`/* prettier-ignore-end */`,
``
].join("\n"),
"utf8"
);

View File

@ -1,50 +1,73 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants";
import * as fs from "fs";
import { assert } from "tsafe/assert";
import { extractArchive } from "../tools/extractArchive";
export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[];
};
export function getMetaInfKeycloakThemesJsonFilePath(params: {
keycloakifyBuildDirPath: string;
resourcesDirPath: string;
}) {
const { keycloakifyBuildDirPath } = params;
const { resourcesDirPath } = params;
return pathJoin(
keycloakifyBuildDirPath === "." ? "" : keycloakifyBuildDirPath,
"src",
"main",
"resources",
resourcesDirPath === "." ? "" : resourcesDirPath,
"META-INF",
"keycloak-themes.json"
);
}
export function readMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string;
}): MetaInfKeycloakTheme {
const { keycloakifyBuildDirPath } = params;
export function readMetaInfKeycloakThemes_fromResourcesDirPath(params: {
resourcesDirPath: string;
}) {
const { resourcesDirPath } = params;
return JSON.parse(
fs
.readFileSync(
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath
resourcesDirPath
})
)
.toString("utf8")
) 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: {
keycloakifyBuildDirPath: string;
resourcesDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme;
}) {
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
const { resourcesDirPath, metaInfKeycloakThemes } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath
resourcesDirPath
});
{

View File

@ -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
);

View File

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

View File

@ -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": []

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 type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
import { readMetaInfKeycloakThemes_fromJar } from "../shared/metaInfKeycloakThemes";
import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange";
@ -21,6 +21,9 @@ import * as runExclusive from "run-exclusive";
import { extractArchive } from "../tools/extractArchive";
import { appBuild } from "./appBuild";
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 & {
port: number;
@ -98,7 +101,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: false,
onlyBuildJarFileBasename: undefined,
buildContext
});
@ -112,13 +115,31 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
}
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath
});
const { doesImplementAccountTheme } = await (async () => {
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(
({ name }) => name === accountV1ThemeName
);
assert(latestJarFilePath !== undefined);
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 } =
await (async function getKeycloakMajor(): Promise<{
@ -138,7 +159,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 17,
startingFromMajor: 18,
excludeMajorVersions: [22],
cacheDirPath: buildContext.cacheDirPath
});
@ -172,7 +194,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return "23" as const;
}
return "24-and-above" as const;
if (keycloakMajorVersionNumber === 24) {
return "24" as const;
}
return "25-and-above" as const;
})();
assert<
@ -206,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}`
@ -262,65 +292,34 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename);
const { doUseBuiltInAccountV1Theme } = await (async () => {
let doUseBuiltInAccountV1Theme = false;
async function extractThemeResourcesFromJar() {
await extractArchive({
archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => {
for (const themeName of buildContext.themeNames) {
if (
relativeFilePathInArchive ===
pathJoin("theme", themeName, "account", "theme.properties")
) {
if (
(await readFile())
.toString("utf8")
.includes("parent=keycloak")
) {
doUseBuiltInAccountV1Theme = true;
}
earlyExit();
}
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
await writeFile({
filePath: pathJoin(
buildContext.keycloakifyBuildDirPath,
relativeFilePathInArchive
)
});
}
}
});
}
return { doUseBuiltInAccountV1Theme };
})();
{
const destDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "theme");
if (await existsAsync(destDirPath)) {
await rm(destDirPath, { recursive: true });
}
}
const accountThemePropertyPatch = !doUseBuiltInAccountV1Theme
? undefined
: () => {
for (const themeName of buildContext.themeNames) {
const filePath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
);
await extractThemeResourcesFromJar();
const sourceCode = fs.readFileSync(filePath);
const jarFilePath_cacheDir = pathJoin(buildContext.cacheDirPath, jarFileBasename);
const modifiedSourceCode = Buffer.from(
sourceCode
.toString("utf8")
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
);
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
fs.writeFileSync(filePath, modifiedSourceCode);
}
};
accountThemePropertyPatch?.();
fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
try {
child_process.execSync(`docker rm --force ${containerName}`, {
@ -342,20 +341,28 @@ 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"]
: []),
...[
...buildContext.themeNames,
...(doUseBuiltInAccountV1Theme ? [] : [accountV1ThemeName])
...(fs.existsSync(
pathJoin(
buildContext.keycloakifyBuildDirPath,
"theme",
accountV1ThemeName
)
)
? [accountV1ThemeName]
: [])
]
.map(themeName => ({
localDirPath: pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName
),
@ -451,7 +458,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: true,
onlyBuildJarFileBasename: jarFileBasename,
buildContext
});
@ -459,7 +466,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return;
}
accountThemePropertyPatch?.();
await extractThemeResourcesFromJar();
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 { 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"));

View File

@ -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;

View File

@ -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;

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;
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 }) => ({

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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 "";
}

View File

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

View File

@ -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";

View File

@ -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";

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-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"