Compare commits

..

119 Commits

Author SHA1 Message Date
a2ea81b3b8 Bump version 2024-11-06 10:10:55 +01:00
a0461e3ef0 #711 2024-11-06 10:10:27 +01:00
5357626317 Bump version 2024-10-31 11:05:43 +01:00
552c95c59e https://github.com/keycloakify/keycloakify/pull/705#issuecomment-2448689532 2024-10-31 11:05:25 +01:00
50590697ca Bump version 2024-10-30 15:51:32 +01:00
e261736fa3 Fix: kcContext.scripts can be undefined in error.ftl 2024-10-30 15:51:16 +01:00
263f55fdd3 Bump version 2024-10-26 22:07:54 +00:00
2b7f8a24a3 Fix import.meta.env.BASE_URL not being corectly replaced when build in windows 2024-10-26 22:07:29 +00:00
dc4eac1a04 Bump version 2024-10-25 04:12:20 +02:00
53a427d190 Delegate add sotry 2024-10-25 04:12:07 +02:00
ae969f91ac Bump version 2024-10-25 02:57:41 +02:00
c83319d6f3 Tell if we should update kcGen based on the hash 2024-10-25 02:57:26 +02:00
329b4cb0fb Enable to link in any keycloakify starter 2024-10-25 02:56:57 +02:00
533f5992d1 coding style fix 2024-10-25 02:44:19 +02:00
cb103cc3e2 Bump version 2024-10-25 00:21:15 +00:00
afdf89fb12 Try to run format on behaf of the user when generating new files with the CLI 2024-10-25 00:20:35 +00:00
26a87b8eaa Remove debug log 2024-10-25 00:01:39 +00:00
25d31463f4 Make script delegation work on windows 2024-10-25 00:01:12 +00:00
2542c38c9b Make linking script work on windows server 2024-10-24 23:47:34 +00:00
7326038424 Fix linking script on windows 2024-10-24 23:21:12 +00:00
12e632d221 More sensible patch for building on windows 2024-10-24 22:38:26 +00:00
3fc2108214 Fix build for windows 2024-10-24 16:56:13 +00:00
babbe39494 Merge pull request #701 from nima70/main
Removed Error Message from Terms and Conditions Page
2024-10-20 21:10:56 +02:00
32b4585e39 I removed the local=en 2024-10-20 13:49:10 -04:00
43469a869c I removed the error message for terms and conditions page 2024-10-20 12:35:14 -04:00
6f823e6478 Bump version 2024-10-20 13:08:59 +02:00
e33693e20e Merge pull request #700 from nima70/main
Storybooks Second Release: Login Page Stories with Improved Coverage
2024-10-20 13:02:27 +02:00
ad3f091d4a Merge pull request #698 from keycloakify/decouple_userprofile_logic
Decouple userprofile logic
2024-10-20 12:52:48 +02:00
3ff01d186d account page test coverage improved 2024-10-19 19:10:32 -04:00
0cf8caa53b storybooks second release 2024-10-19 17:24:29 -04:00
25920c208d Release candidate 2024-10-19 22:33:34 +02:00
19da96113f Don't export internals 2024-10-19 22:33:34 +02:00
6e584e809e Merge branch 'main' into decouple_userprofile_logic 2024-10-19 22:29:48 +02:00
4185188a5b Release candidate 2024-10-19 22:28:10 +02:00
4273322ed5 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:30 +02:00
ba0532c95d docs: update README.md [skip ci] 2024-10-19 22:27:30 +02:00
3a2fe597ba Bump version 2024-10-19 22:27:30 +02:00
dda77952a0 #694 Probably some shell handle double quote differently 2024-10-19 22:27:30 +02:00
d2e518d96b #693 #692 2024-10-19 22:27:30 +02:00
f3a97b2538 Bump version 2024-10-19 22:27:29 +02:00
cacd017244 #696 2024-10-19 22:27:29 +02:00
f5b15a5ef6 Fix Phase two links 2024-10-19 22:27:29 +02:00
de620dca56 Fix light mode rendering 2024-10-19 22:27:29 +02:00
8decf4a3c9 Add phaseTwo as sponsor 2024-10-19 22:27:29 +02:00
831326952b Resize zone2 logo 2024-10-19 22:27:29 +02:00
27da578446 Bump version 2024-10-19 22:27:29 +02:00
2c1cca168f Resolve package.json path relative to the package.json 2024-10-19 22:27:29 +02:00
e498fb784b Bump version 2024-10-19 22:27:29 +02:00
2917719315 Add dir=rtl attribut to html when using a RTL language 2024-10-19 22:27:29 +02:00
9ed90995e4 typesafety fix 2024-10-19 22:27:29 +02:00
0f99bb5bdc fix: added parameter type for story context on register page 2024-10-19 22:27:29 +02:00
1f4d4473e4 Bump version 2024-10-19 22:27:29 +02:00
5332001ff4 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:29 +02:00
22241fd7ad docs: update README.md [skip ci] 2024-10-19 22:27:29 +02:00
ddeade9775 Changes:
- First draft of test coverage improvement for storybooks
- code's page html rendering issue fixed
2024-10-19 22:27:29 +02:00
f1cb165bdd Bump version 2024-10-19 22:27:29 +02:00
9873353990 Fix initialize-email-theme 2024-10-19 22:27:29 +02:00
b879569b81 Announcement about Keycloak 26 2024-10-19 22:27:29 +02:00
c3e821088b Bump version 2024-10-19 22:27:29 +02:00
dc4f386e7a Fix vite quitting if custom handler implemented 2024-10-19 22:27:29 +02:00
a40810b364 Bump version 2024-10-19 22:27:29 +02:00
1690629717 Fix: check for delegation of the eject-page command 2024-10-19 22:27:29 +02:00
9a6a71c8bc Fix litle inconsistency 2024-10-19 22:27:29 +02:00
d626699f08 Bump version 2024-10-19 22:27:29 +02:00
6aa60e685b Release candidate 2024-10-19 22:27:29 +02:00
9910762abc 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-19 22:27:29 +02:00
182fb430f1 Fix dead code 2024-10-19 22:27:29 +02:00
bda20e2fbe Release candidate 2024-10-19 22:27:29 +02:00
bc586eceef Make sure the update-kc-gen command is delegated when building with vite 2024-10-19 22:27:29 +02:00
128b27221a Release candidate 2024-10-19 22:27:29 +02:00
2dfb4eda9d No need to handle non react environement with custom handler support 2024-10-19 22:27:29 +02:00
fed6af4dfe Release candidate 2024-10-19 22:27:29 +02:00
c4ee6cd85c Fix not handling correctly exit cause 2024-10-19 22:27:29 +02:00
8fc307bd8d Release candidate 2024-10-19 22:27:29 +02:00
9e9ffcd586 add debug logs 2024-10-19 22:27:29 +02:00
49b064b5f2 Release candidate 2024-10-19 22:27:29 +02:00
ef6f5a4c23 Add other missing declaration files 2024-10-19 22:27:28 +02:00
e92562fd44 Release candidate 2024-10-19 22:27:28 +02:00
fe65ddb5f8 Fix missing exports 2024-10-19 22:27:28 +02:00
ffd405c6db Release candidate 2024-10-19 22:27:28 +02:00
9e41868e0d Implement custom handler cli hook 2024-10-19 22:27:28 +02:00
ca6accc889 Bump version 2024-10-19 22:27:28 +02:00
dfe2e1562a Fix cache issue 2024-10-19 22:27:28 +02:00
ab43bb73d7 Bump version 2024-10-19 22:27:28 +02:00
22b0b95e54 Update readme, support keycloak 26 2024-10-19 22:27:28 +02:00
290ad8b592 Update version ranges for Multi-Page account theme 2024-10-19 22:27:28 +02:00
d5519dbb55 Release candidate 2024-10-19 22:27:28 +02:00
4de9e059e9 Aditional context exclusion 2024-10-19 22:27:28 +02:00
e573aff6ae Release candidate 2024-10-19 22:27:28 +02:00
908e083dee Update version target range 2024-10-19 22:27:28 +02:00
ec29724997 Fix link in CONTRIBUTING.md 2024-10-19 22:27:28 +02:00
88756e9807 Bump version 2024-10-19 22:27:28 +02:00
80d8a0c4e3 ['select-radiobuttons'/'multiselect-checkboxes'] fixed 'inputOptionLabels' 2024-10-19 22:27:28 +02:00
7241f0c741 Bump version 2024-10-19 22:27:28 +02:00
8565eb3fb8 Update tsafe 2024-10-19 22:27:28 +02:00
87198f6e56 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
fa934da442 docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
6c4dc711d2 Put Kathi as first contributor 2024-10-19 22:27:28 +02:00
1f2a755a97 docs: update .all-contributorsrc [skip ci] 2024-10-19 22:27:28 +02:00
a0e3dc163a docs: update README.md [skip ci] 2024-10-19 22:27:28 +02:00
810dc6ceb5 Bump version 2024-10-19 22:27:28 +02:00
7203c742be Avoid modifying BASE_URL for App context 2024-10-19 22:27:28 +02:00
2fd04cfb61 Bump version 2024-10-19 22:27:28 +02:00
9c44d13f73 Update tsafe (provide ESM distribution) 2024-10-19 22:27:27 +02:00
d6436a58a2 update ci 2024-10-19 22:27:27 +02:00
613167f3a6 Bump version 2024-10-19 22:27:27 +02:00
ab0c281d98 Fix allegated vulnerability 2024-10-19 22:27:27 +02:00
c84dc281a2 Bump version 2024-10-19 22:27:27 +02:00
835833a61b Remove unessesary reference to react specific construct in KcContext 2024-10-19 22:27:27 +02:00
9af542ec89 Bump version 2024-10-19 22:27:27 +02:00
06e33196bb Refactor: Make ClassKey importable without having react as a dependency 2024-10-19 22:27:27 +02:00
36dd324139 complete decoupling of user profile form validation logic 2024-10-19 10:18:22 +02:00
52d4fe920c Merge pull request #697 from keycloakify/all-contributors/add-marvinruder
docs: add marvinruder as a contributor for bug
2024-10-19 03:21:05 +02:00
0d090d50d4 Bump version 2024-10-19 02:28:32 +02:00
e57232edde #694 Probably some shell handle double quote differently 2024-10-19 02:28:11 +02:00
dfe1e7ddd1 docs: update .all-contributorsrc [skip ci] 2024-10-19 00:24:37 +00:00
5ffc42c9db docs: update README.md [skip ci] 2024-10-19 00:24:36 +00:00
c63648a1b0 #693 #692 2024-10-19 02:19:41 +02:00
5892cf2ba7 Decouple user profile form logic so it can be consumed in angular 2024-09-30 00:19:37 +02:00
55 changed files with 3875 additions and 1500 deletions

