Compare commits

..

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

25 changed files with 57 additions and 327 deletions

View File

@ -327,42 +327,6 @@
"contributions": [ "contributions": [
"doc" "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, "contributorsPerLine": 7,

View File

@ -6,7 +6,7 @@
<br> <br>
<br> <br>
<a href="https://github.com/garronej/keycloakify/actions"> <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>
<a href="https://www.npmjs.com/package/keycloakify"> <a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/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/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> <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>
<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> </tbody>
</table> </table>

View File

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

View File

@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params; const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "add-story", commandName: "add-story",
buildContext buildContext
}); });
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
if (themeType === "admin") { if (themeType === "admin") {
console.log( 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); process.exit(0);

View File

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

View File

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

View File

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

View File

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

View File

@ -17,8 +17,8 @@ import chalk from "chalk";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params; const { buildContext } = params;
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme", commandName: "initialize-account-theme",
buildContext buildContext
}); });

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) => await new Promise<void>((resolve, reject) =>
child_process.exec( child_process.exec(
mvnBuildCmd, `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
{ cwd: keycloakifyBuildCacheDirPath }, { cwd: keycloakifyBuildCacheDirPath },
error => { error => {
if (error !== null) { if (error !== null) {
console.error( console.error(
[
`Build jar failed: ${JSON.stringify( `Build jar failed: ${JSON.stringify(
{ {
jarFileBasename, jarFileBasename,
@ -239,10 +235,7 @@ export async function buildJar(params: {
}, },
null, null,
2 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); reject(error);
@ -252,7 +245,6 @@ export async function buildJar(params: {
} }
) )
); );
}
await fs.rename( await fs.rename(
pathJoin( pathJoin(

View File

@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 --> <#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
key == "loginAction" && key == "loginAction" &&
areSamePath(path, ["url"]) && 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()) !(auth?has_content && auth.showTryAnotherWayLink())
) || ( ) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 --> <#-- https://github.com/keycloakify/keycloakify/issues/362 -->

View File

@ -13,15 +13,13 @@ import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>(); assert<Equals<ApiVersion, "v1">>();
export async function maybeDelegateCommandToCustomHandler(params: { export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName; commandName: CommandName;
buildContext: BuildContext; buildContext: BuildContext;
}): Promise<{ hasBeenHandled: boolean }> { }): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params; const { commandName, buildContext } = params;
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({ const nodeModulesBinDirPath = getNodeModulesBinDirPath();
packageJsonFilePath: buildContext.packageJsonFilePath
});
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) { if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false }; 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}.`, `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:`, `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:`, `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}.`, `This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.` `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; let pathOut = pathIsh;
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
pathOut = pathOut.replace(/\//g, pathSep); pathOut = pathOut.replace(/\//g, pathSep);
if (pathOut.startsWith("~")) { if (pathOut.startsWith("~")) {

View File

@ -1,29 +1,10 @@
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path"; import { sep as pathSep } 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";
let cache_bestEffort: string | undefined = undefined; let cache: string | undefined = undefined;
/** NOTE: Careful, this function can fail when the binary export function getNodeModulesBinDirPath() {
* Used is not in the node_modules directory of the project if (cache !== undefined) {
* (for example when running tests with vscode extension we'll get return cache;
* '/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;
} }
const binPath = process.argv[1]; const binPath = process.argv[1];
@ -49,122 +30,9 @@ function getNodeModulesBinDirPath_bestEffort() {
segments.unshift(segment); segments.unshift(segment);
} }
if (!foundNodeModules) {
throw new Error(`Could not find node_modules in path ${binPath}`);
}
const nodeModulesBinDirPath = segments.join(pathSep); const nodeModulesBinDirPath = segments.join(pathSep);
cache_bestEffort = nodeModulesBinDirPath; cache = nodeModulesBinDirPath;
return 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; return getIsPrettierAvailable.cache;
} }
const nodeModulesBinDirPath = getNodeModulesBinDirPath({ const nodeModulesBinDirPath = getNodeModulesBinDirPath();
packageJsonFilePath: undefined
});
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier"); const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
@ -52,25 +50,9 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
// So we do a sketchy eval to bypass ncc. // So we do a sketchy eval to bypass ncc.
// We make sure to only do that when linking, otherwise we import properly. // We make sure to only do that when linking, otherwise we import properly.
if (readThisNpmPackageVersion().startsWith("0.0.0")) { 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( eval(
`import("file:///${pathJoin(prettierDirPath, "index.mjs").replace(/\\/g, "/")}").then(prettier => _resolve(prettier))` `${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
); );
});
}
assert(!is<undefined>(prettier)); assert(!is<undefined>(prettier));
@ -82,7 +64,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
const configHash = await (async () => { const configHash = await (async () => {
const configFilePath = await prettier.resolveConfigFile( const configFilePath = await prettier.resolveConfigFile(
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..") pathJoin(getNodeModulesBinDirPath(), "..")
); );
if (configFilePath === null) { if (configFilePath === null) {

View File

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

View File

@ -769,8 +769,6 @@ export declare namespace Validators {
export type PasswordPolicies = { export type PasswordPolicies = {
/** The minimum length of the password */ /** The minimum length of the password */
length?: number; length?: number;
/** The maximum length of the password */
maxLength?: number;
/** The minimum number of digits required in the password */ /** The minimum number of digits required in the password */
digits?: number; digits?: number;
/** The minimum number of lowercase characters required in the password */ /** 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)} {advancedMsg(attribute.annotations.inputHelperTextAfter)}
</div> </div>
)} )}
{AfterField !== undefined && ( {AfterField !== undefined && (
<AfterField <AfterField
attribute={attribute} attribute={attribute}
@ -106,10 +107,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
</Fragment> </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) { switch (error.source.name) {
case "length": case "length":
return hasLostFocusAtLeastOnce; return hasLostFocusAtLeastOnce;
case "maxLength":
return hasLostFocusAtLeastOnce;
case "digits": case "digits":
return hasLostFocusAtLeastOnce; return hasLostFocusAtLeastOnce;
case "lowerCase": 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: { check_password_policy_x: {
const policyName = "digits"; const policyName = "digits";

View File

@ -31,10 +31,10 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: kcSanitize( __html: kcSanitize(
(() => { (() => {
let html = message.summary?.trim(); let html = message.summary;
if (requiredActions) { if (requiredActions) {
html += " <b>"; html += "<b>";
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", "); 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 { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { kcSanitize } from "keycloakify/lib/kcSanitize"; import { kcSanitize } from "keycloakify/lib/kcSanitize";
import type { PageProps } from "keycloakify/login/pages/PageProps"; 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 { msg, msgStr } = i18n;
const [isSubmitting, setIsSubmitting] = useState(false);
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
@ -28,16 +26,7 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
displayMessage={!messagesPerField.existsError("totp")} displayMessage={!messagesPerField.existsError("totp")}
headerNode={msg("doLogIn")} headerNode={msg("doLogIn")}
> >
<form <form id="kc-otp-login-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
id="kc-otp-login-form"
className={kcClsx("kcFormClass")}
action={url.loginAction}
onSubmit={() => {
setIsSubmitting(true);
return true;
}}
method="post"
>
{otpLogin.userOtpCredentials.length > 1 && ( {otpLogin.userOtpCredentials.length > 1 && (
<div className={kcClsx("kcFormGroupClass")}> <div className={kcClsx("kcFormGroupClass")}>
<div className={kcClsx("kcInputWrapperClass")}> <div className={kcClsx("kcInputWrapperClass")}>
@ -105,7 +94,6 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
id="kc-login" id="kc-login"
type="submit" type="submit"
value={msgStr("doLogIn")} value={msgStr("doLogIn")}
disabled={isSubmitting}
/> />
</div> </div>
</div> </div>

View File

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

View File

@ -46,7 +46,7 @@ export const WithRequiredActions: Story = {
kcContext={{ kcContext={{
messageHeader: "Message header", messageHeader: "Message header",
message: { message: {
summary: "Required actions:" summary: "Required actions: "
}, },
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"], requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
"x-keycloakify": { "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
}}
/>
)
};