Merge branch 'main' into feat/additional-account-pages

This commit is contained in:
George Litos
2024-03-02 13:41:48 +02:00
19 changed files with 374 additions and 143 deletions

View File

@ -10,3 +10,5 @@ export const retrocompatPostfix = "_retrocompat";
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const keycloakifyBuildOptionsForPostPostBuildScriptEnvName = "KEYCLOAKIFY_BUILD_OPTIONS_POST_POST_BUILD_SCRIPT";

View File

@ -181,34 +181,6 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
);
}
{
const totpFtlFilePath = pathJoin(destDirPath, "base", "account", "totp.ftl");
fs.writeFileSync(
totpFtlFilePath,
Buffer.from(
fs
.readFileSync(totpFtlFilePath)
.toString("utf8")
.replace(
[
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>"
].join("\n"),
[
" <#if totp.policy.supportedApplications?has_content>",
" <#list totp.policy.supportedApplications as app>",
" <li>${app}</li>",
" </#list>",
" </#if>"
].join("\n")
),
"utf8"
)
);
}
// Note, this is an optimization for reducing the size of the jar,
// For this version we know exactly which resources are used.
{

View File

@ -0,0 +1,25 @@
import { z } from "zod";
export type UserProvidedBuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
export const zUserProvidedBuildOptions = z.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
});

View File