View File

@ -300,6 +300,15 @@
"code",
"test"
]
},
{
"login": "marvinruder",
"name": "Marvin A. Ruder",
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
"profile": "https://mruder.dev",
"contributions": [
"bug"
]
}
],
"contributorsPerLine": 7,

View File

@ -164,6 +164,7 @@ 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/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>
<td align="center" valign="top" width="14.28%"><a href="https://mruder.dev"><img src="https://avatars.githubusercontent.com/u/18495294?v=4?s=100" width="100px;" alt="Marvin A. Ruder"/><br /><sub><b>Marvin A. Ruder</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/issues?q=author%3Amarvinruder" title="Bug reports">🐛</a></td>
</tr>
</tbody>
</table>

View File

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

View File

@ -1,10 +1,5 @@
import * as fs from "fs";
import {
join as pathJoin,
relative as pathRelative,
basename as pathBasename,
dirname as pathDirname
} from "path";
import { join as pathJoin, basename as pathBasename, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { run } from "../shared/run";
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
@ -41,13 +36,12 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
webpackConfigJsFilePath,
Buffer.from(
[
`const path = require('path');`,
``,
`module.exports = {`,
` mode: 'production',`,
` entry: '${filePath}',`,
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
` output: {`,
` path: '${webpackOutputDirPath}',`,
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
` filename: '${pathBasename(webpackOutputFilePath)}',`,
` libraryTarget: 'module',`,
` },`,

View File

@ -55,7 +55,6 @@ const commonThirdPartyDeps = [
Buffer.from(modifiedPackageJsonContent, "utf8")
);
}
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
@ -64,6 +63,21 @@ fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
if (targetModuleName === undefined) {
const packageJsonFilePath = pathJoin(cwd, "package.json");
const packageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
delete packageJson["packageManager"];
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(packageJson, null, 2))
);
}
const cmd = [
"yarn",
"link",
@ -77,7 +91,10 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
env: {
...process.env,
...(os.platform() === "win32"
? { USERPROFILE: yarnGlobalDirPath }
? {
USERPROFILE: yarnGlobalDirPath,
LOCALAPPDATA: yarnGlobalDirPath
}
: { HOME: yarnGlobalDirPath })
}
});

View File

@ -1,49 +1,88 @@
import * as fs from "fs";
import { join } from "path";
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
import { crawl } from "../src/bin/tools/crawl";
import { join as pathJoin, sep as pathSep } from "path";
import { run } from "./shared/run";
import cliSelect from "cli-select";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import chalk from "chalk";
import { removeNodeModules } from "./tools/removeNodeModules";
import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange";
{
const dirPath = "node_modules";
(async () => {
const parentDirPath = pathJoin(getThisCodebaseRootDirPath(), "..");
try {
fs.rmSync(dirPath, { recursive: true, force: true });
} catch {
// NOTE: This is a workaround for windows
// we can't remove locked executables.
const { starterName } = await (async () => {
const starterNames = fs
.readdirSync(parentDirPath)
.filter(
basename =>
basename.includes("starter") &&
basename.includes("keycloakify") &&
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
);
crawl({
dirPath,
returnedPathsType: "absolute"
}).forEach(filePath => {
try {
fs.rmSync(filePath, { force: true });
} catch (error) {
if (filePath.endsWith(".exe")) {
return;
}
throw error;
if (starterNames.length === 0) {
console.log(
chalk.red(
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
)
);
process.exit(-1);
}
const starterName = await (async () => {
if (starterNames.length === 1) {
return starterNames[0];
}
});
}
}
fs.rmSync("dist", { recursive: true, force: true });
fs.rmSync(".yarn_home", { recursive: true, force: true });
console.log(chalk.cyan(`\nSelect a starter to link in:`));
run("yarn install");
run("yarn build");
const { value } = await cliSelect<string>({
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
}).catch(() => {
process.exit(-1);
});
const starterName = "keycloakify-starter";
return value.split(pathSep)[1];
})();
fs.rmSync(join("..", starterName, "node_modules"), {
recursive: true,
force: true
});
return { starterName };
})();
run("yarn install", { cwd: join("..", starterName) });
const startTime = Date.now();
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
console.log(chalk.cyan(`\n\nLinking in ..${pathSep}${starterName}...`));
startRebuildOnSrcChange();
removeNodeModules({
nodeModulesDirPath: pathJoin(getThisCodebaseRootDirPath(), "node_modules")
});
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), "dist"), {
recursive: true,
force: true
});
fs.rmSync(pathJoin(getThisCodebaseRootDirPath(), ".yarn_home"), {
recursive: true,
force: true
});
run("yarn install");
run("yarn build");
const starterDirPath = pathJoin(parentDirPath, starterName);
removeNodeModules({
nodeModulesDirPath: pathJoin(starterDirPath, "node_modules")
});
run("yarn install", { cwd: starterDirPath });
run(`npx tsx ${pathJoin("scripts", "link-in-app.ts")} ${starterName}`);
const durationSeconds = Math.round((Date.now() - startTime) / 1000);
await new Promise(resolve => setTimeout(resolve, 1000));
startRebuildOnSrcChange();
console.log(chalk.green(`\n\nLinked in ${starterName} in ${durationSeconds}s`));
})();

View File

@ -0,0 +1,27 @@
import * as fs from "fs";
import { crawl } from "../../src/bin/tools/crawl";
export function removeNodeModules(params: { nodeModulesDirPath: string }) {
const { nodeModulesDirPath } = params;
try {
fs.rmSync(nodeModulesDirPath, { recursive: true, force: true });
} catch {
// NOTE: This is a workaround for windows
// we can't remove locked executables.
crawl({
dirPath: nodeModulesDirPath,
returnedPathsType: "absolute"
}).forEach(filePath => {
try {
fs.rmSync(filePath, { force: true });
} catch (error) {
if (filePath.endsWith(".exe")) {
return;
}
throw error;
}
});
}
}

View File

