Refactor checkpoint

This commit is contained in:
Joseph Garrone
2024-09-08 00:06:47 +02:00
parent 8340608045
commit 93c1c56279
28 changed files with 402 additions and 897 deletions

View File

@ -7,7 +7,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions });
await copyKeycloakResourcesToPublic({
copyKeycloakResourcesToPublic({
buildContext
});
}

View File

@ -11,11 +11,7 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import {
type ThemeType,
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
RESOURCES_COMMON
} from "../../shared/constants";
import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
@ -94,7 +90,7 @@ export function generateFtlFilesCodeFactory(params: {
new RegExp(
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
),
`\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/`
`\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/`
)
);
})
@ -119,7 +115,7 @@ export function generateFtlFilesCodeFactory(params: {
.replace("{{keycloakifyVersion}}", keycloakifyVersion)
.replace("{{themeVersion}}", buildContext.themeVersion)
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
.replace("{{RESOURCES_COMMON}}", RESOURCES_COMMON)
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
.replace(
"{{userDefinedExclusions}}",
buildContext.kcContextExclusionsFtlCode ?? ""

View File

@ -1,89 +0,0 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import {
RESOURCES_COMMON,
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
ACCOUNT_V1_THEME_NAME
} from "../../shared/constants";
import {
downloadKeycloakDefaultTheme,
BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
} from "../../shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "../../tools/transformCodebase";
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme;
assert<BuildContext extends BuildContextLike ? true : false>();
export async function bringInAccountV1(params: {
resourcesDirPath: string;
buildContext: BuildContextLike;
}) {
const { resourcesDirPath, buildContext } = params;
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion: LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
buildContext
});
const accountV1DirPath = pathJoin(
resourcesDirPath,
"theme",
ACCOUNT_V1_THEME_NAME,
"account"
);
transformCodebase({
srcDirPath: pathJoin(defaultThemeDirPath, "base", "account"),
destDirPath: accountV1DirPath
});
transformCodebase({
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "account", "resources"),
destDirPath: pathJoin(accountV1DirPath, "resources")
});
transformCodebase({
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
destDirPath: pathJoin(accountV1DirPath, "resources", RESOURCES_COMMON)
});
fs.writeFileSync(
pathJoin(accountV1DirPath, "theme.properties"),
Buffer.from(
[
"accountResourceProvider=account-v1",
"",
"locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
"",
"styles=" +
[
"css/account.css",
"img/icon-sidebar-active.png",
"img/logo.png",
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(
fileBasename =>
`${RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}`
)
].join(" "),
"",
"##### css classes for form buttons",
"# main class used for all buttons",
"kcButtonClass=btn",
"# classes defining priority of the button - primary or default (there is typically only one priority button for the form)",
"kcButtonPrimaryClass=btn-primary",
"kcButtonDefaultClass=btn-default",
"# classes defining size of the button",
"kcButtonLargeClass=btn-lg",
""
].join("\n"),
"utf8"
)
);
}

View File

