Compare commits

...

11 Commits

12 changed files with 165 additions and 132 deletions

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "11.8.3",
"version": "11.8.8",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",

View File

@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-SemiboldItalic-webfont.ttf"
),
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
pathJoin("jquery", "dist", "jquery.min.js"),
pathJoin("rfc4648", "lib", "rfc4648.js")

View File

@ -103,14 +103,21 @@ export async function command(params: { buildContext: BuildContext }) {
const moduleName = `@keycloakify/email-native`;
const [version] = (
JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
)
const [version] = ((): string[] => {
const cmdOutput = child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim();
const versions = JSON.parse(cmdOutput) as string | string[];
// NOTE: Bug in some older npm versions
if (typeof versions === "string") {
return [versions];
}
return versions;
})()
.reverse()
.filter(version => !version.includes("-"));

View File

@ -6,8 +6,7 @@ import {
join as pathJoin,
relative as pathRelative,
dirname as pathDirname,
extname as pathExtname,
sep as pathSep
basename as pathBasename
} from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
@ -37,6 +36,7 @@ import propertiesParser from "properties-parser";
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
import { listInstalledModules } from "../../tools/listInstalledModules";
import { isInside } from "../../tools/isInside";
import { id } from "tsafe/id";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & {
@ -663,7 +663,7 @@ export async function generateResources(params: {
}
}
for (const themeVariantName of buildContext.themeNames) {
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
for (const themeType of [...THEME_TYPES, "email"] as const) {
copy_main_theme_to_theme_variant_theme: {
let isNative: boolean;
@ -678,44 +678,59 @@ export async function generateResources(params: {
isNative = !v.isImplemented && v.isImplemented_native;
}
if (themeVariantName === themeName) {
if (!isNative && themeVariantName === themeName) {
break copy_main_theme_to_theme_variant_theme;
}
transformCodebase({
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName, themeType),
destDirPath: pathJoin(
resourcesDirPath,
"theme",
themeVariantName,
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
destDirPath: getThemeTypeDirPath({
themeName: themeVariantName,
themeType
),
transformSourceCode: isNative
? undefined
: ({ fileRelativePath, sourceCode }) => {
if (
pathExtname(fileRelativePath) === ".ftl" &&
fileRelativePath.split(pathSep).length === 1
) {
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
`"themeName": "${themeName}"`,
`"themeName": "${themeVariantName}"`
),
"utf8"
);
}),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
patch_xKeycloakify_themeName: {
if (!fileRelativePath.endsWith(".ftl")) {
break patch_xKeycloakify_themeName;
}
assert(
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
);
if (
!isNative &&
pathBasename(fileRelativePath) !== fileRelativePath
) {
break patch_xKeycloakify_themeName;
}
return { modifiedSourceCode };
}
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
...id<[string | RegExp, string]>(
isNative
? [
/xKeycloakify\.themeName/g,
`"${themeVariantName}"`
]
: [
`"themeName": "${themeName}"`,
`"themeName": "${themeVariantName}"`
]
)
),
"utf8"
);
return { modifiedSourceCode: sourceCode };
}
if (!isNative) {
assert(
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
);
}
return { modifiedSourceCode };
}
return { modifiedSourceCode: sourceCode };
}
});
}
run_writeMessagePropertiesFiles: {
@ -734,42 +749,6 @@ export async function generateResources(params: {
themeName: themeVariantName
});
}
replace_xKeycloakify_themeName_in_native_ftl_files: {
{
const v = buildContext.implementedThemeTypes[themeType];
if (v.isImplemented || !v.isImplemented_native) {
break replace_xKeycloakify_themeName_in_native_ftl_files;
}
}
const emailThemeDirPath = getThemeTypeDirPath({
themeName: themeVariantName,
themeType
});
transformCodebase({
srcDirPath: emailThemeDirPath,
destDirPath: emailThemeDirPath,
transformSourceCode: ({ filePath, sourceCode }) => {
if (!filePath.endsWith(".ftl")) {
return { modifiedSourceCode: sourceCode };
}
return {
modifiedSourceCode: Buffer.from(
sourceCode
.toString("utf8")
.replace(
/xKeycloakify\.themeName/g,
`"${themeVariantName}"`
),
"utf8"
)
};
}
});
}
}
}