@ -15,10 +15,21 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { runFormat } from "./tools/runFormat";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "add-story",
buildContext
});
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
@ -121,6 +132,10 @@ export async function command(params: { buildContext: BuildContext }) {
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
console.log(
[
`${chalk.green("✓")} ${chalk.bold(

View File

@ -23,6 +23,7 @@ import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runFormat } from "./tools/runFormat";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -243,6 +244,10 @@ export async function command(params: { buildContext: BuildContext }) {
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
console.log(
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))

View File

@ -8,7 +8,7 @@ import {
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { dirname as pathDirname } from "path";
import { sep as pathSep } from "path";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
@ -19,7 +19,34 @@ export function maybeDelegateCommandToCustomHandler(params: {
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
const nodeModulesBinDirPath = (() => {
const binPath = process.argv[1];
const segments: string[] = [".bin"];
let foundNodeModules = false;
for (const segment of binPath.split(pathSep).reverse()) {
skip_segment: {
if (foundNodeModules) {
break skip_segment;
}
if (segment === "node_modules") {
foundNodeModules = true;
break skip_segment;
}
continue;
}
segments.unshift(segment);
}
return segments.join(pathSep);
})();
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false };
}

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] }))

View File

@ -0,0 +1,71 @@
import * as fs from "fs";
import { dirname as pathDirname } from "path";
import { assert, Equals } from "tsafe/assert";
import chalk from "chalk";
import { id } from "tsafe/id";
import { z } from "zod";
import { is } from "tsafe/is";
import * as child_process from "child_process";
export function runFormat(params: { packageJsonFilePath: string }) {
const { packageJsonFilePath } = params;
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
const { scripts } = parsedPackageJson;
if (scripts === undefined) {
return;
}
for (const scriptName of ["format", "lint"]) {
if (!(scriptName in scripts)) {
continue;
}
const command = `npm run ${scriptName}`;
console.log(chalk.grey(`$ ${command}`));
try {
child_process.execSync(`npm run ${scriptName}`, {
stdio: "inherit",
cwd: pathDirname(packageJsonFilePath)
});
} catch {
console.log(
chalk.yellow(
`\`${command}\` failed, it does not matter, please format your code manually, continuing...`
)
);
}
return;
}
}

View File