@ -1,11 +1,6 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import {
join as pathJoin,
resolve as pathResolve,
relative as pathRelative,
dirname as pathDirname
} from "path";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import {
@ -14,26 +9,15 @@ import {
} from "../generateFtl";
import {
type ThemeType,
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1,
KEYCLOAK_RESOURCES,
ACCOUNT_V1_THEME_NAME,
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR,
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS
ACCOUNT_THEME_PAGE_IDS,
WELL_KNOWN_DIRECTORY_BASE_NAME
} from "../../shared/constants";
import type { BuildContext } from "../../shared/buildContext";
import { assert, type Equals } from "tsafe/assert";
import {
downloadKeycloakStaticResources,
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
} from "../../shared/downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import {
bringInAccountV1,
type BuildContextLike as BuildContextLike_bringInAccountV1
} from "./bringInAccountV1";
import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import {
@ -43,20 +27,18 @@ import {
import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import * as child_process from "child_process";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources &
BuildContextLike_bringInAccountV1 & {
extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string;
projectDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
packageJsonFilePath: string;
};
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & {
extraThemeProperties: string[] | undefined;
projectDirPath: string;
projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
themeSrcDirPath: string;
bundler: "vite" | "webpack";
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
@ -88,7 +70,7 @@ export async function generateResourcesForMainTheme(params: {
const destDirPath = pathJoin(
themeTypeDirPath,
"resources",
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
);
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
@ -106,7 +88,7 @@ export async function generateResourcesForMainTheme(params: {
themeType: "login"
}),
"resources",
BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR
WELL_KNOWN_DIRECTORY_BASE_NAME.DIST
),
destDirPath
});
@ -117,7 +99,7 @@ export async function generateResourcesForMainTheme(params: {
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
KEYCLOAK_RESOURCES
WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY
);
if (fs.existsSync(dirPath)) {
@ -125,7 +107,7 @@ export async function generateResourcesForMainTheme(params: {
throw new Error(
[
`Keycloakify build error: The ${KEYCLOAK_RESOURCES} directory shouldn't exist in your build directory.`,
`Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY} directory shouldn't exist in your build directory.`,
`(${pathRelative(process.cwd(), dirPath)}).\n`,
`Theses assets are only required for local development with Storybook.",
"Please remove this directory as an additional step of your command.\n`,
@ -232,23 +214,50 @@ export async function generateResourcesForMainTheme(params: {
});
}
bring_in_account_v3_i18n_messages: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v3_i18n_messages;
}
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
break bring_in_account_v3_i18n_messages;
}
const accountUiDirPath = child_process
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
cwd: pathDirname(buildContext.packageJsonFilePath)
})
.toString("utf8")
.trim();
const messagesDirPath = pathJoin(accountUiDirPath, "messages");
if (!fs.existsSync(messagesDirPath)) {
throw new Error(
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
);
}
transformCodebase({
srcDirPath: messagesDirPath,
destDirPath: pathJoin(
getThemeTypeDirPath({ themeType: "account" }),
"messages"
)
});
}
keycloak_static_resources: {
if (isForAccountSpa) {
break keycloak_static_resources;
}
await downloadKeycloakStaticResources({
keycloakVersion: (() => {
switch (themeType) {
case "account":
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
case "login":
return buildContext.loginThemeResourcesFromKeycloakVersion;
}
})(),
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
themeType,
buildContext
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES,
themeType
),
destDirPath: pathJoin(themeTypeDirPath, "resources")
});
}
@ -259,7 +268,7 @@ export async function generateResourcesForMainTheme(params: {
`parent=${(() => {
switch (themeType) {
case "account":
return isForAccountSpa ? "base" : ACCOUNT_V1_THEME_NAME;
return isForAccountSpa ? "base" : "account-v1";
case "login":
return "keycloak";
}
@ -299,41 +308,12 @@ export async function generateResourcesForMainTheme(params: {
break bring_in_account_v1;
}
await bringInAccountV1({
resourcesDirPath,
buildContext
});
}
bring_in_account_v3_i18n_messages: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v3_i18n_messages;
}
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
break bring_in_account_v3_i18n_messages;
}
const accountUiDirPath = child_process
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
cwd: pathDirname(buildContext.packageJsonFilePath)
})
.toString("utf8")
.trim();
const messagesDirPath = pathJoin(accountUiDirPath, "messages");
if (!fs.existsSync(messagesDirPath)) {
throw new Error(
`Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.`
);
}
transformCodebase({
srcDirPath: messagesDirPath,
destDirPath: pathJoin(
getThemeTypeDirPath({ themeType: "account" }),
"messages"
)
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1
),
destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account")
});
}
@ -349,7 +329,7 @@ export async function generateResourcesForMainTheme(params: {
if (buildContext.implementedThemeTypes.account.isImplemented) {
metaInfKeycloakThemes.themes.push({
name: ACCOUNT_V1_THEME_NAME,
name: "account-v1",
types: ["account"]
});
}

View File

@ -1,5 +1,5 @@
import type { BuildContext } from "../../shared/buildContext";
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../shared/constants";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
import { assert } from "tsafe/assert";
import { posix } from "path";
@ -50,7 +50,7 @@ export function replaceImportsInCssCode(params: {
break inline_style_in_html;
}
return `url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}${assetFileAbsoluteUrlPathname}")`;
return `url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}${assetFileAbsoluteUrlPathname}")`;
}
const assetFileRelativeUrlPathname = posix.relative(

View File

@ -1,4 +1,4 @@
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path";
@ -85,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: {
fixedJsCode = replaceAll(
fixedJsCode,
`"${relativePathOfAssetFile}"`,
`(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
`(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")`
);
fixedJsCode = replaceAll(
fixedJsCode,
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
`(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")`
`(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")`
);
});
}

View File

@ -1,4 +1,4 @@
import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path";
@ -90,7 +90,7 @@ export function replaceImportsInJsCode_webpack(params: {
return "${u}";
})()] = ${
isArrowFunction ? `${e} =>` : `function(${e}) { return `
} "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}${language}/"`
} "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}${language}/"`
.replace(/\s+/g, " ")
.trim();
}
@ -104,7 +104,7 @@ export function replaceImportsInJsCode_webpack(params: {
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
"g"
),
`window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}`
`window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}`
);
return { fixedJsCode };