@ -47,10 +47,15 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
throw new Error("Keycloakify's Vite plugin output not found");
}
const parsedPackageJson = readParsedPackageJson({ reactAppRootDirPath });
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
const userProvidedBuildOptions = {
...userProvidedBuildOptionsFromPackageJson,
...resolvedViteConfig?.userProvidedBuildOptions
};
const themeNames = (() => {
if (parsedPackageJson.keycloakify?.themeName === undefined) {
if (userProvidedBuildOptions.themeName === undefined) {
return [
parsedPackageJson.name
.replace(/^@(.*)/, "$1")
@ -59,11 +64,11 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
];
}
if (typeof parsedPackageJson.keycloakify.themeName === "string") {
return [parsedPackageJson.keycloakify.themeName];
if (typeof userProvidedBuildOptions.themeName === "string") {
return [userProvidedBuildOptions.themeName];
}
return parsedPackageJson.keycloakify.themeName;
return userProvidedBuildOptions.themeName;
})();
const reactAppBuildDirPath = (() => {
@ -72,9 +77,9 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
break webpack;
}
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.reactAppBuildDirPath,
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
@ -94,13 +99,13 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames,
"extraThemeProperties": parsedPackageJson.keycloakify?.extraThemeProperties,
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
"groupId": (() => {
const fallbackGroupId = `${themeNames[0]}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
parsedPackageJson.keycloakify?.groupId ??
userProvidedBuildOptions.groupId ??
(parsedPackageJson.homepage === undefined
? fallbackGroupId
: urlParse(parsedPackageJson.homepage)
@ -110,20 +115,23 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? parsedPackageJson.keycloakify?.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"doCreateJar": parsedPackageJson.keycloakify?.doCreateJar ?? true,
"loginThemeResourcesFromKeycloakVersion": parsedPackageJson.keycloakify?.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? userProvidedBuildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`,
"doCreateJar": userProvidedBuildOptions.doCreateJar ?? true,
"loginThemeResourcesFromKeycloakVersion": userProvidedBuildOptions.loginThemeResourcesFromKeycloakVersion ?? "11.0.3",
reactAppRootDirPath,
reactAppBuildDirPath,
"keycloakifyBuildDirPath": (() => {
if (parsedPackageJson.keycloakify?.keycloakifyBuildDirPath !== undefined) {
if (userProvidedBuildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify?.keycloakifyBuildDirPath,
"pathIsh": userProvidedBuildOptions.keycloakifyBuildDirPath,
"cwd": reactAppRootDirPath
});
}
return resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`;
return pathJoin(
reactAppRootDirPath,
resolvedViteConfig?.buildDir === undefined ? "build_keycloak" : `${resolvedViteConfig.buildDir}_keycloak`
);
})(),
"publicDirPath": (() => {
webpack: {
@ -179,7 +187,7 @@ export function readBuildOptions(params: { processArgv: string[] }): BuildOption
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
})(),
"doBuildRetrocompatAccountTheme": parsedPackageJson.keycloakify?.doBuildRetrocompatAccountTheme ?? true,
"doBuildRetrocompatAccountTheme": userProvidedBuildOptions.doBuildRetrocompatAccountTheme ?? true,
npmWorkspaceRootDirPath
};
}

View File

@ -3,41 +3,20 @@ import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { type UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
doCreateJar?: boolean;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
doBuildRetrocompatAccountTheme?: boolean;
};
keycloakify?: UserProvidedBuildOptions;
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"doCreateJar": z.boolean().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional(),
"doBuildRetrocompatAccountTheme": z.boolean().optional()
})
.optional()
"keycloakify": zUserProvidedBuildOptions.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();

View File

@ -5,19 +5,22 @@ import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
import { UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
userProvidedBuildOptions: UserProvidedBuildOptions;
};
const zResolvedViteConfig = z.object({
"buildDir": z.string(),
"publicDir": z.string(),
"assetsDir": z.string(),
"urlPathname": z.string().optional()
"urlPathname": z.string().optional(),
"userProvidedBuildOptions": zUserProvidedBuildOptions
});
{

View File

@ -431,7 +431,7 @@
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
<#return "ABORT: Too many recursive calls, path: " + path?join(".")>
</#if>
<#local keys = "">
@ -463,9 +463,10 @@
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
<#-- https://github.com/keycloakify/keycloakify/issues/357 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl"]?seq_contains(pageId) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
@ -488,24 +489,33 @@
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
) || (
"applications.ftl" == pageId &&
are_same_path(path, ["applications", "applications", "*", "client", "realm"])
is_subpath(path, ["applications", "applications"]) &&
(
key == "realm" ||
key == "container"
)
) || (
"applications.ftl" == pageId &&
"masterAdminClient" == key
are_same_path(path, ["user"]) &&
key == "delegateForUpdate"
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if pageId == "register.ftl" && key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if (
["register.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>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]>
</#attempt>
</#if>
@ -658,9 +668,9 @@
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#function is_subpath path searchedPath>
<#if path?size != searchedPath?size>
<#if path?size < searchedPath?size>
<#return false>
</#if>
@ -668,8 +678,14 @@
<#list path as property>
<#if i == searchedPath?size >
<#continue>
</#if>
<#local searchedProperty=searchedPath[i]>
<#local i+= 1>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
@ -686,11 +702,13 @@
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
<#function are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function>
</script>

View File

@ -9,6 +9,7 @@ import { getLogger } from "../tools/logger";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants";
export async function main() {
const buildOptions = readBuildOptions({
@ -36,9 +37,31 @@ export async function main() {
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
const containerKeycloakVersion = "23.0.6";
const jarFilePath = pathJoin(buildOptions.keycloakifyBuildDirPath, "target", `${buildOptions.artifactId}-${buildOptions.themeVersion}.jar`);
if (buildOptions.doCreateJar) {
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
child_process.execSync("npx vite", {
"cwd": buildOptions.reactAppRootDirPath,
"env": {
...process.env,
[keycloakifyBuildOptionsForPostPostBuildScriptEnvName]: JSON.stringify(buildOptions)
}
});
create_jar: {
if (!buildOptions.doCreateJar) {
break create_jar;
}
child_process.execSync("mvn clean install", { "cwd": buildOptions.keycloakifyBuildDirPath });
const jarDirPath = pathDirname(jarFilePath);
@ -59,16 +82,6 @@ export async function main() {
);
}
const containerKeycloakVersion = "23.0.6";
generateStartKeycloakTestingContainer({
"keycloakVersion": containerKeycloakVersion,
jarFilePath,
buildOptions
});
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
logger.log(
[
"",