@ -3,6 +3,8 @@ import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "./tools/fs.existsAsync";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runFormat } from "./tools/runFormat";
import * as crypto from "crypto";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -18,95 +20,108 @@ export async function command(params: { buildContext: BuildContext }) {
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
`import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
const newContent = [
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``
]
.filter(item => typeof item === "string")
.join("\n");
const hash = crypto.createHash("sha256").update(newContent).digest("hex");
skip_if_no_changes: {
if (!(await existsAsync(filePath))) {
break skip_if_no_changes;
}
const currentContent = (await fs.readFile(filePath)).toString("utf8");
if (!currentContent.includes(hash)) {
break skip_if_no_changes;
}
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
await fs.writeFile(
filePath,
Buffer.from(
[
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n"),
"utf8"
)
);
runFormat({ packageJsonFilePath: buildContext.packageJsonFilePath });
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");

View File

@ -102,7 +102,7 @@ export declare namespace KcContext {
showTryAnotherWayLink?: boolean;
attemptedUsername?: string;
};
scripts: string[];
scripts?: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;

View File

@ -10,7 +10,7 @@ export type KcContextLike = {
resourcesPath: string;
ssoLoginInOtherTabsUrl: string;
};
scripts: string[];
scripts?: string[];
};
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
@ -45,10 +45,12 @@ export function useInitialize(params: {
type: "module",
src: `${url.resourcesPath}/js/menu-button-links.js`
},
...scripts.map(src => ({
type: "text/javascript" as const,
src
})),
...(scripts === undefined
? []
: scripts.map(src => ({
type: "text/javascript" as const,
src
}))),
{
type: "module",
textContent: `

View File

@ -16,6 +16,9 @@ import type { GenericI18n_noJsx } from "./GenericI18n_noJsx";
export type KcContextLike = {
themeName: string;
realm: {
internationalizationEnabled: boolean;
};
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
@ -91,14 +94,16 @@ export function createGetI18n<
return cachedResult;
}
const kcContextLocale = params.kcContext.realm.internationalizationEnabled ? params.kcContext.locale : undefined;
{
const currentLanguageTag = kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
const currentLanguageTag = kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
const html = document.querySelector("html");
assert(html !== null);
html.lang = currentLanguageTag;
const isRtl = (() => {
const { rtl } = kcContext.locale ?? {};
const { rtl } = kcContextLocale ?? {};
if (rtl !== undefined) {
return rtl;
@ -154,11 +159,11 @@ export function createGetI18n<
}
from_server: {
if (kcContext.locale === undefined) {
if (kcContextLocale === undefined) {
break from_server;
}
const supportedEntry = kcContext.locale.supported.find(entry => entry.languageTag === languageTag);
const supportedEntry = kcContextLocale.supported.find(entry => entry.languageTag === languageTag);
if (supportedEntry === undefined) {
break from_server;
@ -180,7 +185,7 @@ export function createGetI18n<
};
const currentLanguage: I18n["currentLanguage"] = (() => {
const languageTag = id<string>(kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
const languageTag = id<string>(kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
return {
languageTag,
@ -191,8 +196,8 @@ export function createGetI18n<
const enabledLanguages: I18n["enabledLanguages"] = (() => {
const enabledLanguages: I18n["enabledLanguages"] = [];
if (kcContext.locale !== undefined) {
for (const entry of kcContext.locale.supported ?? []) {
if (kcContextLocale !== undefined) {
for (const entry of kcContextLocale.supported ?? []) {
const languageTag = id<string>(entry.languageTag) as LanguageTag;
enabledLanguages.push({

View File

@ -92,7 +92,8 @@ export function createUseI18n<
const styleElement = document.createElement("style");
styleElement.attributes.setNamedItem(document.createAttribute(attributeName));
(styleElement.textContent = `[data-kc-msg] { display: inline-block; }`), document.head.prepend(styleElement);
styleElement.textContent = `[data-kc-msg] { display: inline-block; }`;
document.head.prepend(styleElement);
}
const { getI18n } = createGetI18n({ extraLanguageTranslations, messagesByLanguageTag_themeDefined });

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
export * from "./getUserProfileApi";

View File

@ -0,0 +1,109 @@
import { assert } from "keycloakify/tools/assert";
let cleanup: (() => void) | undefined;
const handledElements = new WeakSet<HTMLElement>();
const KC_NUMBER_UNFORMAT = "kcNumberUnFormat";
const SELECTOR = `input[data-${KC_NUMBER_UNFORMAT}]`;
export function unFormatNumberOnSubmit() {
cleanup?.();
const handleElement = (element: HTMLInputElement) => {
if (handledElements.has(element)) {
return;
}
const form = element.closest("form");
if (form === null) {
return;
}
form.addEventListener("submit", () => {
const rawFormat = element.getAttribute(`data-${KC_NUMBER_UNFORMAT}`);
if (rawFormat) {
element.value = formatNumber(element.value, rawFormat);
}
});
handledElements.add(element);
};
document.querySelectorAll(SELECTOR).forEach(element => {
assert(element instanceof HTMLInputElement);
handleElement(element);
});
const observer = new MutationObserver(mutationsList => {
for (const mutation of mutationsList) {
if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = (node as HTMLElement).querySelector(SELECTOR);
if (element !== null) {
assert(element instanceof HTMLInputElement);
handleElement(element);
}
}
});
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
cleanup = () => observer.disconnect();
}
// NOTE: Keycloak code
const formatNumber = (input: string, format: string) => {
if (!input) {
return "";
}
// array holding the patterns for the number of expected digits in each part
const digitPattern = format.match(/{\d+}/g);
if (!digitPattern) {
return "";
}
// calculate the maximum size of the given pattern based on the sum of the expected digits
const maxSize = digitPattern.reduce(
(total, p) => total + parseInt(p.replace("{", "").replace("}", "")),
0
);
// keep only digits
let rawValue = input.replace(/\D+/g, "");
// make sure the value is a number
//@ts-expect-error
if (parseInt(rawValue) != rawValue) {
return "";
}
// make sure the number of digits does not exceed the maximum size
if (rawValue.length > maxSize) {
rawValue = rawValue.substring(0, maxSize);
}
// build the regex based based on the expected digits in each part
const formatter = digitPattern.reduce((result, p) => result + `(\\d${p})`, "^");
// if the current digits match the pattern we have each group of digits in an array
let digits = new RegExp(formatter).exec(rawValue);
// no match, return the raw value without any format
if (!digits) {
return input;
}
let result = format;
// finally format the current digits accordingly to the given format
for (let i = 0; i < digitPattern.length; i++) {
result = result.replace(digitPattern[i], digits[i + 1]);
}
return result;
};

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -67,6 +68,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -1,5 +1,6 @@
import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type I18nLike = {
msgStr: (key: "recovery-codes-download-file-header" | "recovery-codes-download-file-description" | "recovery-codes-download-file-date") => string;
@ -137,6 +138,12 @@ export function useScript(params: { olRecoveryCodesListId: string; i18n: I18nLik
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: olRecoveryCodesListId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -59,6 +60,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@ -88,6 +89,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
insertScriptTags();
(async () => {
await waitForElementMountedOnDom({
elementId: authButtonId
});
insertScriptTags();
})();
}, [isFetchingTranslations]);
}

View File

@ -0,0 +1,30 @@
export async function waitForElementMountedOnDom(params: {
elementId: string;
}): Promise<void> {
const { elementId } = params;
const getElement = () => document.getElementById(elementId);
const element = getElement();
if (element === null) {
let prElementPresentInTheDom_resolve: () => void;
const prElementPresentInTheDom = new Promise<void>(
resolve => (prElementPresentInTheDom_resolve = resolve)
);
// Observe the dom for the element to be added
const observer = new MutationObserver(() => {
const element = getElement();
if (element === null) {
return;
}
observer.disconnect();
prElementPresentInTheDom_resolve();
});
observer.observe(document.body, { childList: true, subtree: true });
await prElementPresentInTheDom;
}
}

View File

@ -16,6 +16,7 @@ import {
} from "../bin/shared/buildContext";
import MagicString from "magic-string";
import { command as updateKcGenCommand } from "../bin/update-kc-gen";
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
export namespace keycloakify {
export type Params = BuildOptions & {
@ -130,6 +131,8 @@ export function keycloakify(params: keycloakify.Params) {
await updateKcGenCommand({ buildContext });
},
transform: (code, id) => {
id = replaceAll(id, "/", pathSep);
assert(command !== undefined);
assert(shouldGenerateSourcemap !== undefined);

View File

@ -16,3 +16,105 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* UsernameNotEditable:
* - Purpose: Test the scenario where the username field is not editable.
* - Scenario: The component renders, but the username field is disabled.
* - Key Aspect: Ensures that the `editUsernameAllowed` condition is respected and the username field is read-only.
*/
export const UsernameNotEditable: Story = {
render: () => (
<KcPageStory
kcContext={{
account: {
username: "john_doe",
email: "john.doe@gmail.com",
firstName: "John",
lastName: "Doe"
},
realm: {
registrationEmailAsUsername: false,
editUsernameAllowed: false
},
referrer: {
url: "/home"
},
url: {
accountUrl: "/account"
},
messagesPerField: {
printIfExists: () => ""
},
stateChecker: "state-checker"
}}
/>
)
};
/**
* WithValidationErrors:
* - Purpose: Test the form when there are validation errors.
* - Scenario: The component renders with error messages for invalid input in the fields.
* - Key Aspect: Ensures that error messages are properly displayed and the user can correct their inputs.
*/
export const WithValidationErrors: Story = {
render: () => (
<KcPageStory
kcContext={{
account: {
username: "john_doe",
email: "",
firstName: "",
lastName: "Doe"
},
realm: {
registrationEmailAsUsername: false,
editUsernameAllowed: true
},
referrer: {
url: "/home"
},
url: {
accountUrl: "/account"
},
messagesPerField: {
printIfExists: field => (field === "email" || field === "firstName" ? "has-error" : "")
},
stateChecker: "state-checker"
}}
/>
)
};
/**
* EmailAsUsername:
* - Purpose: Test the form where email is used as the username.
* - Scenario: The component renders without a separate username field, and the email field is treated as the username.
* - Key Aspect: Ensures the form functions correctly when `registrationEmailAsUsername` is enabled.
*/
export const EmailAsUsername: Story = {
render: () => (
<KcPageStory
kcContext={{
account: {
email: "john.doe@gmail.com",
firstName: "John",
lastName: "Doe"
},
realm: {
registrationEmailAsUsername: true
},
referrer: {
url: "/home"
},
url: {
accountUrl: "/account"
},
messagesPerField: {
printIfExists: () => ""
},
stateChecker: "state-checker"
}}
/>
)
};

View File

@ -78,3 +78,151 @@ export const Default: Story = {
/>
)
};
// No Available Roles or Grants Scenario
export const NoAvailableRolesOrGrants: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "applications.ftl",
applications: {
applications: [
{
realmRolesAvailable: [],
resourceRolesAvailable: {},
additionalGrants: [],
clientScopesGranted: [],
effectiveUrl: "#",
client: {
clientId: "application1",
name: "Application 1",
consentRequired: true
}
}
]
}
}}
/>
)
};
// Consent Not Required Scenario
export const ConsentNotRequired: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "applications.ftl",
applications: {
applications: [
{
realmRolesAvailable: [],
resourceRolesAvailable: {},
additionalGrants: [],
clientScopesGranted: [],
effectiveUrl: "#",
client: {
clientId: "application1",
name: "Application 1",
consentRequired: false // No consent required
}
}
]
}
}}
/>
)
};
// No Roles Available but Consent Required Scenario
export const NoRolesButConsentRequired: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "applications.ftl",
applications: {
applications: [
{
realmRolesAvailable: [],
resourceRolesAvailable: {},
additionalGrants: [],
clientScopesGranted: ["scope1", "scope2"], // Consent required but no roles
effectiveUrl: "#",
client: {
clientId: "application1",
name: "Application 1",
consentRequired: true
}
}
]
}
}}
/>
)
};
// Only Resource Roles Available Scenario
export const OnlyResourceRolesAvailable: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "applications.ftl",
applications: {
applications: [
{
realmRolesAvailable: [], // No realm roles
resourceRolesAvailable: {
resource1: [
{
roleName: "Resource Role Name 1",
roleDescription: "Resource role 1 description",
clientName: "Client Name 1",
clientId: "client1"
}
]
},
additionalGrants: [],
clientScopesGranted: [],
effectiveUrl: "#",
client: {
clientId: "application1",
name: "Application 1",
consentRequired: true
}
}
]
}
}}
/>
)
};
// No Additional Grants Scenario
export const NoAdditionalGrants: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "applications.ftl",
applications: {
applications: [
{
realmRolesAvailable: [
{
name: "Realm Role Name 1"
}
],
resourceRolesAvailable: {},
additionalGrants: [], // No additional grants
clientScopesGranted: [],
effectiveUrl: "#",
client: {
clientId: "application1",
name: "Application 1",
consentRequired: true
}
}
]
}
}}
/>
)
};

View File

@ -36,3 +36,61 @@ export const NotConnected: Story = {
/>
)
};
/**
* RemoveLinkNotPossible:
* - Federated identities are connected, but the user cannot remove them due to restrictions.
*/
export const RemoveLinkNotPossible: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "federatedIdentity.ftl",
federatedIdentity: {
identities: [
{
providerId: "google",
displayName: "Google",
userName: "john.doe@gmail.com",
connected: true
}
],
removeLinkPossible: false
},
stateChecker: "1234",
url: {
socialUrl: "/social"
}
}}
/>
)
};
/**
* AddLinkForUnconnectedIdentity:
* - The user has an identity that is not connected and can add it.
*/
export const AddLinkForUnconnectedIdentity: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "federatedIdentity.ftl",
federatedIdentity: {
identities: [
{
providerId: "github",
displayName: "GitHub",
userName: "",
connected: false
}
],
removeLinkPossible: true
},
stateChecker: "1234",
url: {
socialUrl: "/social"
}
}}
/>
)
};

View File