View File

@ -14,8 +14,7 @@ import { assert, type Equals } from "tsafe/assert";
import * as child_process from "child_process";
import {
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME
} from "./constants";
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
@ -33,7 +32,6 @@ export type BuildContext = {
extraThemeProperties: string[] | undefined;
groupId: string;
artifactId: string;
loginThemeResourcesFromKeycloakVersion: string;
projectDirPath: string;
projectBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
@ -85,7 +83,6 @@ export type BuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string;
startKeycloakOptions?: {
@ -357,7 +354,6 @@ export function getBuildContext(params: {
extraThemeProperties: z.array(z.string()).optional(),
artifactId: z.string().optional(),
groupId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(),
kcContextExclusionsFtl: z.string().optional(),
startKeycloakOptions: zStartKeycloakOptions.optional()
@ -545,9 +541,6 @@ export function getBuildContext(params: {
process.env.KEYCLOAKIFY_ARTIFACT_ID ??
buildOptions.artifactId ??
`${themeNames[0]}-keycloak-theme`,
loginThemeResourcesFromKeycloakVersion:
buildOptions.loginThemeResourcesFromKeycloakVersion ??
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
projectDirPath,
projectBuildDirPath,
keycloakifyBuildDirPath: (() => {

View File

@ -1,10 +1,12 @@
export const KEYCLOAK_RESOURCES = "keycloak-resources";
export const RESOURCES_COMMON = "resources-common";
export const LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 = "21.1.2";
export const BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR = "dist";
export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
DOT_KEYCLOAKIFY: ".keycloakify",
RESOURCES_COMMON: "resources-common",
DIST: "dist",
RESOURCES: "resources",
ACCOUNT_V1: "account-v1"
};
export const THEME_TYPES = ["login", "account"] as const;
export const ACCOUNT_V1_THEME_NAME = "account-v1";
export type ThemeType = (typeof THEME_TYPES)[number];
@ -71,5 +73,3 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en";
export const LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT = "24.0.4";

View File

@ -1,44 +1,34 @@
import {
downloadKeycloakStaticResources,
type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources
} from "./downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import {
THEME_TYPES,
KEYCLOAK_RESOURCES,
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1
} from "../shared/constants";
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { assert } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "../tools/fs.rmSync";
import type { BuildContext } from "./buildContext";
import { transformCodebase } from "../tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = BuildContextLike_downloadKeycloakStaticResources & {
loginThemeResourcesFromKeycloakVersion: string;
export type BuildContextLike = {
publicDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function copyKeycloakResourcesToPublic(params: {
export function copyKeycloakResourcesToPublic(params: {
buildContext: BuildContextLike;
}) {
const { buildContext } = params;
const destDirPath = pathJoin(buildContext.publicDirPath, KEYCLOAK_RESOURCES);
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
destDirPath,
keycloakifyVersion: readThisNpmPackageVersion(),
buildContext: {
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
fetchOptions: buildContext.fetchOptions
}
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
@ -62,35 +52,33 @@ export async function copyKeycloakResourcesToPublic(params: {
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
for (const themeType of THEME_TYPES) {
await downloadKeycloakStaticResources({
keycloakVersion: (() => {
switch (themeType) {
case "login":
return buildContext.loginThemeResourcesFromKeycloakVersion;
case "account":
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
}
})(),
themeType,
themeDirPath: destDirPath,
buildContext
});
}
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container\n",
"This directory will be automatically excluded from the final build."
].join(" ")
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);

View File

@ -1,338 +0,0 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { type BuildContext } from "../buildContext";
import { assert } from "tsafe/assert";
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants";
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import * as fsPr from "fs/promises";
export type BuildContextLike = {
cacheDirPath: string;
fetchOptions: BuildContext["fetchOptions"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function downloadKeycloakDefaultTheme(params: {
keycloakVersion: string;
buildContext: BuildContextLike;
}): Promise<{ defaultThemeDirPath: string }> {
const { keycloakVersion, buildContext } = params;
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
let areExtraAssetsFor24Copied = false;
const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",
onArchiveFile: async params => {
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
if (fileRelativePath.startsWith("..")) {
return;
}
skip_keycloak_v2: {
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
break skip_keycloak_v2;
}
return;
}
const { readFile, writeFile } = params;
last_account_v1_transformations: {
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
break last_account_v1_transformations;
}
skip_web_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "web_modules")
)
) {
break skip_web_modules;
}
return;
}
skip_lib: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "lib")
)
) {
break skip_lib;
}
return;
}
skip_node_modules: {
const nodeModulesRelativeDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
)
]);
}
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
nodeModulesRelativeDirPath.length + 1
);
if (
kcNodeModulesKeepFilePaths_lastAccountV1.has(
fileRelativeToNodeModulesPath
)
) {
break skip_node_modules;
}
return;
}
patch_account_css: {
if (
fileRelativePath !==
pathJoin("keycloak", "account", "resources", "css", "account.css")
) {
break patch_account_css;
}
await writeFile({
fileRelativePath,
modifiedData: Buffer.from(
(await readFile())
.toString("utf8")
.replace("top: -34px;", "top: -34px !important;"),
"utf8"
)
});
return;
}
}
copy_extra_assets: {
if (keycloakVersion !== "24.0.4") {
break copy_extra_assets;
}
if (areExtraAssetsFor24Copied) {
break copy_extra_assets;
}
const extraAssetsDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"shared",
"downloadKeycloakDefaultTheme",
"extra-assets"
);
await Promise.all(
["webauthnAuthenticate.js", "passkeysConditionalAuth.js"].map(
async fileBasename =>
writeFile({
fileRelativePath: pathJoin(
"base",
"login",
"resources",
"js",
fileBasename
),
modifiedData: await fsPr.readFile(
pathJoin(extraAssetsDirPath, fileBasename)
)
})
)
);
}
skip_unused_resources: {
if (keycloakVersion !== "24.0.4") {
break skip_unused_resources;
}
skip_node_modules: {
const nodeModulesRelativeDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths === undefined) {
kcNodeModulesKeepFilePaths = new Set([
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"fontawesome-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
pathJoin("jquery", "dist", "jquery.min.js")
]);
}
const fileRelativeToNodeModulesPath = fileRelativePath.substring(
nodeModulesRelativeDirPath.length + 1
);
if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) {
break skip_node_modules;
}
return;
}
skip_vendor: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "vendor")
)
) {
break skip_vendor;
}
return;
}
skip_rollup_config: {
if (
fileRelativePath !==
pathJoin("keycloak", "common", "resources", "rollup.config.js")
) {
break skip_rollup_config;
}
return;
}
}
await writeFile({ fileRelativePath });
}
});
return { defaultThemeDirPath: extractedDirPath };
}

