Compare commits

..

No commits in common. "main" and "v11.8.11" have entirely different histories.

25 changed files with 59 additions and 332 deletions

View File

@ -327,42 +327,6 @@
"contributions": [
"doc"
]
},
{
"login": "EternalSide",
"name": "Lesha",
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
"profile": "http://t.me/AAT_L",
"contributions": [
"code"
]
},
{
"login": "bacongobbler",
"name": "Matthew Fisher",
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
"profile": "https://blog.bacongobbler.com",
"contributions": [
"doc"
]
},
{
"login": "kodebach",
"name": "Klemens Böswirth",
"avatar_url": "https://avatars.githubusercontent.com/u/23529132?v=4",
"profile": "https://github.com/kodebach",
"contributions": [
"code"
]
},
{
"login": "wnmzzzz",
"name": "wnmzzzz",
"avatar_url": "https://avatars.githubusercontent.com/u/117174301?v=4",
"profile": "https://github.com/wnmzzzz",
"contributions": [
"test"
]
}
],
"contributorsPerLine": 7,

View File

@ -6,7 +6,7 @@
<br>
<br>
<a href="https://github.com/garronej/keycloakify/actions">
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
</a>
<a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/keycloakify">
@ -168,12 +168,6 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kodebach"><img src="https://avatars.githubusercontent.com/u/23529132?v=4?s=100" width="100px;" alt="Klemens Böswirth"/><br /><sub><b>Klemens Böswirth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kodebach" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wnmzzzz"><img src="https://avatars.githubusercontent.com/u/117174301?v=4?s=100" width="100px;" alt="wnmzzzz"/><br /><sub><b>wnmzzzz</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=wnmzzzz" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>

View File

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

View File

@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "add-story",
buildContext
});
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
if (themeType === "admin") {
console.log(
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
);
process.exit(0);

View File

@ -11,7 +11,7 @@ import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});

View File

@ -22,7 +22,7 @@ import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "eject-page",
buildContext
});

View File

@ -12,7 +12,7 @@ import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});

View File

@ -7,7 +7,7 @@ import { command as updateKcGenCommand } from "./update-kc-gen";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-admin-theme",
buildContext
});

View File