@ -355,3 +355,107 @@ export const Default: Story = {
/>
)
};
export const LogsMissingDetails: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "log.ftl",
log: {
events: [
{
date: "2024-04-26T12:29:08Z",
ipAddress: "127.0.0.1",
client: "",
details: [],
event: "login"
}
]
}
}}
/>
)
};
export const SingleLogEntry: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "log.ftl",
log: {
events: [
{
date: "2024-04-26T12:29:08Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{ key: "auth_method", value: "openid-connect" },
{ key: "username", value: "john.doe" }
],
event: "login"
}
]
}
}}
/>
)
};
export const LogsWithLongDetails: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "log.ftl",
log: {
events: [
{
date: "2024-04-26T12:29:08Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{ key: "auth_method", value: "openid-connect" },
{ key: "username", value: "john.doe" },
{ key: "session_duration", value: "2 hours 30 minutes 45 seconds" },
{ key: "location", value: "Windsor, Ontario, Canada" },
{ key: "user_agent", value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" }
],
event: "login"
}
]
}
}}
/>
)
};
export const EmptyClientField: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "log.ftl",
log: {
events: [
{
date: "2024-04-26T12:29:08Z",
ipAddress: "127.0.0.1",
client: "", // Empty client field
details: [
{ key: "auth_method", value: "openid-connect" },
{ key: "username", value: "john.doe" }
],
event: "login"
}
]
}
}}
/>
)
};
export const NoLogsAvailable: Story = {
render: () => (
<KcPageStory
kcContext={{
pageId: "log.ftl",
log: {
events: [] // No log events
}
}}
/>
)
};

View File

@ -26,3 +26,79 @@ export const WithMessage: Story = {
/>
)
};
/**
* FirstTimePasswordSetup:
* - Purpose: Tests the page when no password is set (e.g., first login).
* - Scenario: This renders the form without the current password field.
* - Key Aspect: Ensures the page only displays fields for setting a new password.
*/
export const FirstTimePasswordSetup: Story = {
render: () => (
<KcPageStory
kcContext={{
account: {
username: "john_doe"
},
password: {
passwordSet: false
},
url: {
passwordUrl: "/password"
},
stateChecker: "state-checker"
}}
/>
)
};
/**
* IncorrectCurrentPassword:
* - Purpose: Simulates validation error when the current password is incorrect.
* - Scenario: This renders the page with an error message indicating the current password is incorrect.
* - Key Aspect: Validates that an error message is correctly displayed for the current password input.
*/
export const IncorrectCurrentPassword: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { type: "error", summary: "Incorrect current password." },
account: {
username: "john_doe"
},
password: {
passwordSet: true
},
url: {
passwordUrl: "/password"
},
stateChecker: "state-checker"
}}
/>
)
};
/**
* SubmissionSuccessWithRedirect:
* - Purpose: Simulates a successful form submission with a redirect or success message.
* - Scenario: After successfully changing the password, a success message and redirect behavior are triggered.
* - Key Aspect: Verifies the handling of successful submissions.
*/
export const SubmissionSuccessWithRedirect: Story = {
render: () => (
<KcPageStory
kcContext={{
message: { type: "success", summary: "Password successfully changed." },
account: {
username: "john_doe"
},
password: {
passwordSet: true
},
url: {
passwordUrl: "/password"
},
stateChecker: "state-checker"
}}
/>
)
};

View File

@ -57,3 +57,97 @@ export const WithError: Story = {
/>
)
};
/**
* No active sessions scenario:
* - Simulates the scenario where no sessions are active for the user.
*/
export const NoActiveSessions: Story = {
render: () => (
<KcPageStory
kcContext={{
sessions: {
sessions: []
},
stateChecker: "randomStateCheckerValue"
}}
/>
)
};
/**
* Single session scenario:
* - Displays only one active session with session details.
*/
export const SingleSession: Story = {
render: () => (
<KcPageStory
kcContext={{
sessions: {
sessions: [
{
expires: "2024-04-26T18:14:19Z",
clients: ["account"],
ipAddress: "172.20.0.1",
started: "2024-04-26T08:14:19Z",
lastAccess: "2024-04-26T08:30:54Z",
id: "single-session-id"
}
]
},
stateChecker: "anotherStateChecker"
}}
/>
)
};
/**
* Multiple clients per session scenario:
* - Displays sessions where each session has multiple associated clients.
*/
export const MultipleClientsSession: Story = {
render: () => (
<KcPageStory
kcContext={{
sessions: {
sessions: [
{
expires: "2024-04-26T18:14:19Z",
clients: ["account", "admin-console", "another-client"],
ipAddress: "172.20.0.1",
started: "2024-04-26T08:14:19Z",
lastAccess: "2024-04-26T08:30:54Z",
id: "multiple-clients-session"
}
]
},
stateChecker: "multiClientsStateChecker"
}}
/>
)
};
/**
* Session without client details scenario:
* - Simulates a session where no client information is provided.
*/
export const SessionWithoutClients: Story = {
render: () => (
<KcPageStory
kcContext={{
sessions: {
sessions: [
{
expires: "2024-04-26T18:14:19Z",
clients: [], // No clients information
ipAddress: "172.20.0.1",
started: "2024-04-26T08:14:19Z",
lastAccess: "2024-04-26T08:30:54Z",
id: "no-clients-session"
}
]
},
stateChecker: "noClientsStateChecker"
}}
/>
)
};

View File

@ -180,3 +180,64 @@ export const MoreThanOneTotpProviders: Story = {
/>
)
};
// TOTP Enabled but No Existing OTP Credentials
export const TotpEnabledNoOtpCredentials: Story = {
render: () => (
<KcPageStory
kcContext={{
totp: {
enabled: true,
totpSecretEncoded: "HE4W MSTC OBKU CY2M",
otpCredentials: [] // No OTP Credentials
},
stateChecker: "stateChecker123",
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
}
}}
/>
)
};
// Manual Mode TOTP without Scanning
export const ManualModeTotp: Story = {
render: () => (
<KcPageStory
kcContext={{
mode: "manual", // Manual mode
totp: {
enabled: false,
totpSecretEncoded: "HE4W MSTC OBKU CY2M",
otpCredentials: []
},
stateChecker: "stateChecker123",
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
}
}}
/>
)
};
// Multiple OTP Devices Scenario
export const MultipleOtpDevices: Story = {
render: () => (
<KcPageStory
kcContext={{
totp: {
enabled: true,
totpSecretEncoded: "G55E MZKC JFUD",
otpCredentials: [
{ id: "1", userLabel: "Phone 1" },
{ id: "2", userLabel: "Tablet" }
]
},
stateChecker: "stateChecker123",
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
}
}}
/>
)
};

View File