View File

@ -1,79 +0,0 @@
import { base64url } from "rfc4648";
import { returnSuccess, returnFailure } from "./webauthnAuthenticate.js";
export function initAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(input.errmsg);
return;
}
if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
document.getElementById("kc-form-passkey-button").style.display = 'block';
} else {
tryAutoFillUI(input);
}
}
function doAuthenticate(input) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(input.errmsg);
return;
}
const publicKey = {
rpId : input.rpId,
challenge: base64url.parse(input.challenge, { loose: true })
};
publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
if (input.createTimeout !== 0) {
publicKey.timeout = input.createTimeout * 1000;
}
if (input.userVerification !== 'not specified') {
publicKey.userVerification = input.userVerification;
}
return navigator.credentials.get({
publicKey: publicKey,
...input.additionalOptions
});
}
async function tryAutoFillUI(input) {
const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalMediationAvailable) {
document.getElementById("kc-form-login").style.display = "block";
input.additionalOptions = { mediation: 'conditional'};
try {
const result = await doAuthenticate(input);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
} else {
document.getElementById("kc-form-passkey-button").style.display = 'block';
}
}
function getAllowCredentials() {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
}
}
return allowCredentials;
}

View File

@ -1,82 +0,0 @@
import { base64url } from "rfc4648";
export async function authenticateByWebAuthn(input) {
if (!input.isUserIdentified) {
try {
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
return;
}
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
}
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
const allowCredentials = [];
const authnUse = document.forms['authn_select'].authn_use_chk;
if (authnUse !== undefined) {
if (authnUse.length === undefined) {
allowCredentials.push({
id: base64url.parse(authnUse.value, {loose: true}),
type: 'public-key',
});
} else {
authnUse.forEach((entry) =>
allowCredentials.push({
id: base64url.parse(entry.value, {loose: true}),
type: 'public-key',
}));
}
}
try {
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
returnSuccess(result);
} catch (error) {
returnFailure(error);
}
}
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
// Check if WebAuthn is supported by this browser
if (!window.PublicKeyCredential) {
returnFailure(errmsg);
return;
}
const publicKey = {
rpId : rpId,
challenge: base64url.parse(challenge, { loose: true })
};
if (createTimeout !== 0) {
publicKey.timeout = createTimeout * 1000;
}
if (allowCredentials.length) {
publicKey.allowCredentials = allowCredentials;
}
if (userVerification !== 'not specified') {
publicKey.userVerification = userVerification;
}
return navigator.credentials.get({publicKey});
}
export function returnSuccess(result) {
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
document.getElementById("credentialId").value = result.id;
if (result.response.userHandle) {
document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
}
document.getElementById("webauth").submit();
}
export function returnFailure(err) {
document.getElementById("error").value = err;
document.getElementById("webauth").submit();
}

