Compare commits
165 Commits
angular_in
...
v11.3.16
Author | SHA1 | Date | |
---|---|---|---|
a2ea81b3b8 | |||
a0461e3ef0 | |||
5357626317 | |||
552c95c59e | |||
50590697ca | |||
e261736fa3 | |||
263f55fdd3 | |||
2b7f8a24a3 | |||
dc4eac1a04 | |||
53a427d190 | |||
ae969f91ac | |||
c83319d6f3 | |||
329b4cb0fb | |||
533f5992d1 | |||
cb103cc3e2 | |||
afdf89fb12 | |||
26a87b8eaa | |||
25d31463f4 | |||
2542c38c9b | |||
7326038424 | |||
12e632d221 | |||
3fc2108214 | |||
babbe39494 | |||
32b4585e39 | |||
43469a869c | |||
6f823e6478 | |||
e33693e20e | |||
ad3f091d4a | |||
3ff01d186d | |||
0cf8caa53b | |||
25920c208d | |||
19da96113f | |||
6e584e809e | |||
4185188a5b | |||
4273322ed5 | |||
ba0532c95d | |||
3a2fe597ba | |||
dda77952a0 | |||
d2e518d96b | |||
f3a97b2538 | |||
cacd017244 | |||
f5b15a5ef6 | |||
de620dca56 | |||
8decf4a3c9 | |||
831326952b | |||
27da578446 | |||
2c1cca168f | |||
e498fb784b | |||
2917719315 | |||
9ed90995e4 | |||
0f99bb5bdc | |||
1f4d4473e4 | |||
5332001ff4 | |||
22241fd7ad | |||
ddeade9775 | |||
f1cb165bdd | |||
9873353990 | |||
b879569b81 | |||
c3e821088b | |||
dc4f386e7a | |||
a40810b364 | |||
1690629717 | |||
9a6a71c8bc | |||
d626699f08 | |||
6aa60e685b | |||
9910762abc | |||
182fb430f1 | |||
bda20e2fbe | |||
bc586eceef | |||
128b27221a | |||
2dfb4eda9d | |||
fed6af4dfe | |||
c4ee6cd85c | |||
8fc307bd8d | |||
9e9ffcd586 | |||
49b064b5f2 | |||
ef6f5a4c23 | |||
e92562fd44 | |||
fe65ddb5f8 | |||
ffd405c6db | |||
9e41868e0d | |||
ca6accc889 | |||
dfe2e1562a | |||
ab43bb73d7 | |||
22b0b95e54 | |||
290ad8b592 | |||
d5519dbb55 | |||
4de9e059e9 | |||
e573aff6ae | |||
908e083dee | |||
ec29724997 | |||
88756e9807 | |||
80d8a0c4e3 | |||
7241f0c741 | |||
8565eb3fb8 | |||
87198f6e56 | |||
fa934da442 | |||
6c4dc711d2 | |||
1f2a755a97 | |||
a0e3dc163a | |||
810dc6ceb5 | |||
7203c742be | |||
2fd04cfb61 | |||
9c44d13f73 | |||
d6436a58a2 | |||
613167f3a6 | |||
ab0c281d98 | |||
c84dc281a2 | |||
835833a61b | |||
9af542ec89 | |||
06e33196bb | |||
36dd324139 | |||
52d4fe920c | |||
0d090d50d4 | |||
e57232edde | |||
dfe1e7ddd1 | |||
5ffc42c9db | |||
c63648a1b0 | |||
80fd4095c4 | |||
31d7a938f2 | |||
ee1b6868f8 | |||
7c7e5544e4 | |||
06fe26fbe7 | |||
c932c7d8f6 | |||
d56c536446 | |||
f5a9a28124 | |||
b86039536e | |||
59c4675e8a | |||
fbf6a329df | |||
ddec3118a4 | |||
94e2786297 | |||
ecfdff5454 | |||
c598a58ec9 | |||
3e38beb190 | |||
61a86e8e82 | |||
9c8e127fa0 | |||
eb23b40e5c | |||
9e19aafcd0 | |||
4cdf26b6f9 | |||
88de58cc22 | |||
f6b48c88b9 | |||
12534e57ad | |||
52e33bba2d | |||
d63e5f4e54 | |||
8d31866a0b | |||
7d818f217a | |||
7156665684 | |||
5045c5e8bf | |||
9de2ed9eaf | |||
096cf7a570 | |||
a04f07d149 | |||
63775b2866 | |||
e8609de7b4 | |||
e62aa89d72 | |||
77f12a940d | |||
0fe49e3d6e | |||
881386a123 | |||
7b9aec4ed0 | |||
cf18f9d06c | |||
052936f769 | |||
590de7a67b | |||
7f608ad8ad | |||
35b012b937 | |||
5892cf2ba7 | |||
c9d7fc1b6e |
@ -290,6 +290,25 @@
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marvinruder",
|
||||
"name": "Marvin A. Ruder",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
|
||||
"profile": "https://mruder.dev",
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
37
README.md
37
README.md
@ -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">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<p align="center">
|
||||
<i><a href="https://phasetwo.io/?utm_source=keycloakify"><strong>Keycloak as a Service</strong></a> - Keycloak community contributors of popular <a href="https://github.com/p2-inc#our-extensions-?utm_source=keycloakify">extensions</a> providing free and dedicated <a href="https://phasetwo.io/hosting/?utm_source=keycloakify">Keycloak hosting</a> and enterprise <a href="https://phasetwo.io/support/?utm_source=keycloakify">Keycloak support</a> to businesses of all sizes.</i>
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</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,8 @@ 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>
|
||||
<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>
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "11.2.10",
|
||||
"version": "11.3.16",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -38,12 +38,14 @@
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/",
|
||||
"dist/bin/**/*.d.ts",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/*.d.ts",
|
||||
"dist/bin/shared/*.js.map",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"dist/bin/shared/customHandler.js",
|
||||
"dist/bin/shared/customHandler.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
|
@ -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',`,
|
||||
` },`,
|
||||
|
@ -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 })
|
||||
}
|
||||
});
|
||||
|
@ -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`));
|
||||
})();
|
||||
|
27
scripts/tools/removeNodeModules.ts
Normal file
27
scripts/tools/removeNodeModules.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -13,17 +13,23 @@ import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
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: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -126,6 +132,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
|
||||
runFormat({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
|
||||
console.log(
|
||||
[
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
|
@ -1,13 +1,96 @@
|
||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
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: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
@ -20,17 +20,23 @@ import {
|
||||
} from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
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: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -238,18 +244,22 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
|
||||
|
||||
runFormat({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
|
||||
console.log(
|
||||
`${chalk.green("✓")} ${chalk.bold(
|
||||
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
|
||||
)} 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(
|
||||
|
@ -1,17 +1,24 @@
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import cliSelect from "cli-select";
|
||||
import child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import * as fs from "fs";
|
||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||
import { generateKcGenTs } from "../shared/generateKcGenTs";
|
||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||
|
||||
@ -97,7 +104,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||
|
||||
await generateKcGenTs({
|
||||
await updateKcGenCommand({
|
||||
buildContext: {
|
||||
...buildContext,
|
||||
implementedThemeTypes: {
|
||||
|
@ -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;
|
||||
})();
|
||||
|
||||
|
@ -1,15 +1,25 @@
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { SemVer } from "./tools/SemVer";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-email-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
@ -29,7 +39,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
let { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
@ -37,8 +47,32 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
buildContext
|
||||
});
|
||||
|
||||
const getUrl = (keycloakVersion: string) => {
|
||||
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
|
||||
};
|
||||
|
||||
keycloakVersion = await (async () => {
|
||||
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
|
||||
|
||||
while (true) {
|
||||
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
|
||||
|
||||
const response = await fetch(url, buildContext.fetchOptions);
|
||||
|
||||
if (response.ok) {
|
||||
break;
|
||||
}
|
||||
|
||||
assert(keycloakVersionParsed.patch !== 0);
|
||||
|
||||
keycloakVersionParsed.patch--;
|
||||
}
|
||||
|
||||
return SemVer.stringify(keycloakVersionParsed);
|
||||
})();
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
url: getUrl(keycloakVersion),
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
||||
|
@ -2,19 +2,16 @@ import { generateResources } from "./generateResources";
|
||||
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
|
||||
import { buildJars } from "./buildJars";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import chalk from "chalk";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import * as os from "os";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
exit_if_maven_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
@ -4,8 +4,9 @@ import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
|
||||
export type CliCommandOptions = {
|
||||
type CliCommandOptions = {
|
||||
projectDirPath: string | undefined;
|
||||
};
|
||||
|
||||
@ -69,10 +70,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./keycloakify");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -130,10 +131,13 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||
const { command } = await import("./start-keycloak");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -144,10 +148,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./eject-page");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -158,10 +162,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./add-story");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -172,10 +176,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./initialize-email-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -186,10 +190,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./initialize-account-theme");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -201,10 +205,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
@ -216,10 +220,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async cliCommandOptions => {
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./update-kc-gen");
|
||||
|
||||
await command({ cliCommandOptions });
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
@ -24,7 +23,7 @@ import { objectEntries } from "tsafe/objectEntries";
|
||||
import { type ThemeType } from "./constants";
|
||||
import { id } from "tsafe/id";
|
||||
import chalk from "chalk";
|
||||
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
|
||||
import { is } from "tsafe/is";
|
||||
|
||||
export type BuildContext = {
|
||||
@ -43,7 +42,7 @@ export type BuildContext = {
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
fetchOptions: ProxyFetchOptions;
|
||||
fetchOptions: FetchOptionsLike;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
@ -129,14 +128,12 @@ export type ResolvedViteConfig = {
|
||||
};
|
||||
|
||||
export function getBuildContext(params: {
|
||||
cliCommandOptions: CliCommandOptions;
|
||||
projectDirPath: string | undefined;
|
||||
}): BuildContext {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const projectDirPath =
|
||||
cliCommandOptions.projectDirPath !== undefined
|
||||
params.projectDirPath !== undefined
|
||||
? getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
pathIsh: params.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
})
|
||||
: process.cwd();
|
||||
@ -511,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") {
|
||||
@ -522,7 +528,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -566,7 +572,7 @@ export function getBuildContext(params: {
|
||||
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -595,7 +601,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -667,7 +673,7 @@ export function getBuildContext(params: {
|
||||
pathIsh:
|
||||
parsedPackageJson.keycloakify
|
||||
.staticDirPathInProjectBuildDirPath,
|
||||
cwd: projectBuildDirPath
|
||||
cwd: relativePathsCwd
|
||||
});
|
||||
}
|
||||
|
||||
@ -995,7 +1001,7 @@ export function getBuildContext(params: {
|
||||
type: "path",
|
||||
path: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: urlOrPath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -1005,7 +1011,7 @@ export function getBuildContext(params: {
|
||||
? undefined
|
||||
: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||
cwd: projectDirPath
|
||||
cwd: relativePathsCwd
|
||||
}),
|
||||
port: buildOptions.startKeycloakOptions?.port
|
||||
}
|
||||
|
@ -71,3 +71,8 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
|
||||
export const CONTAINER_NAME = "keycloak-keycloakify";
|
||||
|
||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||
|
||||
export const CUSTOM_HANDLER_ENV_NAMES = {
|
||||
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
|
||||
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
|
||||
};
|
||||
|
@ -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")
|
||||
);
|
||||
}
|
41
src/bin/shared/customHandler.ts
Normal file
41
src/bin/shared/customHandler.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||
|
||||
export const BIN_NAME = "_keycloakify-custom-handler";
|
||||
|
||||
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
|
||||
|
||||
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";
|
||||
|
||||
export function readParams(params: { apiVersion: ApiVersion }) {
|
||||
const { apiVersion } = params;
|
||||
|
||||
assert(apiVersion === "v1");
|
||||
|
||||
const commandName = (() => {
|
||||
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
|
||||
|
||||
assert(envValue !== undefined);
|
||||
|
||||
return envValue as CommandName;
|
||||
})();
|
||||
|
||||
const buildContext = (() => {
|
||||
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
|
||||
|
||||
assert(envValue !== undefined);
|
||||
|
||||
return JSON.parse(envValue) as BuildContext;
|
||||
})();
|
||||
|
||||
return { commandName, buildContext };
|
||||
}
|
73
src/bin/shared/customHandler_delegate.ts
Normal file
73
src/bin/shared/customHandler_delegate.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
|
||||
import {
|
||||
NOT_IMPLEMENTED_EXIT_CODE,
|
||||
type CommandName,
|
||||
BIN_NAME,
|
||||
ApiVersion
|
||||
} from "./customHandler";
|
||||
import * as child_process from "child_process";
|
||||
import { sep as pathSep } from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
assert<Equals<ApiVersion, "v1">>();
|
||||
|
||||
export function maybeDelegateCommandToCustomHandler(params: {
|
||||
commandName: CommandName;
|
||||
buildContext: BuildContext;
|
||||
}): { hasBeenHandled: boolean } {
|
||||
const { commandName, buildContext } = params;
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
try {
|
||||
child_process.execSync(`npx ${BIN_NAME}`, {
|
||||
stdio: "inherit",
|
||||
env: {
|
||||
...process.env,
|
||||
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
|
||||
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
const status = error.status;
|
||||
|
||||
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
|
||||
return { hasBeenHandled: false };
|
||||
}
|
||||
|
||||
process.exit(status);
|
||||
}
|
||||
|
||||
return { hasBeenHandled: true };
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import * as fs from "fs/promises";
|
||||
import { join as pathJoin } from "path";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { z } from "zod";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
themeNames: string[];
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
implementedThemeTypes: Pick<
|
||||
BuildContext["implementedThemeTypes"],
|
||||
"login" | "account"
|
||||
>;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function generateKcGenTs(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const isReactProject: boolean = await (async () => {
|
||||
const parsedPackageJson = await (async () => {
|
||||
type ParsedPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
return zParsedPackageJson.parse(
|
||||
JSON.parse(
|
||||
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||
)
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
{
|
||||
...parsedPackageJson.dependencies,
|
||||
...parsedPackageJson.devDependencies
|
||||
}.react !== undefined
|
||||
);
|
||||
})();
|
||||
|
||||
const filePath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
`kc.gen.ts${isReactProject ? "x" : ""}`
|
||||
);
|
||||
|
||||
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`,
|
||||
``,
|
||||
isReactProject && `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;`,
|
||||
` }`,
|
||||
`}`,
|
||||
``,
|
||||
...(!isReactProject
|
||||
? []
|
||||
: [
|
||||
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"
|
||||
);
|
||||
|
||||
if (currentContent !== undefined && currentContent.equals(newContent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await fs.writeFile(filePath, newContent);
|
||||
|
||||
delete_legacy_file: {
|
||||
if (!isReactProject) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
||||
|
||||
if (!(await existsAsync(legacyFilePath))) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
await fs.unlink(legacyFilePath);
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||
import { CONTAINER_NAME } from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
@ -29,13 +28,14 @@ import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
|
||||
export type CliCommandOptions = CliCommandOptions_common & {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
cliCommandOptions: {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
}) {
|
||||
exit_if_docker_not_installed: {
|
||||
let commandOutput: string | undefined = undefined;
|
||||
|
||||
@ -88,9 +88,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
const { cliCommandOptions, buildContext } = params;
|
||||
|
||||
const { dockerImageTag } = await (async () => {
|
||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||
@ -398,12 +396,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
...(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`]
|
||||
@ -426,7 +424,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
}))
|
||||
.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] }))
|
||||
|
@ -1,16 +1,18 @@
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
export type FetchOptionsLike = {
|
||||
proxy: string | undefined;
|
||||
noProxy: string | string[];
|
||||
strictSSL: boolean;
|
||||
cert: string | string[] | undefined;
|
||||
ca: string[] | undefined;
|
||||
};
|
||||
|
||||
export function getProxyFetchOptions(params: {
|
||||
npmConfigGetCwd: string;
|
||||
}): ProxyFetchOptions {
|
||||
}): FetchOptionsLike {
|
||||
const { npmConfigGetCwd } = params;
|
||||
|
||||
const cfg = (() => {
|
||||
|
71
src/bin/tools/runFormat.ts
Normal file
71
src/bin/tools/runFormat.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -1,13 +1,135 @@
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import { generateKcGenTs } from "./shared/generateKcGenTs";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
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: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
||||
commandName: "update-kc-gen",
|
||||
buildContext
|
||||
});
|
||||
|
||||
await generateKcGenTs({ buildContext });
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
|
||||
|
||||
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
if (!(await existsAsync(legacyFilePath))) {
|
||||
break delete_legacy_file;
|
||||
}
|
||||
|
||||
await fs.unlink(legacyFilePath);
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +94,7 @@ export declare namespace KcContext {
|
||||
languageTag: string;
|
||||
}[];
|
||||
currentLanguageTag: string;
|
||||
rtl?: boolean;
|
||||
};
|
||||
auth?: {
|
||||
showUsername?: boolean;
|
||||
@ -101,7 +102,7 @@ export declare namespace KcContext {
|
||||
showTryAnotherWayLink?: boolean;
|
||||
attemptedUsername?: string;
|
||||
};
|
||||
scripts: string[];
|
||||
scripts?: string[];
|
||||
message?: {
|
||||
type: "success" | "warning" | "error" | "info";
|
||||
summary: string;
|
||||
|
@ -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: `
|
||||
|
@ -16,9 +16,13 @@ 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 }[];
|
||||
rtl?: boolean;
|
||||
};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
@ -90,11 +94,56 @@ 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 } = kcContextLocale ?? {};
|
||||
|
||||
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) => {
|
||||
@ -110,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;
|
||||
@ -136,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,
|
||||
@ -147,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({
|
||||
|
@ -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 });
|
||||
|
1559
src/login/lib/getUserProfileApi/getUserProfileApi.ts
Normal file
1559
src/login/lib/getUserProfileApi/getUserProfileApi.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
src/login/lib/getUserProfileApi/index.ts
Normal file
1
src/login/lib/getUserProfileApi/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./getUserProfileApi";
|
109
src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
Normal file
109
src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
Normal 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
@ -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>
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
@ -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]);
|
||||
}
|
||||
|
30
src/tools/waitForElementMountedOnDom.ts
Normal file
30
src/tools/waitForElementMountedOnDom.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
@ -15,7 +15,8 @@ import {
|
||||
type ResolvedViteConfig
|
||||
} from "../bin/shared/buildContext";
|
||||
import MagicString from "magic-string";
|
||||
import { generateKcGenTs } from "../bin/shared/generateKcGenTs";
|
||||
import { command as updateKcGenCommand } from "../bin/update-kc-gen";
|
||||
import { replaceAll } from "../bin/tools/String.prototype.replaceAll";
|
||||
|
||||
export namespace keycloakify {
|
||||
export type Params = BuildOptions & {
|
||||
@ -122,15 +123,16 @@ export function keycloakify(params: keycloakify.Params) {
|
||||
}
|
||||
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions: {
|
||||
projectDirPath
|
||||
}
|
||||
projectDirPath
|
||||
});
|
||||
|
||||
copyKeycloakResourcesToPublic({ buildContext }),
|
||||
await generateKcGenTs({ buildContext });
|
||||
await copyKeycloakResourcesToPublicCommand({ buildContext });
|
||||
|
||||
await updateKcGenCommand({ buildContext });
|
||||
},
|
||||
transform: (code, id) => {
|
||||
id = replaceAll(id, "/", pathSep);
|
||||
|
||||
assert(command !== undefined);
|
||||
assert(shouldGenerateSourcemap !== undefined);
|
||||
|
||||
|
@ -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"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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>"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -16,3 +16,14 @@ type Story = StoryObj<typeof meta>;
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithoutRedirectUrl: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
logout: {
|
||||
clients: []
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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" }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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." }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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 }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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" }]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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,6 +27,81 @@ 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"
|
||||
},
|
||||
realm: {
|
||||
displayName: "MyRealm"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* 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={{
|
||||
...mockKcContext,
|
||||
idpAlias: "Facebook",
|
||||
brokerContext: {
|
||||
username: "jane.doe"
|
||||
},
|
||||
realm: {
|
||||
displayName: "CUSTOM REALM DISPLAY NAME"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* 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."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -115,7 +115,6 @@ export const WithFavoritePet: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export const WithNewsletter: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
@ -132,7 +131,7 @@ export const WithNewsletter: Story = {
|
||||
},
|
||||
annotations: {
|
||||
inputOptionLabels: {
|
||||
"yes": "I want my email inbox filled with spam"
|
||||
yes: "I want my email inbox filled with spam"
|
||||
},
|
||||
inputType: "multiselect-checkboxes"
|
||||
},
|
||||
@ -140,13 +139,12 @@ export const WithNewsletter: Story = {
|
||||
readOnly: false
|
||||
} satisfies Attribute
|
||||
}
|
||||
},
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
@ -217,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" }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -45,3 +45,44 @@ export const French: Story = {
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const Spanish: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: {
|
||||
currentLanguageTag: "es"
|
||||
},
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
termsText: "<p>Mis términos en <strong>Español</strong></p>"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
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>
|
||||
`
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
Reference in New Issue
Block a user