@ -2,6 +2,14 @@ import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { createKcPageStory } from "../KcPageStory";
// Mock kcContext to avoid the TS2304 error
const mockKcContext = {
url: {
loginAction: "/login-action"
},
idpAlias: "mockIdpAlias"
};
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm.ftl" });
const meta = {
@ -13,6 +21,35 @@ export default meta;
type Story = StoryObj<typeof meta>;
/**
* Default:
* - Purpose: Tests standard behavior with mock data.
* - Scenario: The component renders with a mocked identity provider alias (`mockIdpAlias`) and a login action URL (`/login-action`).
* - Key Aspect: Ensures the default behavior of the component with standard values for kcContext.
*/
export const Default: Story = {
render: () => <KcPageStory />
render: () => <KcPageStory kcContext={mockKcContext} />
};
/**
* WithFormSubmissionError:
* - Purpose: Tests how the component handles form submission errors.
* - Scenario: Simulates a form submission error by setting the login action URL to `/error` and displays an error message.
* - Key Aspect: Verifies that the component can display error messages during form submission failure, ensuring proper error handling.
*/
export const WithFormSubmissionError: Story = {
render: () => (
<KcPageStory
kcContext={{
...mockKcContext,
url: {
loginAction: "/error"
},
message: {
type: "error",
summary: "An error occurred during form submission."
}
}}
/>
)
};

View File

@ -2,6 +2,20 @@ import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { createKcPageStory } from "../KcPageStory";
// Mock kcContext to avoid TS2304 error and to simulate the real environment
const mockKcContext = {
url: {
loginAction: "/login-action"
},
idpAlias: "mockIdpAlias",
brokerContext: {
username: "mockUser"
},
realm: {
displayName: "MockRealm"
}
};
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-email.ftl" });
const meta = {
@ -13,13 +27,27 @@ export default meta;
type Story = StoryObj<typeof meta>;
/**
* Default:
* - Purpose: Tests the default behavior with mock data.
* - Scenario: The component renders with a mocked identity provider alias (`mockIdpAlias`), a default broker username (`mockUser`), and a default realm name (`MockRealm`).
* - Key Aspect: Ensures the default behavior of the component with typical kcContext values.
*/
export const Default: Story = {
render: () => <KcPageStory />
render: () => <KcPageStory kcContext={mockKcContext} />
};
/**
* WithIdpAlias:
* - Purpose: Tests behavior when the idpAlias is set to "Google".
* - Scenario: Simulates the component being used with a Google identity provider, showing the username "john.doe" and realm "MyRealm".
* - Key Aspect: Ensures the correct identity provider alias ("Google") and broker context (user info) are displayed in the email linking instructions.
*/
export const WithIdpAlias: Story = {
render: () => (
<KcPageStory
kcContext={{
...mockKcContext,
idpAlias: "Google",
brokerContext: {
username: "john.doe"
@ -31,32 +59,47 @@ export const WithIdpAlias: Story = {
/>
)
};
export const WithoutIdpAlias: Story = {
/**
* WithCustomRealmDisplayName:
* - Purpose: Tests behavior when the realm display name is customized.
* - Scenario: Simulates the component with a Facebook identity provider, a broker username "jane.doe", and a custom realm name "CustomRealm".
* - Key Aspect: Ensures that custom realm display names are rendered correctly alongside the idpAlias and broker context.
*/
export const WithCustomRealmDisplayName: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: undefined,
...mockKcContext,
idpAlias: "Facebook",
brokerContext: {
username: "john.doe"
username: "jane.doe"
},
realm: {
displayName: "MyRealm"
displayName: "CUSTOM REALM DISPLAY NAME"
}
}}
/>
)
};
export const WithCustomRealmDisplayName: Story = {
/**
* WithFormSubmissionError:
* - Purpose: Tests how the component handles form submission errors.
* - Scenario: Simulates a form submission error by setting the login action URL to `/error` and displays an error message.
* - Key Aspect: Verifies that the component can display error messages during form submission failure, ensuring proper error handling.
*/
export const WithFormSubmissionError: Story = {
render: () => (
<KcPageStory
kcContext={{
idpAlias: "Facebook",
brokerContext: {
username: "jane.doe"
...mockKcContext,
url: {
loginAction: "/error"
},
realm: {
displayName: "CustomRealm"
message: {
type: "error",
summary: "An error occurred during form submission."
}
}}
/>

View File

@ -16,3 +16,47 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithErrorMessage:
* - Purpose: Tests when there is an error with the OAuth2 device user code entry.
* - Scenario: The component renders with an error message displayed to the user.
* - Key Aspect: Ensures the error message is properly shown when the user enters an invalid code.
*/
export const WithErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
oauth2DeviceVerificationAction: "/mock-oauth2-device-verification"
},
message: {
summary: "The user code you entered is invalid. Please try again.",
type: "error"
}
}}
/>
)
};
/**
* WithEmptyInputField:
* - Purpose: Tests when the user code field is left empty.
* - Scenario: The component renders the form, and the user tries to submit without entering any code.
* - Key Aspect: Ensures the form displays validation errors when the field is left empty.
*/
export const WithEmptyInputField: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
oauth2DeviceVerificationAction: "/mock-oauth2-device-verification"
},
message: {
summary: "User code cannot be empty. Please enter a valid code.",
type: "error"
}
}}
/>
)
};

View File

@ -2,6 +2,25 @@ import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { createKcPageStory } from "../KcPageStory";
// Mock kcContext to simulate real environment
const mockKcContext = {
url: {
oauthAction: "/oauth-action"
},
oauth: {
clientScopesRequested: [{ consentScreenText: "Scope1", dynamicScopeParameter: "dynamicScope1" }, { consentScreenText: "Scope2" }],
code: "mockCode"
},
client: {
attributes: {
policyUri: "https://twitter.com/en/tos",
tosUri: "https://twitter.com/en/privacy"
},
name: "Twitter",
clientId: "twitter-client-id"
}
};
const { KcPageStory } = createKcPageStory({ pageId: "login-oauth-grant.ftl" });
const meta = {
@ -13,6 +32,55 @@ export default meta;
type Story = StoryObj<typeof meta>;
/**
* Default:
* - Purpose: Tests the default behavior with meaningful logo (Twitter).
* - Scenario: The component renders with Twitter as the client, displaying its logo, policy, and terms of service links.
* - Key Aspect: Ensures the component works with a realistic `logoUri` and client name.
*/
export const Default: Story = {
render: () => <KcPageStory />
render: () => <KcPageStory kcContext={mockKcContext} />
};
/**
* WithoutScopes:
* - Purpose: Tests the component when no OAuth scopes are requested.
* - Scenario: The component renders with no scopes listed under the consent screen.
* - Key Aspect: Ensures the component renders correctly when there are no requested scopes.
*/
export const WithoutScopes: Story = {
render: () => (
<KcPageStory
kcContext={{
...mockKcContext,
oauth: {
...mockKcContext.oauth,
clientScopesRequested: []
}
}}
/>
)
};
/**
* WithFormSubmissionError:
* - Purpose: Tests how the component handles form submission errors.
* - Scenario: The `oauthAction` URL is set to an error route and an error message is displayed.
* - Key Aspect: Ensures that the component can display error messages when form submission fails.
*/
export const WithFormSubmissionError: Story = {
render: () => (
<KcPageStory
kcContext={{
...mockKcContext,
url: {
oauthAction: "/error"
},
message: {
type: "error",
summary: "An error occurred during form submission."
}
}}
/>
)
};

View File

@ -16,3 +16,113 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* MultipleOtpCredentials:
* - Purpose: Tests the behavior when the user has multiple OTP credentials to choose from.
* - Scenario: Simulates the scenario where the user is presented with multiple OTP credentials and must select one to proceed.
* - Key Aspect: Ensures that multiple OTP credentials are listed and selectable, and the correct credential is selected by default.
*/
export const MultipleOtpCredentials: Story = {
render: () => (
<KcPageStory
kcContext={{
otpLogin: {
userOtpCredentials: [
{ id: "credential1", userLabel: "Device 1" },
{ id: "credential2", userLabel: "Device 2" },
{ id: "credential2", userLabel: "Device 3" },
{ id: "credential2", userLabel: "Device 4" },
{ id: "credential2", userLabel: "Device 5" },
{ id: "credential2", userLabel: "Device 6" }
],
selectedCredentialId: "credential1"
},
url: {
loginAction: "/login-action"
},
messagesPerField: {
existsError: () => false
}
}}
/>
)
};
/**
* WithOtpError:
* - Purpose: Tests the behavior when an error occurs with the OTP field (e.g., invalid OTP code).
* - Scenario: Simulates an invalid OTP code scenario where an error message is displayed.
* - Key Aspect: Ensures that the OTP input displays error messages correctly and the error is visible.
*/
export const WithOtpError: Story = {
render: () => (
<KcPageStory
kcContext={{
otpLogin: {
userOtpCredentials: []
},
url: {
loginAction: "/login-action"
},
messagesPerField: {
existsError: (field: string) => field === "totp",
get: () => "Invalid OTP code"
}
}}
/>
)
};
/**
* NoOtpCredentials:
* - Purpose: Tests the behavior when no OTP credentials are provided for the user.
* - Scenario: Simulates the scenario where the user is not presented with any OTP credentials, and only the OTP input is displayed.
* - Key Aspect: Ensures that the component handles cases where there are no user OTP credentials, and the user is only prompted for the OTP code.
*/
export const NoOtpCredentials: Story = {
render: () => (
<KcPageStory
kcContext={{
otpLogin: {
userOtpCredentials: []
},
url: {
loginAction: "/login-action"
},
messagesPerField: {
existsError: () => false
}
}}
/>
)
};
/**
* WithErrorAndMultipleOtpCredentials:
* - Purpose: Tests behavior when there is both an error in the OTP field and multiple OTP credentials.
* - Scenario: Simulates the case where the user has multiple OTP credentials and encounters an error with the OTP input.
* - Key Aspect: Ensures that the component can handle both multiple OTP credentials and display an error message simultaneously.
*/
export const WithErrorAndMultipleOtpCredentials: Story = {
render: () => (
<KcPageStory
kcContext={{
otpLogin: {
userOtpCredentials: [
{ id: "credential1", userLabel: "Device 1" },
{ id: "credential2", userLabel: "Device 2" }
],
selectedCredentialId: "credential1"
},
url: {
loginAction: "/login-action"
},
messagesPerField: {
existsError: (field: string) => field === "totp",
get: () => "Invalid OTP code"
}
}}
/>
)
};

View File

@ -16,3 +16,26 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithErrorMessage:
* - Purpose: Tests behavior when an error message is displayed along with the page expiration message.
* - Scenario: Simulates a case where the session expired due to an error, and an error message is displayed alongside the expiration message.
* - Key Aspect: Ensures that error messages are displayed correctly in addition to the page expiration notice.
*/
export const WithErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginRestartFlowUrl: "/mock-restart-flow",
loginAction: "/mock-continue-login"
},
message: {
type: "error",
summary: "An error occurred while processing your session."
}
}}
/>
)
};