View File

@ -1 +0,0 @@
export * from "./downloadKeycloakDefaultTheme";

View File

@ -1,53 +0,0 @@
import { transformCodebase } from "../tools/transformCodebase";
import { join as pathJoin } from "path";
import {
downloadKeycloakDefaultTheme,
type BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
} from "./downloadKeycloakDefaultTheme";
import { RESOURCES_COMMON, type ThemeType } from "./constants";
import type { BuildContext } from "./buildContext";
import { assert } from "tsafe/assert";
import { existsAsync } from "../tools/fs.existsAsync";
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme & {};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function downloadKeycloakStaticResources(params: {
themeType: ThemeType;
themeDirPath: string;
keycloakVersion: string;
buildContext: BuildContextLike;
}) {
const { themeType, themeDirPath, keycloakVersion, buildContext } = params;
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion,
buildContext
});
const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources");
repatriate_base_resources: {
const srcDirPath = pathJoin(defaultThemeDirPath, "base", themeType, "resources");
if (!(await existsAsync(srcDirPath))) {
break repatriate_base_resources;
}
transformCodebase({
srcDirPath,
destDirPath: resourcesDirPath
});
}
transformCodebase({
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", themeType, "resources"),
destDirPath: resourcesDirPath
});
transformCodebase({
srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"),
destDirPath: pathJoin(resourcesDirPath, RESOURCES_COMMON)
});
}