View File

@ -105,14 +105,21 @@ export async function initializeSpa(params: {
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
const version = (
JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
)
const version = ((): string[] => {
const cmdOutput = child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim();
const versions = JSON.parse(cmdOutput) as string | string[];
// NOTE: Bug in some older npm versions
if (typeof versions === "string") {
return [versions];
}
return versions;
})()
.reverse()
.filter(version => !version.includes("-"))
.find(version =>

View File

@ -101,7 +101,6 @@ function addOrEditTestUser(params: {
);
newUser.username = defaultUser_default.username;
newUser.email = defaultUser_default.email;
delete_existing_password_credential_if_any: {
const i = newUser.credentials.findIndex(

View File

@ -100,9 +100,19 @@ function addCommentToSourceCode(params: {
}
if (fileRelativePath.endsWith(".ftl")) {
return toResult(
[`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join("\n")
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
"\n"
);
if (sourceCode.trim().startsWith("<#ftl")) {
const [first, ...rest] = sourceCode.split(">");
const last = rest.join(">");
return [`${first}>`, comment, last].join("\n");
}
return toResult(comment);
}
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {

View File

@ -1,6 +1,7 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useReducer, Fragment } from "react";
import { useEffect, Fragment } from "react";
import { assert } from "keycloakify/tools/assert";
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import {
useUserProfileForm,
@ -249,15 +250,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);
assert(passwordInputElement instanceof HTMLInputElement);
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
}, [isPasswordRevealed]);
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
<div className={kcClsx("kcInputGroup")}>

View File

@ -1,7 +1,7 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState, useEffect, useReducer } from "react";
import { useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { assert } from "keycloakify/tools/assert";
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
@ -200,15 +200,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);
assert(passwordInputElement instanceof HTMLInputElement);
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
}, [isPasswordRevealed]);
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
<div className={kcClsx("kcInputGroup")}>

View File

@ -1,8 +1,8 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState, useEffect, useReducer } from "react";
import { useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { clsx } from "keycloakify/tools/clsx";
import { assert } from "keycloakify/tools/assert";
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);
assert(passwordInputElement instanceof HTMLInputElement);
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
}, [isPasswordRevealed]);
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
<div className={kcClsx("kcInputGroup")}>

View File

@ -1,7 +1,6 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useReducer } from "react";
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { assert } from "keycloakify/tools/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
const { msgStr } = i18n;
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);
assert(passwordInputElement instanceof HTMLInputElement);
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
}, [isPasswordRevealed]);
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
return (
<div className={kcClsx("kcInputGroup")}>

View File

@ -0,0 +1,45 @@
import { useEffect, useReducer } from "react";
import { assert } from "keycloakify/tools/assert";
/**
* Initially false, state that enables to dynamically control if
* the type of a password input is "password" (false) or "text" (true).
*/
export function useIsPasswordRevealed(params: { passwordInputId: string }) {
const { passwordInputId } = params;
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer(
(isPasswordRevealed: boolean) => !isPasswordRevealed,
false
);
useEffect(() => {
const passwordInputElement = document.getElementById(passwordInputId);
assert(passwordInputElement instanceof HTMLInputElement);
const type = isPasswordRevealed ? "text" : "password";
passwordInputElement.type = type;
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.attributeName !== "type") {
return;
}
if (passwordInputElement.type === type) {
return;
}
passwordInputElement.type = type;
});
});
observer.observe(passwordInputElement, { attributes: true });
return () => {
observer.disconnect();
};
}, [isPasswordRevealed]);
return { isPasswordRevealed, toggleIsPasswordRevealed };
}