View File

@ -16,3 +16,54 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithPasswordError:
* - Purpose: Tests the behavior when an error occurs in the password field (e.g., incorrect password).
* - Scenario: Simulates a scenario where an invalid password is entered, and an error message is displayed.
* - Key Aspect: Ensures that the password input field displays error messages correctly.
*/
export const WithPasswordError: Story = {
render: () => (
<KcPageStory
kcContext={{
realm: {
resetPasswordAllowed: true
},
url: {
loginAction: "/mock-login",
loginResetCredentialsUrl: "/mock-reset-password"
},
messagesPerField: {
existsError: (field: string) => field === "password",
get: () => "Invalid password"
}
}}
/>
)
};
/**
* WithoutResetPasswordOption:
* - Purpose: Tests the behavior when the reset password option is disabled.
* - Scenario: Simulates a scenario where the `resetPasswordAllowed` is set to `false`, and the "Forgot Password" link is not rendered.
* - Key Aspect: Ensures that the component handles cases where resetting the password is not allowed.
*/
export const WithoutResetPasswordOption: Story = {
render: () => (
<KcPageStory
kcContext={{
realm: {
resetPasswordAllowed: false
},
url: {
loginAction: "/mock-login",
loginResetCredentialsUrl: "/mock-reset-password"
},
messagesPerField: {
existsError: () => false
}
}}
/>
)
};

View File

@ -16,3 +16,25 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithErrorDuringCodeGeneration:
* - Purpose: Tests when an error occurs while generating recovery authentication codes.
* - Scenario: The component renders an error message to inform the user of the failure during code generation.
* - Key Aspect: Ensures that error messages are properly displayed when recovery code generation fails.
*/
export const WithErrorDuringCodeGeneration: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
message: {
summary: "An error occurred during recovery code generation. Please try again.",
type: "error"
}
}}
/>
)
};

View File

@ -16,3 +16,82 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithoutOtpCredentials:
* - Purpose: Tests the behavior when no OTP credentials are available.
* - Scenario: The component renders without any OTP credentials, showing only the submit button.
* - Key Aspect: Ensures that the component handles the absence of OTP credentials correctly.
*/
export const WithoutOtpCredentials: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login"
},
configuredOtpCredentials: {
userOtpCredentials: [],
selectedCredentialId: undefined
},
messagesPerField: {
existsError: () => false
}
}}
/>
)
};
/**
* WithOtpError:
* - Purpose: Tests the behavior when an error occurs with the OTP selection.
* - Scenario: Simulates a scenario where an error occurs (e.g., no OTP selected), and an error message is displayed.
* - Key Aspect: Ensures that error messages are displayed correctly for OTP-related errors.
*/
export const WithOtpError: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login"
},
configuredOtpCredentials: {
userOtpCredentials: [
{ id: "otp1", userLabel: "Device 1" },
{ id: "otp2", userLabel: "Device 2" }
],
selectedCredentialId: "otp1"
},
messagesPerField: {
existsError: (field: string) => field === "totp",
get: () => "Invalid OTP selection"
}
}}
/>
)
};
/**
* WithOnlyOneOtpCredential:
* - Purpose: Tests the behavior when there is only one OTP credential available.
* - Scenario: Simulates the case where the user has only one OTP credential, and it is pre-selected by default.
* - Key Aspect: Ensures that the component renders correctly with only one OTP credential pre-selected.
*/
export const WithOnlyOneOtpCredential: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login"
},
configuredOtpCredentials: {
userOtpCredentials: [{ id: "otp1", userLabel: "Device 1" }],
selectedCredentialId: "otp1"
},
messagesPerField: {
existsError: () => false
}
}}
/>
)
};

View File

@ -29,3 +29,33 @@ export const WithEmailAsUsername: Story = {
/>
)
};
/**
* WithUsernameError:
* - Purpose: Tests behavior when an error occurs with the username input (e.g., invalid username).
* - Scenario: The component displays an error message next to the username input field.
* - Key Aspect: Ensures the username input shows error messages when validation fails.
*/
export const WithUsernameError: Story = {
render: () => (
<KcPageStory
kcContext={{
realm: {
loginWithEmailAllowed: false,
registrationEmailAsUsername: false,
duplicateEmailsAllowed: false
},
url: {
loginAction: "/mock-login-action",
loginUrl: "/mock-login-url"
},
messagesPerField: {
existsError: (field: string) => field === "username",
get: () => "Invalid username"
},
auth: {
attemptedUsername: "invalid_user"
}
}}
/>
)
};

View File

@ -16,3 +16,49 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithPasswordError:
* - Purpose: Tests when there is an error in the password input (e.g., invalid password).
* - Scenario: Simulates the case where the user enters an invalid password, and an error message is displayed.
* - Key Aspect: Ensures the password input field shows an error message when validation fails.
*/
export const WithPasswordError: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
messagesPerField: {
existsError: (field: string) => field === "password",
get: () => "Password must be at least 8 characters long."
},
isAppInitiatedAction: false
}}
/>
)
};
/**
* WithPasswordConfirmError:
* - Purpose: Tests when there is an error in the password confirmation input (e.g., passwords do not match).
* - Scenario: Simulates the case where the user enters mismatching passwords, and an error message is displayed in the confirmation field.
* - Key Aspect: Ensures that the password confirmation field shows an error when passwords do not match.
*/
export const WithPasswordConfirmError: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
messagesPerField: {
existsError: (field: string) => field === "password-confirm",
get: () => "Passwords do not match."
},
isAppInitiatedAction: false
}}
/>
)
};

View File

@ -16,3 +16,26 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithProfileError:
* - Purpose: Tests when an error occurs in one or more profile fields (e.g., invalid email format).
* - Scenario: The component displays error messages next to the affected fields.
* - Key Aspect: Ensures the profile fields show error messages when validation fails.
*/
export const WithProfileError: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
messagesPerField: {
existsError: (field: string) => field === "email",
get: () => "Invalid email format"
},
isAppInitiatedAction: false
}}
/>
)
};

View File

@ -28,3 +28,78 @@ export const Default: Story = {
/>
)
};
/**
* WithSuccessMessage:
* - Purpose: Tests when the email verification is successful, and the user receives a confirmation message.
* - Scenario: The component renders a success message instead of a warning or error.
* - Key Aspect: Ensures the success message is displayed correctly when the email is successfully verified.
*/
export const WithSuccessMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: {
summary: "Your email has been successfully verified.",
type: "success"
},
user: {
email: "john.doe@gmail.com"
},
url: {
loginAction: "/mock-login-action"
}
}}
/>
)
};
/**
* WithErrorMessage:
* - Purpose: Tests when there is an error during the email verification process.
* - Scenario: The component renders an error message indicating the email verification failed.
* - Key Aspect: Ensures the error message is shown correctly when the verification process encounters an issue.
*/
export const WithErrorMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: {
summary: "There was an error verifying your email. Please try again.",
type: "error"
},
user: {
email: "john.doe@gmail.com"
},
url: {
loginAction: "/mock-login-action"
}
}}
/>
)
};
/**
* WithInfoMessage:
* - Purpose: Tests when the user is prompted to verify their email without any urgency.
* - Scenario: The component renders with an informational message for email verification.
* - Key Aspect: Ensures the informational message is displayed properly.
*/
export const WithInfoMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
message: {
summary: "Please verify your email to continue using our services.",
type: "info"
},
user: {
email: "john.doe@gmail.com"
},
url: {
loginAction: "/mock-login-action"
}
}}
/>
)
};

View File

@ -16,3 +16,28 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithoutUserEnabled:
* - Purpose: Tests when the user is not enabled to log in via x509.
* - Scenario: The component renders the certificate details but does not provide the option to log in or cancel.
* - Key Aspect: Ensures that the login buttons are not displayed when the user is not enabled.
*/
export const WithoutUserEnabled: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
x509: {
formData: {
subjectDN: "CN=John Doe, OU=Example Org, O=Example Inc, C=US",
username: "johndoe",
isUserEnabled: false // User not enabled for login
}
}
}}
/>
)
};

View File