@ -17,8 +17,8 @@ import chalk from "chalk";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
@ -49,15 +49,12 @@ export async function command(params: { buildContext: BuildContext }) {
}
const { value: emailThemeType } = await cliSelect({
values: [
"native (FreeMarker)" as const,
"Another email templating solution" as const
]
values: ["native (FreeMarker)" as const, "jsx-email (React)" as const]
}).catch(() => {
process.exit(-1);
});
if (emailThemeType === "Another email templating solution") {
if (emailThemeType === "jsx-email (React)") {
console.log(
[
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",

View File

@ -220,17 +220,13 @@ export async function buildJar(params: {
);
}
{
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
await new Promise<void>((resolve, reject) =>
child_process.exec(
mvnBuildCmd,
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
{ cwd: keycloakifyBuildCacheDirPath },
error => {
if (error !== null) {
console.error(
[
`Build jar failed: ${JSON.stringify(
{
jarFileBasename,
@ -239,10 +235,7 @@ export async function buildJar(params: {
},
null,
2
)}`,
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
].join("\n")
)}`
);
reject(error);
@ -252,7 +245,6 @@ export async function buildJar(params: {
}
)
);
}
await fs.rename(
pathJoin(

View File

@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" &&
areSamePath(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.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(xKeycloakify.pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->

View File

@ -13,15 +13,13 @@ import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
export async function maybeDelegateCommandToCustomHandler(params: {
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}): Promise<{ hasBeenHandled: boolean }> {
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
packageJsonFilePath: buildContext.packageJsonFilePath
});
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false };

View File

@ -45,12 +45,12 @@ export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
`To relinquish ownership and restore this file to its original content, run the following command:`,
``,
`$ npx keycloakify own --path "${path}" --revert`
`$ npx keycloakify own --path '${path}' --revert`
]
: [
`WARNING: Before modifying this file, run the following command:`,
``,
`$ npx keycloakify own --path "${path}"`,
`$ npx keycloakify own --path '${path}'`,
``,
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`

View File

@ -14,8 +14,6 @@ export function getAbsoluteAndInOsFormatPath(params: {
let pathOut = pathIsh;
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
pathOut = pathOut.replace(/\//g, pathSep);
if (pathOut.startsWith("~")) {

View File

@ -1,29 +1,10 @@
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
import { getInstalledModuleDirPath } from "./getInstalledModuleDirPath";
import { existsAsync } from "./fs.existsAsync";
import { z } from "zod";
import * as fs from "fs/promises";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { sep as pathSep } from "path";
let cache_bestEffort: string | undefined = undefined;
let cache: string | undefined = undefined;
/** NOTE: Careful, this function can fail when the binary
* Used is not in the node_modules directory of the project
* (for example when running tests with vscode extension we'll get
* '/Users/dylan/.vscode/extensions/vitest.explorer-1.16.0/dist/worker.js'
*
* instead of
* '/Users/joseph/.nvm/versions/node/v22.12.0/bin/node'
* or
* '/Users/joseph/github/keycloakify-starter/node_modules/.bin/vite'
*
* as the value of process.argv[1]
*/
function getNodeModulesBinDirPath_bestEffort() {
if (cache_bestEffort !== undefined) {
return cache_bestEffort;
export function getNodeModulesBinDirPath() {
if (cache !== undefined) {
return cache;
}
const binPath = process.argv[1];
@ -49,122 +30,9 @@ function getNodeModulesBinDirPath_bestEffort() {
segments.unshift(segment);
}
if (!foundNodeModules) {
throw new Error(`Could not find node_modules in path ${binPath}`);
}
const nodeModulesBinDirPath = segments.join(pathSep);
cache_bestEffort = nodeModulesBinDirPath;
cache = nodeModulesBinDirPath;
return nodeModulesBinDirPath;
}
let cache_withPackageJsonFileDirPath:
| { packageJsonFilePath: string; nodeModulesBinDirPath: string }
| undefined = undefined;
async function getNodeModulesBinDirPath_withPackageJsonFileDirPath(params: {
packageJsonFilePath: string;
}): Promise<string> {
const { packageJsonFilePath } = params;
use_cache: {
if (cache_withPackageJsonFileDirPath === undefined) {
break use_cache;
}
if (
cache_withPackageJsonFileDirPath.packageJsonFilePath !== packageJsonFilePath
) {
cache_withPackageJsonFileDirPath = undefined;
break use_cache;
}
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
}
// [...]node_modules/keycloakify
const installedModuleDirPath = await getInstalledModuleDirPath({
// Here it will always be "keycloakify" but since we are in tools/ we make something generic
moduleName: await (async () => {
type ParsedPackageJson = {
name: string;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
name: z.string()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
(
await fs.readFile(
pathJoin(getThisCodebaseRootDirPath(), "package.json")
)
).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson.name;
})(),
packageJsonDirPath: pathDirname(packageJsonFilePath)
});
const segments = installedModuleDirPath.split(pathSep);
while (true) {
const segment = segments.pop();
if (segment === undefined) {
throw new Error(
`Could not find .bin directory relative to ${packageJsonFilePath}`
);
}
if (segment !== "node_modules") {
continue;
}
const candidate = pathJoin(segments.join(pathSep), segment, ".bin");
if (!(await existsAsync(candidate))) {
continue;
}
cache_withPackageJsonFileDirPath = {
packageJsonFilePath,
nodeModulesBinDirPath: candidate
};
break;
}
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
}
export function getNodeModulesBinDirPath(params: {
packageJsonFilePath: string;
}): Promise<string>;
export function getNodeModulesBinDirPath(params: {
packageJsonFilePath: undefined;
}): string;
export function getNodeModulesBinDirPath(params: {
packageJsonFilePath: string | undefined;
}): string | Promise<string> {
const { packageJsonFilePath } = params ?? {};
return packageJsonFilePath === undefined
? getNodeModulesBinDirPath_bestEffort()
: getNodeModulesBinDirPath_withPackageJsonFileDirPath({ packageJsonFilePath });
}

View File

@ -15,9 +15,7 @@ export async function getIsPrettierAvailable(): Promise<boolean> {
return getIsPrettierAvailable.cache;
}
const nodeModulesBinDirPath = getNodeModulesBinDirPath({
packageJsonFilePath: undefined
});
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
@ -52,25 +50,9 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
// So we do a sketchy eval to bypass ncc.
// We make sure to only do that when linking, otherwise we import properly.
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
const prettierDirPath = pathResolve(
pathJoin(
getNodeModulesBinDirPath({ packageJsonFilePath: undefined }),
"..",
"prettier"
)
);
const isCJS = typeof module !== "undefined" && module.exports;
if (isCJS) {
eval(`${symToStr({ prettier })} = require("${prettierDirPath}")`);
} else {
prettier = await new Promise(_resolve => {
eval(
`import("file:///${pathJoin(prettierDirPath, "index.mjs").replace(/\\/g, "/")}").then(prettier => _resolve(prettier))`
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
);
});
}
assert(!is<undefined>(prettier));
@ -82,7 +64,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
const configHash = await (async () => {
const configFilePath = await prettier.resolveConfigFile(
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..")
pathJoin(getNodeModulesBinDirPath(), "..")
);
if (configFilePath === null) {

View File

@ -19,7 +19,7 @@ export async function command(params: { buildContext: BuildContext }) {
await command({ buildContext });
}
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
});

View File

@ -769,8 +769,6 @@ export declare namespace Validators {
export type PasswordPolicies = {
/** The minimum length of the password */
length?: number;
/** The maximum length of the password */
maxLength?: number;
/** The minimum number of digits required in the password */
digits?: number;
/** The minimum number of lowercase characters required in the password */

View File

@ -90,6 +90,7 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
</div>
)}
{AfterField !== undefined && (
<AfterField
attribute={attribute}
@ -106,10 +107,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
</Fragment>
);
})}
{/* See: https://github.com/keycloak/keycloak/issues/38029 */}
{kcContext.locale !== undefined && formFieldStates.find(x => x.attribute.name === "locale") === undefined && (
<input type="hidden" name="locale" value={i18n.currentLanguage.languageTag} />
)}
</>
);
}

View File

@ -509,8 +509,6 @@ function formStateSelector(params: { state: internal.State }): FormState {
switch (error.source.name) {
case "length":
return hasLostFocusAtLeastOnce;
case "maxLength":
return hasLostFocusAtLeastOnce;
case "digits":
return hasLostFocusAtLeastOnce;
case "lowerCase":
@ -969,34 +967,6 @@ function createGetErrors(params: { kcContext: KcContextLike_useGetErrors }) {
});
}
check_password_policy_x: {
const policyName = "maxLength";
const policy = passwordPolicies[policyName];
if (!policy) {
break check_password_policy_x;
}
const maxLength = policy;
if (value.length <= maxLength) {
break check_password_policy_x;
}
errors.push({
advancedMsgArgs: [
"invalidPasswordMaxLengthMessage" satisfies MessageKey_defaultSet,
`${maxLength}`
] as const,
fieldIndex: undefined,
source: {
type: "passwordPolicy",
name: policyName
}
});
}
check_password_policy_x: {
const policyName = "digits";

View File

@ -31,10 +31,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
dangerouslySetInnerHTML={{
__html: kcSanitize(
(() => {
let html = message.summary?.trim();
let html = message.summary;
if (requiredActions) {
html += " <b>";
html += "<b>";
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", ");

View File

@ -1,4 +1,4 @@
import { Fragment, useState } from "react";
import { Fragment } from "react";
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import type { PageProps } from "keycloakify/login/pages/PageProps";
@ -17,8 +17,6 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { msg, msgStr } = i18n;
const [isSubmitting, setIsSubmitting] = useState(false);
return (
<Template
kcContext={kcContext}
@ -28,16 +26,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
displayMessage={!messagesPerField.existsError("totp")}
headerNode={msg("doLogIn")}
>
<form
id="kc-otp-login-form"
className={kcClsx("kcFormClass")}
action={url.loginAction}
onSubmit={() => {
setIsSubmitting(true);
return true;
}}
method="post"
>
<form id="kc-otp-login-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && (
<div className={kcClsx("kcFormGroupClass")}>
<div className={kcClsx("kcInputWrapperClass")}>
@ -105,7 +94,6 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isSubmitting}
/>
</div>
</div>

View File

@ -98,7 +98,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
defaultValue={login.username ?? ""}
type="text"
autoFocus
autoComplete="username"
autoComplete="off"
aria-invalid={messagesPerField.existsError("username")}
/>
{messagesPerField.existsError("username") && (

View File

@ -46,7 +46,7 @@ export const WithRequiredActions: Story = {
kcContext={{
messageHeader: "Message header",
message: {
summary: "Required actions:"
summary: "Required actions: "
},
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
"x-keycloakify": {

View File

@ -62,22 +62,3 @@ export const WithPasswordConfirmError: Story = {
/>
)
};
/**
* WithAppInitiatedAction:
* - Purpose: Tests when the update password action was triggered by an app.
* - Scenario: Simulates the case where the user presses a 'change password' button in an app and is redirected to Keycloak to change it.
* - Key Aspect: Ensures the 'Cancel' button is shown correctly, which displays only when the action is app initiated.
*/
export const WithAppInitiatedAction: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isAppInitiatedAction: true
}}
/>
)
};