Compare commits

..

31 Commits

Author SHA1 Message Date
e84e7fab86 Release candidate 2024-10-17 15:58:15 +02:00
a2c2e37378 #694 2024-10-17 15:57:52 +02:00
7c7e5544e4 Fix light mode rendering 2024-10-16 05:13:52 +02:00
06fe26fbe7 Add phaseTwo as sponsor 2024-10-16 04:10:17 +02:00
c932c7d8f6 Resize zone2 logo 2024-10-16 03:37:00 +02:00
d56c536446 Bump version 2024-10-13 00:55:23 +02:00
f5a9a28124 Resolve package.json path relative to the package.json 2024-10-13 00:55:06 +02:00
b86039536e Bump version 2024-10-12 17:33:44 +02:00
59c4675e8a Add dir=rtl attribut to html when using a RTL language 2024-10-12 17:30:30 +02:00
fbf6a329df typesafety fix 2024-10-11 23:55:07 +02:00
ddec3118a4 Merge pull request #688 from keycloakify/fix/missing-type-register-story
fix: added parameter type for story context on register page
2024-10-08 23:58:06 +02:00
94e2786297 fix: added parameter type for story context on register page 2024-10-08 16:50:11 -05:00
ecfdff5454 Bump version 2024-10-07 23:53:05 +02:00
c598a58ec9 Merge pull request #685 from keycloakify/all-contributors/add-nima70
docs: add nima70 as a contributor for code, and test
2024-10-07 23:47:25 +02:00
3e38beb190 docs: update .all-contributorsrc [skip ci] 2024-10-07 21:47:11 +00:00
61a86e8e82 docs: update README.md [skip ci] 2024-10-07 21:47:10 +00:00
9c8e127fa0 Merge pull request #672 from nima70/main
Changes:
2024-10-07 23:46:13 +02:00
eb23b40e5c Bump version 2024-10-07 21:03:04 +02:00
9e19aafcd0 Fix initialize-email-theme 2024-10-07 21:02:51 +02:00
4cdf26b6f9 Announcement about Keycloak 26 2024-10-07 20:56:03 +02:00
88de58cc22 Bump version 2024-10-06 22:56:42 +02:00
f6b48c88b9 Fix vite quitting if custom handler implemented 2024-10-06 22:55:18 +02:00
12534e57ad Bump version 2024-10-06 22:09:21 +02:00
52e33bba2d Fix: check for delegation of the eject-page command 2024-10-06 22:08:43 +02:00
d63e5f4e54 Fix litle inconsistency 2024-10-06 15:37:32 +02:00
8d31866a0b Bump version 2024-10-06 15:09:53 +02:00
7d818f217a Merge pull request #683 from keycloakify/feat_custom_handler
Feat custom handler
2024-10-06 15:09:26 +02:00
7156665684 Release candidate 2024-10-06 13:19:12 +02:00
5045c5e8bf Add initialize-email-theme, initialize-account-theme and copy-keycloak-resources-to-public to commands that can be delegated to a custom handler 2024-10-06 13:18:30 +02:00
9de2ed9eaf Fix dead code 2024-10-06 12:44:46 +02:00
c9d7fc1b6e Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-09-29 04:35:02 -04:00
31 changed files with 759 additions and 125 deletions

View File

@ -290,6 +290,16 @@
"code",
"test"
]
},
{
"login": "nima70",
"name": "Nima Shokouhfar",
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
"profile": "https://github.com/nima70",
"contributions": [
"code",
"test"
]
}
],
"contributorsPerLine": 7,

View File