@ -16,3 +16,32 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithCustomLogoutMessage:
* - Purpose: Tests when a custom message is displayed for the logout confirmation.
* - Scenario: The component renders with a custom logout confirmation message instead of the default one.
* - Key Aspect: Ensures the custom logout message is displayed correctly.
*/
export const WithCustomLogoutMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
logoutConfirmAction: "/mock-logout-action"
},
client: {
baseUrl: "/mock-client-url"
},
logoutConfirm: {
code: "mock-session-code",
skipLink: false
},
message: {
summary: "Are you sure you want to log out from all sessions?",
type: "warning"
}
}}
/>
)
};

View File

@ -82,3 +82,24 @@ export const WithRealmTranslations: Story = {
/>
)
};
/**
* WithoutAuthenticationSelections:
* - Purpose: Tests when no authentication methods are available for selection.
* - Scenario: The component renders without any authentication options, providing a default message or fallback.
* - Key Aspect: Ensures that the component gracefully handles the absence of available authentication methods.
*/
export const WithoutAuthenticationSelections: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
auth: {
authenticationSelections: [] // No authentication methods available
}
}}
/>
)
};

View File

@ -45,18 +45,6 @@ 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: () => (
@ -74,3 +62,27 @@ export const Spanish: Story = {
/>
)
};
export const LongMessage: Story = {
render: () => (
<KcPageStory
kcContext={{
"x-keycloakify": {
messages: {
termsText: `
<p>These are the terms and conditions. Please read them carefully.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus lacinia odio vitae vestibulum vestibulum.</p>
<p>Cras vehicula diam vel metus faucibus, at scelerisque lacus pretium. Donec ac consectetur justo. Morbi in sollicitudin nulla.</p>
<p>Suspendisse potenti. Phasellus pharetra consequat ante, at dictum ligula volutpat a. Quisque ultricies velit nec nulla gravida accumsan.</p>
<p>Curabitur tristique, magna id hendrerit tristique, enim nunc laoreet erat, non accumsan lacus arcu et nulla.</p>
<p>Fusce feugiat nisi orci, in placerat dui luctus ut. Suspendisse in velit eu urna auctor consequat a euismod enim.</p>
<p>Etiam et massa a sapien pharetra mollis. In lacinia quam id libero tincidunt, at egestas felis viverra.</p>
<p>Nunc pulvinar imperdiet facilisis. Curabitur ultricies dictum lectus, nec consectetur metus fringilla id.</p>
<p><strong>Please accept the terms to proceed.</strong></p>
`
}
}
}}
/>
)
};

View File

@ -16,3 +16,25 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithAppInitiatedAction:
* - Purpose: Tests when the form is displayed as part of an application-initiated action.
* - Scenario: The component renders the form with additional buttons like "Cancel."
* - Key Aspect: Ensures the "Cancel" button is visible and functional during app-initiated actions.
*/
export const WithAppInitiatedAction: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
messagesPerField: {
exists: () => false
},
isAppInitiatedAction: true
}}
/>
)
};

View File

@ -16,3 +16,144 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithMultipleAuthenticators:
* - Purpose: Tests when multiple WebAuthn authenticators are available for selection.
* - Scenario: The component renders multiple authenticators, allowing the user to choose between them.
* - Key Aspect: Ensures that the available authenticators are displayed, and the user can select one.
*/
export const WithMultipleAuthenticators: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
authenticators: {
authenticators: [
{
credentialId: "authenticator-1",
label: "Security Key 1",
transports: {
iconClass: "kcAuthenticatorUsbIcon",
displayNameProperties: ["USB"]
},
createdAt: "2023-01-01"
},
{
credentialId: "authenticator-2",
label: "Security Key 2",
transports: {
iconClass: "kcAuthenticatorNfcIcon",
displayNameProperties: ["NFC"]
},
createdAt: "2023-02-01"
}
]
},
shouldDisplayAuthenticators: true
}}
/>
)
};
/**
* WithSingleAuthenticator:
* - Purpose: Tests when only one WebAuthn authenticator is available.
* - Scenario: The component renders the WebAuthn form with a single available authenticator.
* - Key Aspect: Ensures the form renders correctly when there is only one authenticator available.
*/
export const WithSingleAuthenticator: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
authenticators: {
authenticators: [
{
credentialId: "authenticator-1",
label: "My Security Key",
transports: {
iconClass: "kcAuthenticatorUsbIcon",
displayNameProperties: ["USB"]
},
createdAt: "2023-01-01"
}
]
},
shouldDisplayAuthenticators: true
}}
/>
)
};
/**
* WithErrorDuringAuthentication:
* - Purpose: Tests the behavior when an error occurs during WebAuthn authentication.
* - Scenario: The component renders with an error message displayed to the user.
* - Key Aspect: Ensures the form handles authentication errors and displays a relevant message.
*/
export const WithErrorDuringAuthentication: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
authenticators: {
authenticators: [
{
credentialId: "authenticator-1",
label: "My Security Key",
transports: {
iconClass: "kcAuthenticatorUsbIcon",
displayNameProperties: ["USB"]
},
createdAt: "2023-01-01"
}
]
},
shouldDisplayAuthenticators: true,
message: {
summary: "An error occurred during WebAuthn authentication.",
type: "error"
}
}}
/>
)
};
/**
* WithJavaScriptDisabled:
* - Purpose: Tests the behavior when JavaScript is disabled or not functioning.
* - Scenario: The component renders a fallback message prompting the user to enable JavaScript for WebAuthn authentication.
* - Key Aspect: Ensures the form provides a clear message when JavaScript is required but unavailable.
*/
export const WithJavaScriptDisabled: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
authenticators: {
authenticators: [
{
credentialId: "authenticator-1",
label: "My Security Key",
transports: {
iconClass: "kcAuthenticatorUsbIcon",
displayNameProperties: ["USB"]
},
createdAt: "2023-01-01"
}
]
},
shouldDisplayAuthenticators: true
}}
/>
)
};

View File

@ -16,3 +16,72 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithRetryAvailable:
* - Purpose: Tests when the user can retry the WebAuthn authentication after an error.
* - Scenario: The component renders with a "Try Again" button to allow retrying the authentication process.
* - Key Aspect: Ensures the retry button functionality is visible and the user can retry authentication.
*/
export const WithRetryAvailable: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isAppInitiatedAction: false,
message: {
summary: "WebAuthn authentication failed. Please try again.",
type: "error"
}
}}
/>
)
};
/**
* WithAppInitiatedAction:
* - Purpose: Tests when the WebAuthn error form is part of an application-initiated action.
* - Scenario: The component renders with both a "Try Again" button and a "Cancel" button for app-initiated actions.
* - Key Aspect: Ensures the form renders correctly with both "Try Again" and "Cancel" options.
*/
export const WithAppInitiatedAction: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isAppInitiatedAction: true,
message: {
summary: "WebAuthn authentication failed. You can try again or cancel.",
type: "error"
}
}}
/>
)
};
/**
* WithJavaScriptDisabled:
* - Purpose: Tests the behavior when JavaScript is disabled or not functioning.
* - Scenario: The component renders with a message prompting the user to retry manually without JavaScript.
* - Key Aspect: Ensures the retry mechanism works properly when JavaScript is disabled or unavailable.
*/
export const WithJavaScriptDisabled: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isAppInitiatedAction: false,
message: {
summary: "JavaScript is disabled or not working. Please retry manually.",
type: "warning"
}
}}
/>
)
};

View File

@ -16,3 +16,47 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};
/**
* WithRetryAvailable:
* - Purpose: Tests when the user is allowed to retry WebAuthn registration after a failure.
* - Scenario: The component renders the form with a retry option.
* - Key Aspect: Ensures the retry functionality is available and the form allows the user to retry.
*/
export const WithRetryAvailable: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isSetRetry: true,
isAppInitiatedAction: false
}}
/>
)
};
/**
* WithErrorDuringRegistration:
* - Purpose: Tests when an error occurs during WebAuthn registration.
* - Scenario: The component displays an error message related to WebAuthn registration failure.
* - Key Aspect: Ensures the error message is displayed correctly, informing the user of the registration failure.
*/
export const WithErrorDuringRegistration: Story = {
render: () => (
<KcPageStory
kcContext={{
url: {
loginAction: "/mock-login-action"
},
isSetRetry: false,
isAppInitiatedAction: false,
message: {
summary: "An error occurred during WebAuthn registration. Please try again.",
type: "error"
}
}}
/>
)
};