@ -43,21 +43,48 @@
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
> 📣 **Keycloakify 26 Released**
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
## Sponsors
Friends for the project, we trust and recommend their services.
Project backers, we trust and recommend their services.
<br/>
<div align="center">
![Logo Dark](https://github.com/user-attachments/assets/088f6631-b7ef-42ad-812b-df4870dc16ae#gh-dark-mode-only)
![Logo Dark](https://github.com/user-attachments/assets/d8f6b6f5-3de4-4adc-ba15-cb4074e8309b#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/53fb16f8-02ef-4523-9c36-b42d6e59837e#gh-light-mode-only)
![Logo Light](https://github.com/user-attachments/assets/20736d6f-f22d-4a9d-9dfe-93be209a8191#gh-light-mode-only)
</div>
<br/>
<p align="center">
<i><a href="https://phasetwo.io/"><strong>Keycloak on Steroids as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/">Keycloak support</a> to businesses of all sizes.</i>
</p>
<br/>
<br/>
<br/>
<div align="center">
![Logo Dark](https://github.com/user-attachments/assets/dd3925fb-a58a-4e91-b360-69c2fa1f1087#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://github.com/user-attachments/assets/6c00c201-eed7-485a-a887-70891559d69b#gh-light-mode-only)
</div>
@ -82,7 +109,7 @@ Friends for the project, we trust and recommend their services.
</div>
<p align="center">
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. </a>
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company. </a>
<br/>
Use code <code>keycloakify5</code> at checkout for a 5% discount.
</p>
@ -136,6 +163,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://www.linkedin.com/in/oes-rioniz/"><img src="https://avatars.githubusercontent.com/u/5172296?v=4?s=100" width="100px;" alt="Omid"/><br /><sub><b>Omid</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=uchar" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kathari00"><img src="https://avatars.githubusercontent.com/u/42547712?v=4?s=100" width="100px;" alt="Katharina Eiserfey"/><br /><sub><b>Katharina Eiserfey</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Tests">⚠️</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=kathari00" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/luca-peruzzo"><img src="https://avatars.githubusercontent.com/u/69015314?v=4?s=100" width="100px;" alt="Luca Peruzzo"/><br /><sub><b>Luca Peruzzo</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=luca-peruzzo" title="Tests">⚠️</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nima70"><img src="https://avatars.githubusercontent.com/u/5094767?v=4?s=100" width="100px;" alt="Nima Shokouhfar"/><br /><sub><b>Nima Shokouhfar</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Code">💻</a> <a href="https://github.com/keycloakify/keycloakify/commits?author=nima70" title="Tests">⚠️</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "11.3.0-rc.6",
"version": "11.3.7-rc.0",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",

View File

@ -1,10 +1,96 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import type { BuildContext } from "./shared/buildContext";
import { transformCodebase } from "./tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
copyKeycloakResourcesToPublic({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
if (hasBeenHandled) {
return;
}
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
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
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"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")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -22,10 +22,20 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "eject-page",
buildContext
});
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
@ -239,12 +249,12 @@ export async function command(params: { buildContext: BuildContext }) {
)} copy pasted from the Keycloakify source code into your project`
);
edit_KcApp: {
edit_KcPage: {
if (
pageIdOrComponent !== templateValue &&
pageIdOrComponent !== userProfileFormFieldsValue
) {
break edit_KcApp;
break edit_KcPage;
}
const kcAppTsxPath = pathJoin(

View File

@ -6,10 +6,20 @@ import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (

View File

@ -5,15 +5,18 @@ import * as fs from "fs";
import chalk from "chalk";
import { z } from "zod";
import { id } from "tsafe/id";
import { is } from "tsafe/is";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
projectDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function updateAccountThemeImplementationInConfig(params: {
buildContext: BuildContext;
buildContext: BuildContextLike;
accountThemeType: "Single-Page" | "Multi-Page";
}) {
const { buildContext, accountThemeType } = params;
@ -81,6 +84,8 @@ export function updateAccountThemeImplementationInConfig(params: {
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();

View File

@ -4,10 +4,20 @@ import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if (

View File

@ -171,7 +171,7 @@ program
program
.command({
name: "initialize-login-theme",
name: "initialize-email-theme",
description: "Initialize an email theme."
})
.task({

View File

@ -508,6 +508,15 @@ export function getBuildContext(params: {
return themeNames;
})();
const relativePathsCwd = (() => {
switch (bundler) {
case "vite":
return projectDirPath;
case "webpack":
return pathDirname(packageJsonFilePath);
}
})();
const projectBuildDirPath = (() => {
webpack: {
if (bundler !== "webpack") {
@ -519,7 +528,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -563,7 +572,7 @@ export function getBuildContext(params: {
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.keycloakifyBuildDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -592,7 +601,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
cwd: projectDirPath
cwd: relativePathsCwd
});
}
@ -664,7 +673,7 @@ export function getBuildContext(params: {
pathIsh:
parsedPackageJson.keycloakify
.staticDirPathInProjectBuildDirPath,
cwd: projectBuildDirPath
cwd: relativePathsCwd
});
}
@ -992,7 +1001,7 @@ export function getBuildContext(params: {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
cwd: projectDirPath
cwd: relativePathsCwd
})
};
}
@ -1002,7 +1011,7 @@ export function getBuildContext(params: {
? undefined
: getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
cwd: projectDirPath
cwd: relativePathsCwd
}),
port: buildOptions.startKeycloakOptions?.port
}

View File

@ -1,95 +0,0 @@
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 = {
publicDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function copyKeycloakResourcesToPublic(params: {
buildContext: BuildContextLike;
}) {
const { buildContext } = params;
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
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
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"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")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

View File

@ -6,7 +6,13 @@ export const BIN_NAME = "_keycloakify-custom-handler";
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
export type CommandName = "update-kc-gen" | "eject-page" | "add-story";
export type CommandName =
| "update-kc-gen"
| "eject-page"
| "add-story"
| "initialize-account-theme"
| "initialize-email-theme"
| "copy-keycloak-resources-to-public";
export type ApiVersion = "v1";

View File

@ -16,11 +16,11 @@ assert<Equals<ApiVersion, "v1">>();
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}) {
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
return;
return { hasBeenHandled: false };
}
try {
@ -36,11 +36,11 @@ export function maybeDelegateCommandToCustomHandler(params: {
const status = error.status;
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
return;
return { hasBeenHandled: false };
}
process.exit(status);
}
process.exit(0);
return { hasBeenHandled: true };
}

View File

@ -396,12 +396,12 @@ export async function command(params: {
...(realmJsonFilePath === undefined
? []
: [
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
]),
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map(
jarFilePath =>
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
),
...(keycloakMajorVersionNumber <= 20
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
@ -424,7 +424,7 @@ export async function command(params: {
}))
.map(
({ localDirPath, containerDirPath }) =>
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
),
...buildContext.environmentVariables
.map(({ name }) => ({ name, envValue: process.env[name] }))
@ -451,6 +451,10 @@ export async function command(params: {
])
];
console.log("DEBUG issue #694");
console.log(JSON.stringify(dockerRunArgs, null, 2));
console.log(
chalk.blue(
[

View File

@ -7,11 +7,15 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
maybeDelegateCommandToCustomHandler({
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
});
if (hasBeenHandled) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))

View File

@ -94,6 +94,7 @@ export declare namespace KcContext {
languageTag: string;
}[];
currentLanguageTag: string;
rtl?: boolean;
};
auth?: {
showUsername?: boolean;

View File

@ -19,6 +19,7 @@ export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
rtl?: boolean;
};
"x-keycloakify": {
messages: Record<string, string>;
@ -95,6 +96,49 @@ export function createGetI18n<
const html = document.querySelector("html");
assert(html !== null);
html.lang = currentLanguageTag;
const isRtl = (() => {
const { rtl } = kcContext.locale ?? {};
if (rtl !== undefined) {
return rtl;
}
return [
/* spell-checker: disable */
// Common RTL languages
"ar", // Arabic
"fa", // Persian (Farsi)
"he", // Hebrew
"ur", // Urdu
"ps", // Pashto
"syr", // Syriac
"dv", // Divehi (Maldivian)
"ku", // Kurdish (Sorani)
"ug", // Uighur
"az", // Azerbaijani (Arabic script)
"sd", // Sindhi
// Less common RTL languages
"yi", // Yiddish
"ha", // Hausa (when written in Arabic script)
"ks", // Kashmiri (written in the Perso-Arabic script)
"bal", // Balochi (when written in Arabic script)
"khw", // Khowar (Chitrali)
"brh", // Brahui (when written in Arabic script)
"tmh", // Tamashek (some dialects use Arabic script)
"bgn", // Western Balochi
"arc", // Aramaic
"sam", // Samaritan Aramaic
"prd", // Parsi-Dari (a dialect of Persian)
"huz", // Hazaragi (a dialect of Persian)
"gbz", // Zaza (written in Arabic script in some areas)
"urj" // Urdu in Romanized script (not always RTL, but to account for edge cases)
/* spell-checker: enable */
].includes(currentLanguageTag);
})();
html.dir = isRtl ? "rtl" : "ltr";
}
const getLanguageLabel = (languageTag: LanguageTag) => {

View File

@ -2,6 +2,7 @@ import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -30,7 +31,14 @@ export default function Code(props: PageProps<Extract<KcContext, { pageId: "code
<input id="code" className={kcClsx("kcTextareaClass")} defaultValue={code.code} />
</>
) : (
<p id="error">{code.error}</p>
code.error && (
<p
id="error"
dangerouslySetInnerHTML={{
__html: kcSanitize(code.error)
}}
/>
)
)}
</div>
</Template>

View File

@ -6,7 +6,7 @@ import {
} from "../bin/shared/constants";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
import { copyKeycloakResourcesToPublic } from "../bin/shared/copyKeycloakResourcesToPublic";
import { command as copyKeycloakResourcesToPublicCommand } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
import {
getBuildContext,
@ -125,7 +125,7 @@ export function keycloakify(params: keycloakify.Params) {
projectDirPath
});
copyKeycloakResourcesToPublic({ buildContext });
await copyKeycloakResourcesToPublicCommand({ buildContext });
await updateKcGenCommand({ buildContext });
},

View File

@ -16,3 +16,42 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithErrorCode: Story = {
render: () => (
<KcPageStory
kcContext={{
code: {
success: false,
error: "Failed to generate code"
}
}}
/>
)
};
export const WithFrenchLanguage: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: {
currentLanguageTag: "fr"
},
code: {
success: true,
code: "XYZ789"
}
}}
/>
)
};
export const WithHtmlErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
code: {
success: false,
error: "Something went wrong. <a href='https://example.com'>Try again</a>"
}
}}
/>
)
};

View File

@ -16,3 +16,33 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithAIAFlow: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: true,
url: { loginAction: "/login-action" }
}}
/>
)
};
export const WithoutAIAFlow: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: false,
url: { loginAction: "/login-action" }
}}
/>
)
};
export const WithCustomButtonStyle: Story = {
render: () => (
<KcPageStory
kcContext={{
triggered_from_aia: true,
url: { loginAction: "/login-action" }
}}
/>
)
};

View File

@ -16,3 +16,13 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithCustomCredentialLabel: Story = {
render: () => (
<KcPageStory
kcContext={{
credentialLabel: "Test Credential",
url: { loginAction: "/login-action" }
}}
/>
)
};

View File

@ -26,3 +26,38 @@ export const WithAnotherMessage: Story = {
/>
)
};
export const WithHtmlErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: {
summary: "<strong>Error:</strong> Something went wrong. <a href='https://example.com'>Go back</a>"
}
}}
/>
)
};
export const FrenchError: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: { currentLanguageTag: "fr" },
message: { summary: "Une erreur s'est produite" }
}}
/>
)
};
export const WithSkipLink: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "An error occurred" },
skipLink: true,
client: {
baseUrl: "https://example.com"
}
}}
/>
)
};

View File

@ -16,3 +16,14 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithoutRedirectUrl: Story = {
render: () => (
<KcPageStory
kcContext={{
logout: {
clients: []
}
}}
/>
)
};

View File

@ -16,3 +16,47 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithFormValidationErrors: Story = {
render: () => (
<KcPageStory
kcContext={{
messagesPerField: {
existsError: (fieldName: string) => ["email", "firstName"].includes(fieldName),
get: (fieldName: string) => {
if (fieldName === "email") return "Invalid email format.";
if (fieldName === "firstName") return "First name is required.";
}
}
}}
/>
)
};
export const WithReadOnlyFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
email: { value: "jane.doe@example.com", readOnly: true },
firstName: { value: "Jane", readOnly: false }
}
}
}}
/>
)
};
export const WithPrefilledFormFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
firstName: { value: "Jane" },
lastName: { value: "Doe" },
email: { value: "jane.doe@example.com" }
}
}
}}
/>
)
};

View File

@ -55,3 +55,42 @@ export const WithRequiredActions: Story = {
/>
)
};
export const WithPageRedirect: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "You will be redirected shortly." },
pageRedirectUri: "https://example.com"
}}
/>
)
};
export const WithoutClientBaseUrl: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "No client base URL defined." },
client: { baseUrl: undefined }
}}
/>
)
};
export const WithMessageHeader: Story = {
render: () => (
<KcPageStory
kcContext={{
messageHeader: "Important Notice",
message: { summary: "This is an important message." }
}}
/>
)
};
export const WithAdvancedMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { summary: "Please take note of this <strong>important</strong> information." }
}}
/>
)
};

View File

@ -231,3 +231,131 @@ export const WithErrorMessage: Story = {
/>
)
};
export const WithOneSocialProvider: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
}
]
}
}}
/>
)
};
export const WithTwoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
},
{
loginUrl: "microsoft",
alias: "microsoft",
providerId: "microsoft",
displayName: "Microsoft",
iconClasses: "fa fa-windows"
}
]
}
}}
/>
)
};
export const WithNoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: []
}
}}
/>
)
};
export const WithMoreThanTwoSocialProviders: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
},
{
loginUrl: "microsoft",
alias: "microsoft",
providerId: "microsoft",
displayName: "Microsoft",
iconClasses: "fa fa-windows"
},
{
loginUrl: "facebook",
alias: "facebook",
providerId: "facebook",
displayName: "Facebook",
iconClasses: "fa fa-facebook"
},
{
loginUrl: "twitter",
alias: "twitter",
providerId: "twitter",
displayName: "Twitter",
iconClasses: "fa fa-twitter"
}
]
}
}}
/>
)
};
export const WithSocialProvidersAndWithoutRememberMe: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
social: {
displayInfo: true,
providers: [
{
loginUrl: "google",
alias: "google",
providerId: "google",
displayName: "Google",
iconClasses: "fa fa-google"
}
]
},
realm: { rememberMe: false }
}}
/>
)
};

View File

@ -41,3 +41,24 @@ export const WithError: Story = {
/>
)
};
export const WithAppInitiatedAction: Story = {
render: () => (
<KcPageStory
kcContext={{
isAppInitiatedAction: true
}}
/>
)
};
export const WithPreFilledUserLabel: Story = {
render: () => (
<KcPageStory
kcContext={{
totp: {
otpCredentials: [{ userLabel: "MyDevice" }]
}
}}
/>
)
};

View File

@ -16,3 +16,49 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
export const WithIdpAlias: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: "Google",
brokerContext: {
username: "john.doe"
},
realm: {
displayName: "MyRealm"
}
}}
/>
)
};
export const WithoutIdpAlias: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: undefined,
brokerContext: {
username: "john.doe"
},
realm: {
displayName: "MyRealm"
}
}}
/>
)
};
export const WithCustomRealmDisplayName: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: "Facebook",
brokerContext: {
username: "jane.doe"
},
realm: {
displayName: "CustomRealm"
}
}}
/>
)
};

View File

@ -215,3 +215,65 @@ export const WithTermsAcceptance: Story = {
/>
)
};
export const WithTermsNotAccepted: Story = {
render: args => (
<KcPageStory
{...args}
kcContext={{
termsAcceptanceRequired: true,
messagesPerField: {
existsError: (fieldName: string) => fieldName === "termsAccepted",
get: (fieldName: string) => (fieldName === "termsAccepted" ? "You must accept the terms." : undefined)
}
}}
/>
)
};
export const WithFieldErrors: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "" },
email: { value: "invalid-email" }
}
},
messagesPerField: {
existsError: (fieldName: string) => ["username", "email"].includes(fieldName),
get: (fieldName: string) => {
if (fieldName === "username") return "Username is required.";
if (fieldName === "email") return "Invalid email format.";
}
}
}}
/>
)
};
export const WithReadOnlyFields: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "johndoe", readOnly: true },
email: { value: "jhon.doe@gmail.com", readOnly: false }
}
}
}}
/>
)
};
export const WithAutoGeneratedUsername: Story = {
render: () => (
<KcPageStory
kcContext={{
profile: {
attributesByName: {
username: { value: "autogenerated_username" }
}
}
}}
/>
)
};

View File

@ -45,3 +45,32 @@ export const French: Story = {
/>
)
};
export const WithErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
messagesPerField: {
existsError: () => true,
get: () => "An error occurred while processing your request."
}
}}
/>
)
};
export const Spanish: Story = {
render: () => (
<KcPageStory
kcContext={{
locale: {
currentLanguageTag: "es"
},
"x-keycloakify": {
messages: {
termsText: "<p>Mis términos en <strong>Español</strong></p>"
}
}
}}
/>
)
};