Compare commits

...

77 Commits

Author SHA1 Message Date
ec74ceef4d Implement hot module replacement for developing Account SPA and Admin UI 2024-11-30 23:55:24 +01:00
fd3261cdf1 Bump version 2024-11-25 11:41:36 +01:00
b4b53d2552 Re export wide type def of the kcContext 2024-11-25 11:41:17 +01:00
0371d9ea7a Bump version 2024-11-23 08:49:17 +01:00
73031e74ec Make it so it's not required to manually call the copy-keycloak-resources-to-public script even in webpack projects 2024-11-23 08:48:06 +01:00
f71ab4635f Bump version 2024-11-22 06:16:42 +01:00
983db6780a #730 2024-11-22 06:16:15 +01:00
ea22107b9b Bump version 2024-11-21 07:12:53 +01:00
8e4a7fed9e Fix invalid dom nesting errors 2024-11-21 07:12:23 +01:00
30efd8fcf4 Bump version 2024-11-21 06:27:09 +01:00
f4c4e92ca1 #726 2024-11-21 06:26:54 +01:00
cfda99f5b0 Bump version 2024-11-19 03:49:32 +01:00
5063b1c7ab Rename LoginDeviceVerifyUserCode to LoginOauth2DeviceVerifyUserCode (as it should have been) 2024-11-19 03:49:01 +01:00
955b6cac45 Remove noisy stories 2024-11-19 03:22:49 +01:00
1fa3d6133c Remove unused template prop 2024-11-19 03:22:35 +01:00
023939a064 Bump version 2024-11-18 21:38:59 +01:00
de4490cf0f Update tsafe 2024-11-18 21:38:27 +01:00
e0cda43724 Change pattern for telling if linked 2024-11-18 21:34:10 +01:00
d4ac67dba8 Bump version 2024-11-18 18:44:57 +01:00
23f4b59559 Merge pull request #725 from keycloakify/all-contributors/add-tripheo0412
docs: add tripheo0412 as a contributor for doc
2024-11-18 17:44:35 +00:00
4a7ba4a1c9 docs: update .all-contributorsrc [skip ci] 2024-11-18 17:27:30 +00:00
a3e765e1fc docs: update README.md [skip ci] 2024-11-18 17:27:29 +00:00
ee3614dbf1 Merge pull request #723 from keycloakify/all-contributors/add-zvn2060
docs: add zvn2060 as a contributor for code
2024-11-18 17:24:28 +00:00
8099ec1ffe docs: update .all-contributorsrc [skip ci] 2024-11-18 17:24:15 +00:00
2e2b0ab3ae docs: update README.md [skip ci] 2024-11-18 17:24:14 +00:00
a1f15f2f6b Merge pull request #722 from zvn2060/main
Fix #721: mismatched LoginPasskeysConditionalAuthenticate
2024-11-18 17:23:15 +00:00
8fe74fe7ee Fix #721: mismatched LoginPasskeysConditionalAuthenticate 2024-11-18 13:59:44 +08:00
232be50225 Bump version 2024-11-18 04:46:58 +01:00
a00bb0c4db Fix logical error 2024-11-18 04:46:29 +01:00
5517d6baf4 Make runPrettier work when project is linked 2024-11-18 04:46:06 +01:00
e2975503a4 Automatically untrack files implemented by ui modules 2024-11-18 03:47:57 +01:00
1231c92198 Update comment 2024-11-18 03:19:13 +01:00
64ca0bc0ca Bump version 2024-11-18 01:09:46 +01:00
2bceb9385c Fix: creating directory 2024-11-18 00:26:48 +01:00
954bc43c22 Bump version 2024-11-17 23:51:02 +01:00
8c99aa4b9d gererate diffrent comment depending on the file type 2024-11-17 23:50:49 +01:00
884195d30d Bump version 2024-11-17 23:21:44 +01:00
17dd726158 Fix passing wrong path to npmInstall 2024-11-17 23:20:24 +01:00
7ee30b6a42 Fix managed gitignore 2024-11-17 23:13:45 +01:00
397f8133bf Fix comment formatting 2024-11-17 23:08:52 +01:00
c9abc6dc5c Fix crawl async 2024-11-17 23:05:41 +01:00
e78eafd1f1 Fix 2024-11-17 20:53:38 +01:00
e50f2bd692 Bump version 2024-11-17 19:35:37 +01:00
ed0428bd55 Remove runFormat 2024-11-17 19:34:57 +01:00
2a126d65c5 Merge pull request #717 from keycloakify/admin_theme_support
Run prettier of a file per file basis (and prepare for extension support)
2024-11-17 18:34:17 +00:00
30149ff1f2 Remove ignored file that where removed 2024-11-17 19:33:44 +01:00
32f471624a Remove ignored file that where removed 2024-11-17 19:31:29 +01:00
7f5eabb639 Format page when ejecting for the account 2024-11-17 19:25:53 +01:00
32fb1e2f71 Fix runPrettier script 2024-11-17 19:22:34 +01:00
7c3c6d3643 Fix misnamed kc.gen 2024-11-17 16:46:25 +01:00
b6d2154d56 Fix usage of deprecated node api 2024-11-17 16:45:14 +01:00
b8d4daf4c1 Temporarely restore runFormat (for merge conflicts) 2024-11-17 03:38:38 +01:00
c03623875a The admin theme does not support traditional eject 2024-11-17 03:35:01 +01:00
c423e4cacc Adapt the npmInstall script so that it works when packages are linked 2024-11-17 03:24:41 +01:00
c593f5cb97 Re-sync version with main 2024-11-16 21:38:31 +01:00
2ad36a8137 Fix runPrettier 2024-11-16 21:38:31 +01:00
bccaddc2de Bump version 2024-11-13 14:38:48 +01:00
97c12c8a12 Merge pull request #720 from sukvvon/fix/add-versatility-in-runFormat
fix(runFormat.ts): improve filtering in scriptNames
2024-11-13 13:38:25 +00:00
b349a819ba fix(runFormat.ts): improve filtering in scriptNames 2024-11-13 21:49:55 +09:00
792d4262c8 Bump version 2024-11-10 14:30:00 +01:00
37a9046a40 Merge pull request #718 from kathari00/fix_storyname
fix story
2024-11-10 13:29:39 +00:00
5ad29d9f43 fix story 2024-11-10 10:33:21 +00:00
645031543e Merge branch 'main' into admin_theme_support 2024-11-10 09:39:26 +00:00
b43c02f279 Release candidate 2024-11-10 10:35:19 +01:00
63877d53be Eject file command implementation 2024-11-09 20:33:53 +01:00
79a580b4a5 Bump version 2024-11-09 19:50:48 +01:00
994f1f8d3d #714 #713 2024-11-09 19:50:29 +01:00
a73281d46d Checkpoint 2024-11-09 14:02:19 +01:00
a60a0d0696 checkpoint 2024-11-09 09:35:41 +01:00
a2ea81b3b8 Bump version 2024-11-06 10:10:55 +01:00
a0461e3ef0 #711 2024-11-06 10:10:27 +01:00
93fcf96cde checkpoint 2024-11-03 01:56:41 +01:00
d7455fd100 Only format kc-gen file 2024-11-03 00:25:28 +01:00
af7a45d125 checkpoint 2024-11-02 22:39:03 +01:00
db37320280 up 2024-10-27 00:10:39 +02:00
b0aa0feab5 up 2024-10-26 22:06:37 +02:00
0e93d4ed09 Implement admin theme support (checkpoint) 2024-10-26 21:23:18 +02:00
60 changed files with 2671 additions and 538 deletions

View File

@ -309,6 +309,24 @@
"contributions": [
"bug"
]
},
{
"login": "zvn2060",
"name": "HI_OuO",
"avatar_url": "https://avatars.githubusercontent.com/u/45450852?v=4",
"profile": "https://github.com/zvn2060",
"contributions": [
"code"
]
},
{
"login": "tripheo0412",
"name": "Tri Hoang",
"avatar_url": "https://avatars.githubusercontent.com/u/25382052?v=4",
"profile": "https://github.com/tripheo0412",
"contributions": [
"doc"
]
}
],
"contributorsPerLine": 7,

View File

@ -165,6 +165,8 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/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>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "11.3.15",
"version": "11.3.32",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
@ -64,7 +64,7 @@
],
"homepage": "https://www.keycloakify.dev",
"dependencies": {
"tsafe": "^1.7.5"
"tsafe": "^1.8.5"
},
"devDependencies": {
"@babel/core": "^7.24.5",
@ -95,14 +95,14 @@
"cli-select": "^1.1.2",
"dompurify": "^3.1.6",
"eslint-plugin-storybook": "^0.6.7",
"evt": "^2.5.7",
"evt": "^2.5.8",
"html-entities": "^2.5.2",
"husky": "^4.3.8",
"isomorphic-dompurify": "^2.15.0",
"lint-staged": "^11.0.0",
"magic-string": "^0.30.7",
"make-fetch-happen": "^11.0.3",
"powerhooks": "^1.0.10",
"powerhooks": "^1.0.19",
"prettier": "^3.2.5",
"properties-parser": "^0.3.1",
"react": "^18.2.0",

View File

@ -40,7 +40,9 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
);
}
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
run(
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
);
transformCodebase({
srcDirPath: join("dist", "ncc_out"),
@ -113,7 +115,7 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
}
run(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join(
"dist",
"ncc_out"
)}`

View File

@ -4,8 +4,7 @@ import { SemVer } from "../src/bin/tools/SemVer";
import { join as pathJoin, relative as pathRelative } from "path";
import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { assert, is } from "tsafe/assert";
import { run } from "./shared/run";
(async () => {

View File

@ -37,7 +37,7 @@ async function generateI18nMessages() {
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
for (const themeType of THEME_TYPES) {
for (const themeType of THEME_TYPES.filter(themeType => themeType !== "admin")) {
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersionId: (() => {
switch (themeType) {

View File

@ -125,7 +125,54 @@ if (testAppPaths.length === 0) {
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
testAppPaths.forEach(testAppPath => {
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
scripts?: Record<string, string>;
};
let hasPostInstallOrPrepareScript = false;
if (parsedPackageJson.scripts !== undefined) {
for (const scriptName of ["postinstall", "prepare"]) {
if (parsedPackageJson.scripts[scriptName] === undefined) {
continue;
}
hasPostInstallOrPrepareScript = true;
delete parsedPackageJson.scripts[scriptName];
}
}
if (hasPostInstallOrPrepareScript) {
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
);
}
const restorePackageJson = () => {
if (!hasPostInstallOrPrepareScript) {
return;
}
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
};
try {
execSync("yarn install", { cwd: testAppPath });
} catch (error) {
restorePackageJson();
throw error;
}
restorePackageJson();
});
console.log("=== Linking common dependencies ===");
@ -172,4 +219,20 @@ testAppPaths.forEach(testAppPath =>
})
);
testAppPaths.forEach(testAppPath => {
const { scripts = {} } = JSON.parse(
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
) as {
scripts?: Record<string, string>;
};
for (const scriptName of ["postinstall", "prepare"]) {
if (scripts[scriptName] === undefined) {
continue;
}
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
}
});
export {};

View File

@ -5,8 +5,7 @@ import {
ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId,
type AccountThemePageId,
THEME_TYPES,
type ThemeType
THEME_TYPES
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
@ -15,7 +14,7 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { runFormat } from "./tools/runFormat";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
@ -39,6 +38,8 @@ export async function command(params: { buildContext: BuildContext }) {
return buildContext.implementedThemeTypes.account.isImplemented;
case "login":
return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
}
assert<Equals<typeof themeType, never>>(false);
});
@ -49,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) {
return values[0];
}
const { value } = await cliSelect<ThemeType>({
const { value } = await cliSelect({
values
}).catch(() => {
process.exit(-1);
@ -68,6 +69,16 @@ export async function command(params: { buildContext: BuildContext }) {
);
process.exit(0);
return;
}
if (themeType === "admin") {
console.log(
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
);
process.exit(0);
return;
}
console.log(`${themeType}`);
@ -108,7 +119,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
}
const componentCode = fs
let sourceCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
@ -122,6 +133,17 @@ export async function command(params: { buildContext: BuildContext }) {
.replace('import React from "react";\n', "")
.replace(/from "[./]+dist\//, 'from "keycloakify/');
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetFilePath,
sourceCode: sourceCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
@ -130,11 +152,7 @@ export async function command(params: { buildContext: BuildContext }) {
}
}
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8"));
console.log(
[

68
src/bin/eject-file.ts Normal file
View File

@ -0,0 +1,68 @@
import type { BuildContext } from "./shared/buildContext";
import { getUiModuleFileSourceCodeReadyToBeCopied } from "./postinstall/getUiModuleFileSourceCodeReadyToBeCopied";
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
import { getUiModuleMetas } from "./postinstall/uiModuleMeta";
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
import * as fsPr from "fs/promises";
import {
readManagedGitignoreFile,
writeManagedGitignoreFile
} from "./postinstall/managedGitignoreFile";
export async function command(params: {
buildContext: BuildContext;
cliCommandOptions: {
file: string;
};
}) {
const { buildContext, cliCommandOptions } = params;
const fileRelativePath = pathRelative(
buildContext.themeSrcDirPath,
getAbsoluteAndInOsFormatPath({
cwd: buildContext.themeSrcDirPath,
pathIsh: cliCommandOptions.file
})
);
const uiModuleMetas = await getUiModuleMetas({ buildContext });
const uiModuleMeta = uiModuleMetas.find(({ files }) =>
files.map(({ fileRelativePath }) => fileRelativePath).includes(fileRelativePath)
);
if (!uiModuleMeta) {
throw new Error(`No UI module found for the file ${fileRelativePath}`);
}
const uiModuleDirPath = await getInstalledModuleDirPath({
moduleName: uiModuleMeta.moduleName,
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath),
projectDirPath: buildContext.projectDirPath
});
const sourceCode = await getUiModuleFileSourceCodeReadyToBeCopied({
buildContext,
fileRelativePath,
isForEjection: true,
uiModuleName: uiModuleMeta.moduleName,
uiModuleDirPath,
uiModuleVersion: uiModuleMeta.version
});
await fsPr.writeFile(
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
sourceCode
);
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
buildContext
});
await writeManagedGitignoreFile({
buildContext,
uiModuleMetas,
ejectedFilesRelativePaths: [...ejectedFilesRelativePaths, fileRelativePath]
});
}

View File

@ -7,8 +7,7 @@ import {
ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId,
type AccountThemePageId,
THEME_TYPES,
type ThemeType
THEME_TYPES
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
@ -23,7 +22,7 @@ import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runFormat } from "./tools/runFormat";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -46,6 +45,8 @@ export async function command(params: { buildContext: BuildContext }) {
return buildContext.implementedThemeTypes.account.isImplemented;
case "login":
return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
}
assert<Equals<typeof themeType, never>>(false);
});
@ -56,7 +57,7 @@ export async function command(params: { buildContext: BuildContext }) {
return values[0];
}
const { value } = await cliSelect<ThemeType>({
const { value } = await cliSelect({
values
}).catch(() => {
process.exit(-1);
@ -65,6 +66,14 @@ export async function command(params: { buildContext: BuildContext }) {
return value;
})();
if (themeType === "admin") {
console.log(
"Use `npx keycloakify eject-file` command instead, see documentation"
);
process.exit(-1);
}
if (
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
@ -74,13 +83,13 @@ export async function command(params: { buildContext: BuildContext }) {
pathDirname(buildContext.packageJsonFilePath),
"node_modules",
"@keycloakify",
"keycloak-account-ui",
`keycloak-account-ui`,
"src"
);
console.log(
[
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
`There isn't an interactive CLI to eject components of the Account SPA UI.`,
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
``,
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
@ -89,41 +98,44 @@ export async function command(params: { buildContext: BuildContext }) {
);
eject_entrypoint: {
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const;
const accountThemeSrcDirPath = pathJoin(
buildContext.themeSrcDirPath,
"account"
);
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
const targetFilePath = pathJoin(
accountThemeSrcDirPath,
kcAccountUiTsxFileRelativePath
);
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath);
if (fs.existsSync(targetFilePath)) {
break eject_entrypoint;
}
fs.cpSync(
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
targetFilePath
);
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath);
{
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx");
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
const componentName = pathBasename(
kcAccountUiTsxFileRelativePath
).replace(/.tsx$/, "");
const componentName = pathBasename(kcUiTsxFileRelativePath).replace(
/.tsx$/,
""
);
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
let modifiedKcPageTsxCode = kcPageTsxCode.replace(
`@keycloakify/keycloak-account-ui/${componentName}`,
`./${componentName}`
);
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
modifiedKcPageTsxCode = await runPrettier({
filePath: kcPageTsxFilePath,
sourceCode: modifiedKcPageTsxCode
});
}
fs.writeFileSync(
kcPageTsxFilePath,
Buffer.from(modifiedKcPageTsxCode, "utf8")
@ -139,13 +151,14 @@ export async function command(params: { buildContext: BuildContext }) {
[
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``,
`then update the import of routes in ${kcUiTsxFileRelativePath}.`
].join("\n")
);
}
process.exit(0);
return;
}
console.log(`${themeType}`);
@ -222,7 +235,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
}
const componentCode = fs
let componentCode = fs
.readFileSync(
pathJoin(
getThisCodebaseRootDirPath(),
@ -234,6 +247,17 @@ export async function command(params: { buildContext: BuildContext }) {
)
.toString("utf8");
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
componentCode = await runPrettier({
filePath: targetFilePath,
sourceCode: componentCode
});
}
{
const targetDirPath = pathDirname(targetFilePath);
@ -244,10 +268,6 @@ export async function command(params: { buildContext: BuildContext }) {
fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
runFormat({
packageJsonFilePath: buildContext.packageJsonFilePath
});
console.log(
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))

View File

@ -1,12 +1,12 @@
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 { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -38,37 +38,9 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
}
exit_if_uncommitted_changes: {
let hasUncommittedChanges: boolean | undefined = undefined;
try {
hasUncommittedChanges =
child_process
.execSync(`git status --porcelain`, {
cwd: buildContext.projectDirPath
})
.toString()
.trim() !== "";
} catch {
// Probably not a git repository
break exit_if_uncommitted_changes;
}
if (!hasUncommittedChanges) {
break exit_if_uncommitted_changes;
}
console.warn(
[
chalk.red(
"Please commit or stash your changes before running this command.\n"
),
"This command will modify your project's files so it's better to have a clean working directory",
"so that you can easily see what has been changed and revert if needed."
].join(" ")
);
process.exit(-1);
}
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
const { value: accountThemeType } = await cliSelect({
values: ["Single-Page" as const, "Multi-Page" as const]

View File

@ -1,4 +1,4 @@
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { relative as pathRelative, dirname as pathDirname } from "path";
import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs";
import chalk from "chalk";
@ -9,12 +9,10 @@ import {
import { SemVer } from "../tools/SemVer";
import fetch from "make-fetch-happen";
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is";
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import { npmInstall } from "../tools/npmInstall";
import { copyBoilerplate } from "./copyBoilerplate";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
fetchOptions: BuildContext["fetchOptions"];
@ -121,20 +119,7 @@ export async function initializeAccountTheme_singlePage(params: {
JSON.stringify(parsedPackageJson, undefined, 4)
);
run_npm_install: {
if (
JSON.parse(
fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
.toString("utf8")
)["version"] === "0.0.0"
) {
//NOTE: Linked version
break run_npm_install;
}
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
}
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
copyBoilerplate({
accountThemeType: "Single-Page",

View File

@ -1,11 +1,10 @@
import { join as pathJoin } from "path";
import { assert, type Equals } from "tsafe/assert";
import { assert, type Equals, is } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
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"];

View File

@ -84,8 +84,47 @@ attributes_to_attributesByName: {
kcContext.profile.attributesByName[attribute.name] = attribute;
});
}
redirect_to_dev_server: {
switch(kcContext.themeType){
case "login":
break redirect_to_dev_server;
case "account":
if( kcContext.pageId !== "index.ftl" ){
break redirect_to_dev_server;
}
break;
case "admin":
break;
default:
break redirect_to_dev_server;
}
const devSeverPort = kcContext.properties.KEYCLOAKIFY_SPA_DEV_SERVER_PORT;
if( !devSeverPort ){
break redirect_to_dev_server;
}
const redirectUrl = new URL(window.location.href);
redirectUrl.port = devSeverPort;
delete kcContext.msgJSON;
console.log(kcContext);
redirectUrl.searchParams.set("kcContext", encodeURIComponent(JSON.stringify(kcContext)) );
window.location.href = redirectUrl.toString();
}
window.kcContext = kcContext;
<#if xKeycloakify.themeType == "login" >
{
const script = document.createElement("script");

View File

@ -22,7 +22,7 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export function generateMessageProperties(params: {
buildContext: BuildContextLike;
themeType: ThemeType;
themeType: Exclude<ThemeType, "admin">;
}): {
languageTags: string[];
writeMessagePropertiesFiles: (params: {

View File

@ -19,7 +19,9 @@ import {
type ThemeType,
LOGIN_THEME_PAGE_IDS,
ACCOUNT_THEME_PAGE_IDS,
WELL_KNOWN_DIRECTORY_BASE_NAME
WELL_KNOWN_DIRECTORY_BASE_NAME,
THEME_TYPES,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
} from "../../shared/constants";
import { assert, type Equals } from "tsafe/assert";
import { readFieldNameUsage } from "./readFieldNameUsage";
@ -78,15 +80,29 @@ export async function generateResources(params: {
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
> = {};
for (const themeType of ["login", "account"] as const) {
for (const themeType of THEME_TYPES) {
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
continue;
}
const isForAccountSpa =
themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page");
const getAccountThemeType = () => {
assert(themeType === "account");
assert(buildContext.implementedThemeTypes.account.isImplemented);
return buildContext.implementedThemeTypes.account.type;
};
const isSpa = (() => {
switch (themeType) {
case "login":
return false;
case "account":
return getAccountThemeType() === "Single-Page";
case "admin":
return true;
}
})();
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
@ -101,7 +117,7 @@ export async function generateResources(params: {
rmSync(destDirPath, { recursive: true, force: true });
if (
themeType === "account" &&
themeType !== "login" &&
buildContext.implementedThemeTypes.login.isImplemented
) {
// NOTE: We prevent doing it twice, it has been done for the login theme.
@ -182,10 +198,13 @@ export async function generateResources(params: {
buildContext,
keycloakifyVersion: readThisNpmPackageVersion(),
themeType,
fieldNames: readFieldNameUsage({
themeSrcDirPath: buildContext.themeSrcDirPath,
themeType
})
fieldNames: isSpa
? []
: (assert(themeType !== "admin"),
readFieldNameUsage({
themeSrcDirPath: buildContext.themeSrcDirPath,
themeType
}))
});
[
@ -194,10 +213,14 @@ export async function generateResources(params: {
case "login":
return LOGIN_THEME_PAGE_IDS;
case "account":
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
return getAccountThemeType() === "Single-Page"
? ["index.ftl"]
: ACCOUNT_THEME_PAGE_IDS;
case "admin":
return ["index.ftl"];
}
})(),
...(isForAccountSpa
...(isSpa
? []
: readExtraPagesNames({
themeType,
@ -215,10 +238,12 @@ export async function generateResources(params: {
let languageTags: string[] | undefined = undefined;
i18n_messages_generation: {
if (isForAccountSpa) {
if (isSpa) {
break i18n_messages_generation;
}
assert(themeType !== "admin");
const wrap = generateMessageProperties({
buildContext,
themeType
@ -231,16 +256,15 @@ export async function generateResources(params: {
writeMessagePropertiesFiles;
}
bring_in_account_v3_i18n_messages: {
if (!buildContext.implementedThemeTypes.account.isImplemented) {
break bring_in_account_v3_i18n_messages;
}
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
break bring_in_account_v3_i18n_messages;
bring_in_spas_messages: {
if (!isSpa) {
break bring_in_spas_messages;
}
assert(themeType !== "login");
const accountUiDirPath = child_process
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
cwd: pathDirname(buildContext.packageJsonFilePath)
})
.toString("utf8")
@ -255,7 +279,7 @@ export async function generateResources(params: {
}
const messagesDirPath_dest = pathJoin(
getThemeTypeDirPath({ themeName, themeType: "account" }),
getThemeTypeDirPath({ themeName, themeType }),
"messages"
);
@ -267,7 +291,7 @@ export async function generateResources(params: {
apply_theme_changes: {
const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath,
"account",
themeType,
"messages"
);
@ -316,7 +340,7 @@ export async function generateResources(params: {
}
keycloak_static_resources: {
if (isForAccountSpa) {
if (isSpa) {
break keycloak_static_resources;
}
@ -339,15 +363,27 @@ export async function generateResources(params: {
`parent=${(() => {
switch (themeType) {
case "account":
return isForAccountSpa ? "base" : "account-v1";
switch (getAccountThemeType()) {
case "Multi-Page":
return "account-v1";
case "Single-Page":
return "base";
}
case "login":
return "keycloak";
case "admin":
return "base";
}
assert<Equals<typeof themeType, never>>(false);
})()}`,
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
...(themeType === "account" && getAccountThemeType() === "Single-Page"
? ["deprecatedMode=false"]
: []),
...(buildContext.extraThemeProperties ?? []),
...buildContext.environmentVariables.map(
...[
...buildContext.environmentVariables,
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
].map(
({ name, default: defaultValue }) =>
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
),

View File

@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
/** Assumes the theme type exists */
export function readFieldNameUsage(params: {
themeSrcDirPath: string;
themeType: ThemeType;
themeType: Exclude<ThemeType, "admin">;
}): string[] {
const { themeSrcDirPath, themeType } = params;

View File

@ -5,6 +5,9 @@ import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
import { getBuildContext } from "./shared/buildContext";
import { SemVer } from "./tools/SemVer";
import { assert, is } from "tsafe/assert";
import chalk from "chalk";
type CliCommandOptions = {
projectDirPath: string | undefined;
@ -80,7 +83,7 @@ program
program
.command<{
port: number | undefined;
keycloakVersion: string | undefined;
keycloakVersion: string | number | undefined;
realmJsonFilePath: string | undefined;
}>({
name: "start-keycloak",
@ -134,9 +137,50 @@ program
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
const { command } = await import("./start-keycloak");
validate_keycloak_version: {
if (keycloakVersion === undefined) {
break validate_keycloak_version;
}
const isValidVersion = (() => {
if (typeof keycloakVersion === "number") {
return false;
}
try {
SemVer.parse(keycloakVersion);
} catch {
return false;
}
return;
})();
if (isValidVersion) {
break validate_keycloak_version;
}
console.log(
chalk.red(
[
`Invalid Keycloak version: ${keycloakVersion}`,
"It should be a valid semver version example: 26.0.4"
].join(" ")
)
);
process.exit(1);
}
assert(is<string | undefined>(keycloakVersion));
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
cliCommandOptions: {
keycloakVersion,
port,
realmJsonFilePath
}
});
}
});
@ -201,7 +245,7 @@ program
.command({
name: "copy-keycloak-resources-to-public",
description:
"(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
"(Internal) Copy Keycloak default theme resources to the public directory."
})
.task({
skip,
@ -227,6 +271,56 @@ program
}
});
program
.command({
name: "postinstall",
description: "Initialize all the Keycloakify UI modules installed in the project."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./postinstall");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
program
.command<{
file: string;
}>({
name: "eject-file",
description: [
"WARNING: Not usable yet, will be used for future features",
"Take ownership over a given file"
].join(" ")
})
.option({
key: "file",
name: (() => {
const name = "file";
optionsKeys.push(name);
return name;
})(),
description: [
"Relative path of the file relative to the directory of your keycloak theme source",
"Example `--file src/login/page/Login.tsx`"
].join(" ")
})
.task({
skip,
handler: async ({ projectDirPath, file }) => {
const { command } = await import("./eject-file");
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { file }
});
}
});
// Fallback to build command if no command is provided
{
const [, , ...rest] = process.argv;

View File

@ -0,0 +1,82 @@
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
import * as fsPr from "fs/promises";
import { join as pathJoin, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import { KEYCLOAK_THEME } from "../shared/constants";
export type BuildContextLike = {
themeSrcDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getUiModuleFileSourceCodeReadyToBeCopied(params: {
buildContext: BuildContextLike;
fileRelativePath: string;
isForEjection: boolean;
uiModuleDirPath: string;
uiModuleName: string;
uiModuleVersion: string;
}): Promise<Buffer> {
const {
buildContext,
uiModuleDirPath,
fileRelativePath,
isForEjection,
uiModuleName,
uiModuleVersion
} = params;
let sourceCode = (
await fsPr.readFile(pathJoin(uiModuleDirPath, KEYCLOAK_THEME, fileRelativePath))
).toString("utf8");
const toComment = (lines: string[]) => {
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
if (!fileRelativePath.endsWith(ext)) {
continue;
}
return [`/**`, ...lines.map(line => ` * ${line}`), ` */`].join("\n");
}
if (fileRelativePath.endsWith(".html")) {
return [`<!--`, ...lines.map(line => ` ${line}`), `-->`].join("\n");
}
return undefined;
};
const comment = toComment(
isForEjection
? [`This file was ejected from ${uiModuleName} version ${uiModuleVersion}.`]
: [
`WARNING: Before modifying this file run the following command:`,
``,
`$ npx keycloakify eject-file --file ${fileRelativePath.split(pathSep).join("/")}`,
``,
`This file comes from ${uiModuleName} version ${uiModuleVersion}.`,
`This file has been copied over to your repo by your postinstall script: \`npx keycloakify postinstall\``
]
);
if (comment !== undefined) {
sourceCode = [comment, ``, sourceCode].join("\n");
}
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
sourceCode = await runPrettier({
filePath: destFilePath,
sourceCode
});
}
return Buffer.from(sourceCode, "utf8");
}

View File

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

View File

@ -0,0 +1,157 @@
import { assert, type Equals, is } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta";
import { z } from "zod";
import { id } from "tsafe/id";
import * as fsPr from "fs/promises";
import { SemVer } from "../tools/SemVer";
import { same } from "evt/tools/inDepth/same";
import { runPrettier, getIsPrettierAvailable } from "../tools/runPrettier";
import { npmInstall } from "../tools/npmInstall";
import { dirname as pathDirname } from "path";
export type BuildContextLike = {
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export type UiModuleMetaLike = {
moduleName: string;
peerDependencies: Record<string, string>;
};
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
export async function installUiModulesPeerDependencies(params: {
buildContext: BuildContextLike;
uiModuleMetas: UiModuleMetaLike[];
}): Promise<void | never> {
const { buildContext, uiModuleMetas } = params;
const { uiModulesPerDependencies } = (() => {
const uiModulesPerDependencies: Record<string, string> = {};
for (const { peerDependencies } of uiModuleMetas) {
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
peerDependencies
)) {
const versionRange = (() => {
const versionRange_current =
uiModulesPerDependencies[peerDependencyName];
if (versionRange_current === undefined) {
return versionRange_candidate;
}
if (versionRange_current === "*") {
return versionRange_candidate;
}
if (versionRange_candidate === "*") {
return versionRange_current;
}
const { versionRange } = [
versionRange_current,
versionRange_candidate
]
.map(versionRange => ({
versionRange,
semVer: SemVer.parse(
(() => {
if (
versionRange.startsWith("^") ||
versionRange.startsWith("~")
) {
return versionRange.slice(1);
}
return versionRange;
})()
)
}))
.sort((a, b) => SemVer.compare(b.semVer, a.semVer))[0];
return versionRange;
})();
uiModulesPerDependencies[peerDependencyName] = versionRange;
}
}
return { uiModulesPerDependencies };
})();
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zParsedPackageJson = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
type InferredType = z.infer<typeof zParsedPackageJson>;
assert<Equals<InferredType, TargetType>>();
return id<z.ZodType<TargetType>>(zParsedPackageJson);
})();
const parsedPackageJson = JSON.parse(
(await fsPr.readFile(buildContext.packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
if (moduleName.startsWith("@types/")) {
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
continue;
}
if (parsedPackageJson.devDependencies !== undefined) {
delete parsedPackageJson.devDependencies[moduleName];
}
(parsedPackageJson.dependencies ??= {})[moduleName] = versionRange;
}
if (same(parsedPackageJson, parsedPackageJson_before)) {
return;
}
let packageJsonContentStr = JSON.stringify(parsedPackageJson, null, 2);
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
packageJsonContentStr = await runPrettier({
sourceCode: packageJsonContentStr,
filePath: buildContext.packageJsonFilePath
});
}
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});
process.exit(0);
}

View File

@ -0,0 +1,136 @@
import * as fsPr from "fs/promises";
import {
join as pathJoin,
sep as pathSep,
dirname as pathDirname,
relative as pathRelative
} from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta";
import { existsAsync } from "../tools/fs.existsAsync";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
export type BuildContextLike = {
themeSrcDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
const DELIMITER_START = `# === Ejected files start ===`;
const DELIMITER_END = `# === Ejected files end =====`;
export async function writeManagedGitignoreFile(params: {
buildContext: BuildContextLike;
uiModuleMetas: UiModuleMeta[];
ejectedFilesRelativePaths: string[];
}): Promise<void> {
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
if (uiModuleMetas.length === 0) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
const content_new = Buffer.from(
[
`# This file is managed by Keycloakify, do not edit it manually.`,
``,
DELIMITER_START,
...ejectedFilesRelativePaths
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
.map(line => `# ${line}`),
DELIMITER_END,
``,
...uiModuleMetas
.map(uiModuleMeta => [
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
...uiModuleMeta.files
.map(({ fileRelativePath }) => fileRelativePath)
.filter(
fileRelativePath =>
!ejectedFilesRelativePaths.includes(fileRelativePath)
)
.map(
fileRelativePath =>
`/${fileRelativePath.split(pathSep).join("/").replace(/^\.\//, "")}`
),
``
])
.flat()
].join("\n"),
"utf8"
);
const content_current = await (async () => {
if (!(await existsAsync(filePath))) {
return undefined;
}
return await fsPr.readFile(filePath);
})();
if (content_current !== undefined && content_current.equals(content_new)) {
return;
}
create_dir: {
const dirPath = pathDirname(filePath);
if (await existsAsync(dirPath)) {
break create_dir;
}
await fsPr.mkdir(dirPath, { recursive: true });
}
await fsPr.writeFile(filePath, content_new);
}
export async function readManagedGitignoreFile(params: {
buildContext: BuildContextLike;
}): Promise<{
ejectedFilesRelativePaths: string[];
}> {
const { buildContext } = params;
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
if (!(await existsAsync(filePath))) {
return { ejectedFilesRelativePaths: [] };
}
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
const payload = (() => {
const index_start = contentStr.indexOf(DELIMITER_START);
const index_end = contentStr.indexOf(DELIMITER_END);
if (index_start === -1 || index_end === -1) {
return undefined;
}
return contentStr.slice(index_start + DELIMITER_START.length, index_end).trim();
})();
if (payload === undefined) {
return { ejectedFilesRelativePaths: [] };
}
const ejectedFilesRelativePaths = payload
.split("\n")
.map(line => line.trim())
.map(line => line.replace(/^# /, ""))
.filter(line => line !== "")
.map(line =>
getAbsoluteAndInOsFormatPath({
cwd: buildContext.themeSrcDirPath,
pathIsh: line
})
)
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
return { ejectedFilesRelativePaths };
}

View File

@ -0,0 +1,101 @@
import type { BuildContext } from "../shared/buildContext";
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
import {
readManagedGitignoreFile,
writeManagedGitignoreFile
} from "./managedGitignoreFile";
import { dirname as pathDirname } from "path";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import * as fsPr from "fs/promises";
import { getIsTrackedByGit } from "../tools/isTrackedByGit";
import { untrackFromGit } from "../tools/untrackFromGit";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const uiModuleMetas = await getUiModuleMetas({ buildContext });
await installUiModulesPeerDependencies({
buildContext,
uiModuleMetas
});
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
buildContext
});
await writeManagedGitignoreFile({
buildContext,
ejectedFilesRelativePaths,
uiModuleMetas
});
await Promise.all(
uiModuleMetas
.map(uiModuleMeta =>
Promise.all(
uiModuleMeta.files.map(
async ({ fileRelativePath, copyableFilePath, hash }) => {
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
return;
}
const destFilePath = pathJoin(
buildContext.themeSrcDirPath,
fileRelativePath
);
const doesFileExist = await existsAsync(destFilePath);
skip_condition: {
if (!doesFileExist) {
break skip_condition;
}
const destFileHash = computeHash(
await fsPr.readFile(destFilePath)
);
if (destFileHash !== hash) {
break skip_condition;
}
return;
}
git_untrack: {
if (!doesFileExist) {
break git_untrack;
}
const isTracked = await getIsTrackedByGit({
filePath: destFilePath
});
if (!isTracked) {
break git_untrack;
}
await untrackFromGit({
filePath: destFilePath
});
}
{
const dirName = pathDirname(destFilePath);
if (!(await existsAsync(dirName))) {
await fsPr.mkdir(dirName, { recursive: true });
}
}
await fsPr.copyFile(copyableFilePath, destFilePath);
}
)
)
)
.flat()
);
}

View File

@ -0,0 +1,308 @@
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fsPr from "fs/promises";
import type { BuildContext } from "../shared/buildContext";
import { existsAsync } from "../tools/fs.existsAsync";
import { listInstalledModules } from "../tools/listInstalledModules";
import { crawlAsync } from "../tools/crawlAsync";
import { getIsPrettierAvailable, getPrettier } from "../tools/runPrettier";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import {
getUiModuleFileSourceCodeReadyToBeCopied,
type BuildContextLike as BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied
} from "./getUiModuleFileSourceCodeReadyToBeCopied";
import * as crypto from "crypto";
import { KEYCLOAK_THEME } from "../shared/constants";
import { exclude } from "tsafe/exclude";
export type UiModuleMeta = {
moduleName: string;
version: string;
files: {
fileRelativePath: string;
hash: string;
copyableFilePath: string;
}[];
peerDependencies: Record<string, string>;
};
const zUiModuleMeta = (() => {
type ExpectedType = UiModuleMeta;
const zTargetType = z.object({
moduleName: z.string(),
version: z.string(),
files: z.array(
z.object({
fileRelativePath: z.string(),
hash: z.string(),
copyableFilePath: z.string()
})
),
peerDependencies: z.record(z.string())
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
type ParsedCacheFile = {
keycloakifyVersion: string;
prettierConfigHash: string | null;
thisFilePath: string;
uiModuleMetas: UiModuleMeta[];
};
const zParsedCacheFile = (() => {
type ExpectedType = ParsedCacheFile;
const zTargetType = z.object({
keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]),
thisFilePath: z.string(),
uiModuleMetas: z.array(zUiModuleMeta)
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<InferredType, ExpectedType>>();
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
export type BuildContextLike =
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
cacheDirPath: string;
packageJsonFilePath: string;
projectDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getUiModuleMetas(params: {
buildContext: BuildContextLike;
}): Promise<UiModuleMeta[]> {
const { buildContext } = params;
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
const keycloakifyVersion = readThisNpmPackageVersion();
const prettierConfigHash = await (async () => {
if (!(await getIsPrettierAvailable())) {
return null;
}
const { configHash } = await getPrettier();
return configHash;
})();
const installedUiModules = await (async () => {
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
});
return (
await Promise.all(
installedModulesWithKeycloakifyInTheName.map(async entry => {
if (!(await existsAsync(pathJoin(entry.dirPath, KEYCLOAK_THEME)))) {
return undefined;
}
return entry;
})
)
).filter(exclude(undefined));
})();
const cacheContent = await (async () => {
if (!(await existsAsync(cacheFilePath))) {
return undefined;
}
return await fsPr.readFile(cacheFilePath);
})();
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
if (cacheContent === undefined) {
return undefined;
}
const cacheContentStr = cacheContent.toString("utf8");
let parsedCacheFile: unknown;
try {
parsedCacheFile = JSON.parse(cacheContentStr);
} catch {
return undefined;
}
try {
zParsedCacheFile.parse(parsedCacheFile);
} catch {
return undefined;
}
assert(is<ParsedCacheFile>(parsedCacheFile));
return parsedCacheFile;
})();
if (parsedCacheFile === undefined) {
return [];
}
if (parsedCacheFile.keycloakifyVersion !== keycloakifyVersion) {
return [];
}
if (parsedCacheFile.prettierConfigHash !== prettierConfigHash) {
return [];
}
if (parsedCacheFile.thisFilePath !== cacheFilePath) {
return [];
}
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
uiModuleMeta => {
const correspondingInstalledUiModule = installedUiModules.find(
installedUiModule =>
installedUiModule.moduleName === uiModuleMeta.moduleName
);
if (correspondingInstalledUiModule === undefined) {
return false;
}
return correspondingInstalledUiModule.version === uiModuleMeta.version;
}
);
return uiModuleMetas_cacheUpToDate;
})();
const uiModuleMetas = await Promise.all(
installedUiModules.map(
async ({
moduleName,
version,
peerDependencies,
dirPath
}): Promise<UiModuleMeta> => {
use_cache: {
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
uiModuleMeta => uiModuleMeta.moduleName === moduleName
);
if (uiModuleMeta_cache === undefined) {
break use_cache;
}
return uiModuleMeta_cache;
}
const files: UiModuleMeta["files"] = [];
{
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
await crawlAsync({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => {
const sourceCode =
await getUiModuleFileSourceCodeReadyToBeCopied({
buildContext,
fileRelativePath,
isForEjection: false,
uiModuleDirPath: dirPath,
uiModuleName: moduleName,
uiModuleVersion: version
});
const hash = computeHash(sourceCode);
const copyableFilePath = pathJoin(
pathDirname(cacheFilePath),
KEYCLOAK_THEME,
fileRelativePath
);
{
const dirPath = pathDirname(copyableFilePath);
if (!(await existsAsync(dirPath))) {
await fsPr.mkdir(dirPath, { recursive: true });
}
}
fsPr.writeFile(copyableFilePath, sourceCode);
files.push({
fileRelativePath,
hash,
copyableFilePath
});
}
});
}
return id<UiModuleMeta>({
moduleName,
version,
files,
peerDependencies
});
}
)
);
update_cache: {
const parsedCacheFile = id<ParsedCacheFile>({
keycloakifyVersion,
prettierConfigHash,
thisFilePath: cacheFilePath,
uiModuleMetas
});
const cacheContent_new = Buffer.from(
JSON.stringify(parsedCacheFile, null, 2),
"utf8"
);
if (cacheContent !== undefined && cacheContent_new.equals(cacheContent)) {
break update_cache;
}
create_dir: {
const dirPath = pathDirname(cacheFilePath);
if (await existsAsync(dirPath)) {
break create_dir;
}
await fsPr.mkdir(dirPath, { recursive: true });
}
await fsPr.writeFile(cacheFilePath, cacheContent_new);
}
return uiModuleMetas;
}
export function computeHash(data: Buffer) {
return crypto.createHash("sha256").update(data).digest("hex");
}

View File

@ -9,7 +9,7 @@ import {
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import { z } from "zod";
import * as fs from "fs";
import { assert, type Equals } from "tsafe/assert";
import { assert, type Equals, is } from "tsafe/assert";
import * as child_process from "child_process";
import {
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
@ -18,13 +18,11 @@ import {
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { THEME_TYPES } from "./constants";
import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants";
import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
import { is } from "tsafe/is";
export type BuildContext = {
themeVersion: string;
@ -52,6 +50,7 @@ export type BuildContext = {
account:
| { isImplemented: false }
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
admin: { isImplemented: boolean };
};
packageJsonFilePath: string;
bundler: "vite" | "webpack";
@ -146,7 +145,10 @@ export function getBuildContext(params: {
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
for (const themeSrcDirBasename of [
KEYCLOAK_THEME,
KEYCLOAK_THEME.replace(/-/g, "_")
]) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
@ -172,7 +174,7 @@ export function getBuildContext(params: {
[
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
`If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory`
].join("\n")
)
);
@ -448,7 +450,10 @@ export function getBuildContext(params: {
isImplemented: true,
type: buildOptions.accountThemeImplementation
};
})()
})(),
admin: {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
}
};
if (

View File

@ -4,13 +4,14 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
DIST: "dist"
} as const;
export const THEME_TYPES = ["login", "account"] as const;
export const THEME_TYPES = ["login", "account", "admin"] as const;
export type ThemeType = (typeof THEME_TYPES)[number];
export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG",
READ_KC_CONTEXT_FROM_URL: "KEYCLOAKIFY_READ_KC_CONTEXT_FROM_URL"
} as const;
export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME =
@ -76,3 +77,7 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};
export const KEYCLOAK_THEME = "keycloak-theme";
export const KEYCLOAKIFY_SPA_DEV_SERVER_PORT = "KEYCLOAKIFY_SPA_DEV_SERVER_PORT";

View File

@ -11,6 +11,7 @@ export type CommandName =
| "eject-page"
| "add-story"
| "initialize-account-theme"
| "initialize-admin-theme"
| "initialize-email-theme"
| "copy-keycloak-resources-to-public";

View File

@ -8,7 +8,7 @@ import {
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { sep as pathSep } from "path";
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
@ -19,32 +19,7 @@ export function maybeDelegateCommandToCustomHandler(params: {
}): { 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);
})();
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false };

View File

@ -0,0 +1,36 @@
import child_process from "child_process";
import chalk from "chalk";
export function exitIfUncommittedChanges(params: { projectDirPath: string }) {
const { projectDirPath } = params;
let hasUncommittedChanges: boolean | undefined = undefined;
try {
hasUncommittedChanges =
child_process
.execSync(`git status --porcelain`, {
cwd: projectDirPath
})
.toString()
.trim() !== "";
} catch {
// Probably not a git repository
return;
}
if (!hasUncommittedChanges) {
return;
}
console.warn(
[
chalk.red(
"Please commit or stash your changes before running this command.\n"
),
"This command will modify your project's files so it's better to have a clean working directory",
"so that you can easily see what has been changed and revert if needed."
].join(" ")
);
process.exit(-1);
}

View File

@ -628,14 +628,16 @@
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
"clientId": "account-console",
"name": "${client_account-console}",
"description": "",
"rootUrl": "${authBaseUrl}",
"adminUrl": "",
"baseUrl": "/realms/myrealm/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": ["*"],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -647,8 +649,13 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256"
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
@ -1574,14 +1581,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper"
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper"
]
}
},
@ -1612,13 +1619,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper"
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper"
]
}
},
@ -1671,13 +1678,6 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
],
"keyUse": ["SIG"],
"certificate": [
"MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
],
"priority": ["100"]
}
},
@ -1687,13 +1687,6 @@
"providerId": "rsa-enc-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
],
"keyUse": ["ENC"],
"certificate": [
"MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
],
"priority": ["100"],
"algorithm": ["RSA-OAEP"]
}
@ -1704,8 +1697,6 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
"secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
"priority": ["100"]
}
},
@ -1715,10 +1706,6 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
"secret": [
"j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
],
"priority": ["100"],
"algorithm": ["HS512"]
}
@ -2388,7 +2375,7 @@
"clientSessionMaxLifespan": "0",
"organizationsEnabled": "false"
},
"keycloakVersion": "25.0.0",
"keycloakVersion": "25.0.6",
"userManagedAccessAllowed": false,
"organizationsEnabled": false,
"clientProfiles": {

View File

@ -38,6 +38,7 @@
"bruteForceProtected": false,
"permanentLockout": false,
"maxTemporaryLockouts": 0,
"bruteForceStrategy": "MULTIPLE",
"maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60,
@ -604,6 +605,7 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {},
@ -628,14 +630,16 @@
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
"clientId": "account-console",
"name": "${client_account-console}",
"description": "",
"rootUrl": "${authBaseUrl}",
"adminUrl": "",
"baseUrl": "/realms/myrealm/account/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": ["*"],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -647,8 +651,14 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256"
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
@ -699,10 +709,12 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"client.use.lightweight.access.token.enabled": "true",
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": 0,
"defaultClientScopes": [
"web-origins",
@ -740,6 +752,7 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "true",
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {},
@ -789,6 +802,7 @@
"frontchannelLogout": true,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
@ -855,6 +869,7 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "true",
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {},
@ -898,11 +913,13 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"client.use.lightweight.access.token.enabled": "true",
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
@ -1574,14 +1591,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper"
"oidc-usermodel-attribute-mapper"
]
}
},
@ -1611,14 +1628,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper"
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper"
]
}
},
@ -1671,13 +1688,6 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
],
"keyUse": ["SIG"],
"certificate": [
"MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
],
"priority": ["100"]
}
},
@ -1687,13 +1697,6 @@
"providerId": "rsa-enc-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
],
"keyUse": ["ENC"],
"certificate": [
"MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
],
"priority": ["100"],
"algorithm": ["RSA-OAEP"]
}
@ -1704,8 +1707,6 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
"secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
"priority": ["100"]
}
},
@ -1715,10 +1716,6 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
"secret": [
"j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
],
"priority": ["100"],
"algorithm": ["HS512"]
}
@ -2388,7 +2385,7 @@
"clientSessionMaxLifespan": "0",
"organizationsEnabled": "false"
},
"keycloakVersion": "25.0.0",
"keycloakVersion": "26.0.6",
"userManagedAccessAllowed": false,
"organizationsEnabled": false,
"clientProfiles": {

View File

@ -1,7 +1,7 @@
import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { CONTAINER_NAME } from "../shared/constants";
import { CONTAINER_NAME, KEYCLOAKIFY_SPA_DEV_SERVER_PORT } from "../shared/constants";
import { SemVer } from "../tools/SemVer";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
@ -27,6 +27,7 @@ import { isInside } from "../tools/isInside";
import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
import { startViteDevServer } from "./startViteDevServer";
export async function command(params: {
buildContext: BuildContext;
@ -379,6 +380,54 @@ export async function command(params: {
const port =
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
const devServerPort = (() => {
const hasSpaUi =
buildContext.implementedThemeTypes.admin.isImplemented ||
(buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page");
if (!hasSpaUi) {
return undefined;
}
if (buildContext.bundler !== "vite") {
console.log(
chalk.yellow(
[
`WARNING: Since you are using ${buildContext.bundler} instead of Vite,`,
`you'll have to wait serval seconds for the changes you made on your account or admin theme to be reflected in the browser.\n`,
`For a better development experience, consider migrating to Vite.`
].join(" ")
)
);
return undefined;
}
if (keycloakMajorVersionNumber < 25) {
console.log(
chalk.yellow(
[
`WARNING: Your account or admin theme can't be tested with hot module replacement on Keycloak ${keycloakMajorVersionNumber}.`,
`This mean that you'll have to wait serval seconds for the changes to be reflected in the browser.`,
`For a better development experience, select a more recent version of Keycloak.`
].join("\n")
)
);
return undefined;
}
return port + 1;
})();
if (devServerPort !== undefined) {
startViteDevServer({
buildContext,
port: devServerPort
});
}
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
const dockerRunArgs: string[] = [
@ -386,6 +435,11 @@ export async function command(params: {
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
...(devServerPort === undefined
? []
: [
`-e${SPACE_PLACEHOLDER}${KEYCLOAKIFY_SPA_DEV_SERVER_PORT}=${devServerPort}`
]),
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
? []
: [
@ -592,6 +646,44 @@ export async function command(params: {
}
)
.on("all", async (...[, filePath]) => {
ignore_account_spa: {
const doImplementAccountSpa =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page";
if (!doImplementAccountSpa) {
break ignore_account_spa;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
filePath
})
) {
break ignore_account_spa;
}
return;
}
ignore_admin: {
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
break ignore_admin;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
filePath
})
) {
break ignore_admin;
}
return;
}
console.log(`Detected changes in ${filePath}`);
await waitForDebounce();

View File

@ -0,0 +1,39 @@
import * as child_process from "child_process";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import chalk from "chalk";
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
export type BuildContextLike = {
projectDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function startViteDevServer(params: {
buildContext: BuildContextLike;
port: number;
}): void {
const { buildContext, port } = params;
console.log(chalk.blue(`$ npx vite dev --port ${port}`));
const child = child_process.spawn("npx", ["vite", "dev", "--port", `${port}`], {
cwd: buildContext.projectDirPath,
env: {
...process.env,
[VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.READ_KC_CONTEXT_FROM_URL]: "true"
},
shell: true
});
child.stdout.on("data", data => {
if (!data.toString("utf8").includes("[vite] hmr")) {
return;
}
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data));
}

View File

@ -0,0 +1,51 @@
import * as fsPr from "fs/promises";
import { join as pathJoin, relative as pathRelative } from "path";
import { assert, type Equals } from "tsafe/assert";
/** List all files in a given directory return paths relative to the dir_path */
export async function crawlAsync(params: {
dirPath: string;
returnedPathsType: "absolute" | "relative to dirPath";
onFileFound: (filePath: string) => Promise<void>;
}) {
const { dirPath, returnedPathsType, onFileFound } = params;
await crawlAsyncRec({
dirPath,
onFileFound: async ({ filePath }) => {
switch (returnedPathsType) {
case "absolute":
await onFileFound(filePath);
return;
case "relative to dirPath":
await onFileFound(pathRelative(dirPath, filePath));
return;
}
assert<Equals<typeof returnedPathsType, never>>();
}
});
}
async function crawlAsyncRec(params: {
dirPath: string;
onFileFound: (params: { filePath: string }) => Promise<void>;
}) {
const { dirPath, onFileFound } = params;
await Promise.all(
(await fsPr.readdir(dirPath)).map(async basename => {
const fileOrDirPath = pathJoin(dirPath, basename);
const isDirectory = await fsPr
.lstat(fileOrDirPath)
.then(stat => stat.isDirectory());
if (isDirectory) {
await crawlAsyncRec({ dirPath: fileOrDirPath, onFileFound });
return;
}
await onFileFound({ filePath: fileOrDirPath });
})
);
}

View File

@ -0,0 +1,51 @@
import { join as pathJoin } from "path";
import { existsAsync } from "./fs.existsAsync";
import * as child_process from "child_process";
import { assert } from "tsafe/assert";
export async function getInstalledModuleDirPath(params: {
moduleName: string;
packageJsonDirPath: string;
projectDirPath: string;
}) {
const { moduleName, packageJsonDirPath, projectDirPath } = params;
common_case: {
const dirPath = pathJoin(
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
);
if (!(await existsAsync(dirPath))) {
break common_case;
}
return dirPath;
}
node_modules_at_root_case: {
if (projectDirPath === packageJsonDirPath) {
break node_modules_at_root_case;
}
const dirPath = pathJoin(
...[projectDirPath, "node_modules", ...moduleName.split("/")]
);
if (!(await existsAsync(dirPath))) {
break node_modules_at_root_case;
}
return dirPath;
}
const dirPath = child_process
.execSync(`npm list ${moduleName}`, {
cwd: packageJsonDirPath
})
.toString("utf8")
.trim();
assert(dirPath !== "");
return dirPath;
}

View File

@ -0,0 +1,29 @@
import * as child_process from "child_process";
import { dirname as pathDirname, basename as pathBasename } from "path";
import { Deferred } from "evt/tools/Deferred";
export function getIsTrackedByGit(params: { filePath: string }): Promise<boolean> {
const { filePath } = params;
const dIsTracked = new Deferred<boolean>();
child_process.exec(
`git ls-files --error-unmatch ${pathBasename(filePath)}`,
{ cwd: pathDirname(filePath) },
error => {
if (error === null) {
dIsTracked.resolve(true);
return;
}
if (error.code === 1) {
dIsTracked.resolve(false);
return;
}
dIsTracked.reject(error);
}
);
return dIsTracked.pr;
}

View File

@ -0,0 +1,130 @@
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fsPr from "fs/promises";
import { getInstalledModuleDirPath } from "../tools/getInstalledModuleDirPath";
import { exclude } from "tsafe/exclude";
export async function listInstalledModules(params: {
packageJsonFilePath: string;
projectDirPath: string;
filter: (params: { moduleName: string }) => boolean;
}): Promise<
{
moduleName: string;
version: string;
dirPath: string;
peerDependencies: Record<string, string>;
}[]
> {
const { packageJsonFilePath, projectDirPath, filter } = params;
const parsedPackageJson = await readPackageJsonDependencies({
packageJsonFilePath
});
const uiModuleNames = (
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
)
.filter(exclude(undefined))
.map(obj => Object.keys(obj))
.flat()
.filter(moduleName => filter({ moduleName }));
const result = await Promise.all(
uiModuleNames.map(async moduleName => {
const dirPath = await getInstalledModuleDirPath({
moduleName,
packageJsonDirPath: pathDirname(packageJsonFilePath),
projectDirPath
});
const { version, peerDependencies } =
await readPackageJsonVersionAndPeerDependencies({
packageJsonFilePath: pathJoin(dirPath, "package.json")
});
return { moduleName, version, peerDependencies, dirPath } as const;
})
);
return result;
}
const { readPackageJsonDependencies } = (() => {
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);
})();
async function readPackageJsonDependencies(params: { packageJsonFilePath: string }) {
const { packageJsonFilePath } = params;
const parsedPackageJson = JSON.parse(
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
}
return { readPackageJsonDependencies };
})();
const { readPackageJsonVersionAndPeerDependencies } = (() => {
type ParsedPackageJson = {
version: string;
peerDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
version: z.string(),
peerDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
async function readPackageJsonVersionAndPeerDependencies(params: {
packageJsonFilePath: string;
}): Promise<{ version: string; peerDependencies: Record<string, string> }> {
const { packageJsonFilePath } = params;
const parsedPackageJson = JSON.parse(
(await fsPr.readFile(packageJsonFilePath)).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return {
version: parsedPackageJson.version,
peerDependencies: parsedPackageJson.peerDependencies ?? {}
};
}
return { readPackageJsonVersionAndPeerDependencies };
})();

View File

@ -0,0 +1,38 @@
import { sep as pathSep } from "path";
let cache: string | undefined = undefined;
export function getNodeModulesBinDirPath() {
if (cache !== undefined) {
return cache;
}
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);
}
const nodeModulesBinDirPath = segments.join(pathSep);
cache = nodeModulesBinDirPath;
return nodeModulesBinDirPath;
}

View File

@ -1,7 +1,14 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as child_process from "child_process";
import chalk from "chalk";
import { z } from "zod";
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import { objectKeys } from "tsafe/objectKeys";
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
import { exclude } from "tsafe/exclude";
import { rmSync } from "./fs.rmSync";
export function npmInstall(params: { packageJsonDirPath: string }) {
const { packageJsonDirPath } = params;
@ -23,6 +30,10 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
{
binName: "bun",
lockFileBasename: "bun.lockdb"
},
{
binName: "deno",
lockFileBasename: "deno.lock"
}
] as const;
@ -37,27 +48,411 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
}
}
return undefined;
throw new Error(
"No lock file found, cannot tell which package manager to use for installing dependencies."
);
})();
install_dependencies: {
if (packageManagerBinName === undefined) {
break install_dependencies;
console.log(`Installing the new dependencies...`);
install_without_breaking_links: {
if (packageManagerBinName !== "yarn") {
break install_without_breaking_links;
}
console.log(`Installing the new dependencies...`);
const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath });
try {
child_process.execSync(`${packageManagerBinName} install`, {
cwd: packageJsonDirPath,
stdio: "inherit"
});
} catch {
console.log(
chalk.yellow(
`\`${packageManagerBinName} install\` failed, continuing anyway...`
)
);
if (garronejLinkInfos === undefined) {
break install_without_breaking_links;
}
console.log(chalk.green("Installing in a way that won't break the links..."));
installWithoutBreakingLinks({
packageJsonDirPath,
garronejLinkInfos
});
return;
}
try {
child_process.execSync(`${packageManagerBinName} install`, {
cwd: packageJsonDirPath,
stdio: "inherit"
});
} catch {
console.log(
chalk.yellow(
`\`${packageManagerBinName} install\` failed, continuing anyway...`
)
);
}
}
function getGarronejLinkInfos(params: {
packageJsonDirPath: string;
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
const { packageJsonDirPath } = params;
const nodeModuleDirPath = pathJoin(packageJsonDirPath, "node_modules");
if (!fs.existsSync(nodeModuleDirPath)) {
return undefined;
}
const linkedModuleNames: string[] = [];
let yarnHomeDirPath: string | undefined = undefined;
const getIsLinkedByGarronejScript = (path: string) => {
let realPath: string;
try {
realPath = fs.readlinkSync(path);
} catch {
return false;
}
const doesIncludeYarnHome = realPath.includes(".yarn_home");
if (!doesIncludeYarnHome) {
return false;
}
set_yarnHomeDirPath: {
if (yarnHomeDirPath !== undefined) {
break set_yarnHomeDirPath;
}
const [firstElement] = getAbsoluteAndInOsFormatPath({
pathIsh: realPath,
cwd: pathDirname(path)
}).split(".yarn_home");
yarnHomeDirPath = pathJoin(firstElement, ".yarn_home");
}
return true;
};
for (const basename of fs.readdirSync(nodeModuleDirPath)) {
const path = pathJoin(nodeModuleDirPath, basename);
if (fs.lstatSync(path).isSymbolicLink()) {
if (basename.startsWith("@")) {
return undefined;
}
if (!getIsLinkedByGarronejScript(path)) {
return undefined;
}
linkedModuleNames.push(basename);
continue;
}
if (!fs.lstatSync(path).isDirectory()) {
continue;
}
if (basename.startsWith("@")) {
for (const subBasename of fs.readdirSync(path)) {
const subPath = pathJoin(path, subBasename);
if (!fs.lstatSync(subPath).isSymbolicLink()) {
continue;
}
if (!getIsLinkedByGarronejScript(subPath)) {
return undefined;
}
linkedModuleNames.push(`${basename}/${subBasename}`);
}
}
}
if (yarnHomeDirPath === undefined) {
return undefined;
}
return { linkedModuleNames, yarnHomeDirPath };
}
function installWithoutBreakingLinks(params: {
packageJsonDirPath: string;
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
}) {
const {
packageJsonDirPath,
garronejLinkInfos: { linkedModuleNames, yarnHomeDirPath }
} = params;
const parsedPackageJson = (() => {
const packageJsonFilePath = pathJoin(packageJsonDirPath, "package.json");
type ParsedPackageJson = {
scripts?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.string()).optional()
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
) as unknown;
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
const isImplementedScriptByName = {
postinstall: false,
prepare: false
};
delete_postinstall_script: {
if (parsedPackageJson.scripts === undefined) {
break delete_postinstall_script;
}
for (const scriptName of objectKeys(isImplementedScriptByName)) {
if (parsedPackageJson.scripts[scriptName] === undefined) {
continue;
}
isImplementedScriptByName[scriptName] = true;
delete parsedPackageJson.scripts[scriptName];
}
}
const tmpProjectDirPath = pathJoin(yarnHomeDirPath, "tmpProject");
if (fs.existsSync(tmpProjectDirPath)) {
rmSync(tmpProjectDirPath, { recursive: true });
}
fs.mkdirSync(tmpProjectDirPath, { recursive: true });
fs.writeFileSync(
pathJoin(tmpProjectDirPath, "package.json"),
JSON.stringify(parsedPackageJson, undefined, 4)
);
const YARN_LOCK = "yarn.lock";
fs.copyFileSync(
pathJoin(packageJsonDirPath, YARN_LOCK),
pathJoin(tmpProjectDirPath, YARN_LOCK)
);
child_process.execSync(`yarn install`, {
cwd: tmpProjectDirPath,
stdio: "inherit"
});
// NOTE: Moving the modules from the tmp project to the actual project
// without messing up the links.
{
const { getAreSameVersions } = (() => {
type ParsedPackageJson = {
version: string;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
version: z.string()
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
function readVersion(params: { moduleDirPath: string }): string {
const { moduleDirPath } = params;
const packageJsonFilePath = pathJoin(moduleDirPath, "package.json");
const packageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(packageJson);
assert(is<ParsedPackageJson>(packageJson));
return packageJson.version;
}
function getAreSameVersions(params: {
moduleDirPath_a: string;
moduleDirPath_b: string;
}): boolean {
const { moduleDirPath_a, moduleDirPath_b } = params;
return (
readVersion({ moduleDirPath: moduleDirPath_a }) ===
readVersion({ moduleDirPath: moduleDirPath_b })
);
}
return { getAreSameVersions };
})();
const nodeModulesDirPath_tmpProject = pathJoin(tmpProjectDirPath, "node_modules");
const nodeModulesDirPath = pathJoin(packageJsonDirPath, "node_modules");
const modulePaths = fs
.readdirSync(nodeModulesDirPath_tmpProject)
.map(basename => {
if (basename.startsWith(".")) {
return undefined;
}
const path = pathJoin(nodeModulesDirPath_tmpProject, basename);
if (basename.startsWith("@")) {
return fs
.readdirSync(path)
.map(subBasename => {
if (subBasename.startsWith(".")) {
return undefined;
}
const subPath = pathJoin(path, subBasename);
if (!fs.lstatSync(subPath).isDirectory()) {
return undefined;
}
return {
moduleName: `${basename}/${subBasename}`,
moduleDirPath_tmpProject: subPath,
moduleDirPath: pathJoin(
nodeModulesDirPath,
basename,
subBasename
)
};
})
.filter(exclude(undefined));
}
if (!fs.lstatSync(path).isDirectory()) {
return undefined;
}
return [
{
moduleName: basename,
moduleDirPath_tmpProject: path,
moduleDirPath: pathJoin(nodeModulesDirPath, basename)
}
];
})
.filter(exclude(undefined))
.flat();
for (const {
moduleName,
moduleDirPath,
moduleDirPath_tmpProject
} of modulePaths) {
if (linkedModuleNames.includes(moduleName)) {
continue;
}
let doesTargetModuleExist = false;
skip_condition: {
if (!fs.existsSync(moduleDirPath)) {
break skip_condition;
}
doesTargetModuleExist = true;
const areSameVersions = getAreSameVersions({
moduleDirPath_a: moduleDirPath,
moduleDirPath_b: moduleDirPath_tmpProject
});
if (!areSameVersions) {
break skip_condition;
}
continue;
}
if (doesTargetModuleExist) {
rmSync(moduleDirPath, { recursive: true });
}
{
const dirPath = pathDirname(moduleDirPath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.renameSync(moduleDirPath_tmpProject, moduleDirPath);
}
move_bin: {
const binDirPath_tmpProject = pathJoin(nodeModulesDirPath_tmpProject, ".bin");
const binDirPath = pathJoin(nodeModulesDirPath, ".bin");
if (!fs.existsSync(binDirPath_tmpProject)) {
break move_bin;
}
for (const basename of fs.readdirSync(binDirPath_tmpProject)) {
const path_tmpProject = pathJoin(binDirPath_tmpProject, basename);
const path = pathJoin(binDirPath, basename);
if (fs.existsSync(path)) {
continue;
}
fs.renameSync(path_tmpProject, path);
}
}
}
fs.cpSync(
pathJoin(tmpProjectDirPath, YARN_LOCK),
pathJoin(packageJsonDirPath, YARN_LOCK)
);
rmSync(tmpProjectDirPath, { recursive: true });
for (const scriptName of objectKeys(isImplementedScriptByName)) {
if (!isImplementedScriptByName[scriptName]) {
continue;
}
child_process.execSync(`yarn run ${scriptName}`, {
cwd: packageJsonDirPath,
stdio: "inherit"
});
}
}

View File

@ -3,7 +3,13 @@ import { assert } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin } from "path";
let cache: string | undefined = undefined;
export function readThisNpmPackageVersion(): string {
if (cache !== undefined) {
return cache;
}
const version = JSON.parse(
fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json"))
@ -12,5 +18,7 @@ export function readThisNpmPackageVersion(): string {
assert(typeof version === "string");
cache = version;
return version;
}

View File

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

View File

@ -0,0 +1,126 @@
import { getNodeModulesBinDirPath } from "./nodeModulesBinDirPath";
import { join as pathJoin, resolve as pathResolve } from "path";
import * as fsPr from "fs/promises";
import { id } from "tsafe/id";
import { assert, is } from "tsafe/assert";
import chalk from "chalk";
import * as crypto from "crypto";
import { symToStr } from "tsafe/symToStr";
import { readThisNpmPackageVersion } from "./readThisNpmPackageVersion";
getIsPrettierAvailable.cache = id<boolean | undefined>(undefined);
export async function getIsPrettierAvailable(): Promise<boolean> {
if (getIsPrettierAvailable.cache !== undefined) {
return getIsPrettierAvailable.cache;
}
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
const stats = await fsPr.stat(prettierBinPath).catch(() => undefined);
const isPrettierAvailable = stats?.isFile() ?? false;
getIsPrettierAvailable.cache = isPrettierAvailable;
return isPrettierAvailable;
}
type PrettierAndConfigHash = {
prettier: typeof import("prettier");
configHash: string;
};
getPrettier.cache = id<PrettierAndConfigHash | undefined>(undefined);
export async function getPrettier(): Promise<PrettierAndConfigHash> {
assert(getIsPrettierAvailable());
if (getPrettier.cache !== undefined) {
return getPrettier.cache;
}
let prettier = id<typeof import("prettier") | undefined>(undefined);
import_prettier: {
// NOTE: When module is linked we want to make sure we import the correct version
// of prettier, that is the one of the project, not the one of this repo.
// So we do a sketchy eval to bypass ncc.
// We make sure to only do that when linking, otherwise we import properly.
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
eval(
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
);
assert(!is<undefined>(prettier));
break import_prettier;
}
prettier = await import("prettier");
}
const configHash = await (async () => {
const configFilePath = await prettier.resolveConfigFile(
pathJoin(getNodeModulesBinDirPath(), "..")
);
if (configFilePath === null) {
return "";
}
const data = await fsPr.readFile(configFilePath);
return crypto.createHash("sha256").update(data).digest("hex");
})();
const prettierAndConfig: PrettierAndConfigHash = {
prettier,
configHash
};
getPrettier.cache = prettierAndConfig;
return prettierAndConfig;
}
export async function runPrettier(params: {
sourceCode: string;
filePath: string;
}): Promise<string> {
const { sourceCode, filePath } = params;
let formattedSourceCode: string;
try {
const { prettier } = await getPrettier();
const { ignored, inferredParser } = await prettier.getFileInfo(filePath, {
resolveConfig: true
});
if (ignored) {
return sourceCode;
}
const config = await prettier.resolveConfig(filePath);
formattedSourceCode = await prettier.format(sourceCode, {
...config,
filePath,
parser: inferredParser ?? undefined
});
} catch (error) {
console.log(
chalk.red(
`You probably need to upgrade the version of prettier in your project`
)
);
throw error;
}
return formattedSourceCode;
}

View File

@ -0,0 +1,24 @@
import * as child_process from "child_process";
import { dirname as pathDirname, basename as pathBasename } from "path";
import { Deferred } from "evt/tools/Deferred";
export async function untrackFromGit(params: { filePath: string }): Promise<void> {
const { filePath } = params;
const dDone = new Deferred<void>();
child_process.exec(
`git rm --cached ${pathBasename(filePath)}`,
{ cwd: pathDirname(filePath) },
error => {
if (error !== null) {
dDone.reject(error);
return;
}
dDone.resolve();
}
);
await dDone.pr;
}

View File

@ -3,12 +3,22 @@ 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";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
run_copy_assets_to_public: {
if (buildContext.bundler !== "webpack") {
break run_copy_assets_to_public;
}
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ buildContext });
}
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
@ -18,12 +28,13 @@ export async function command(params: { buildContext: BuildContext }) {
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx");
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
const newContent = [
let newContent = [
``,
`/* eslint-disable */`,
``,
@ -51,9 +62,15 @@ export async function command(params: { buildContext: BuildContext }) {
2
)};`,
``,
`type KcContext =`,
`/**`,
` * NOTE: Do not import this type except maybe in your entrypoint. `,
` * If you need to import the KcContext import it either from src/login/KcContext.ts or src/account/KcContext.ts.`,
` * Depending on the theme type you are working on.`,
` */`,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
@ -66,6 +83,8 @@ export async function command(params: { buildContext: BuildContext }) {
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
hasAdminTheme &&
`export const KcAdminPage = lazy(() => import("./admin/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
@ -82,6 +101,8 @@ export async function command(params: { buildContext: BuildContext }) {
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
hasAdminTheme &&
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
@ -108,20 +129,25 @@ export async function command(params: { buildContext: BuildContext }) {
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"
)
);
newContent = [
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n");
runFormat({ packageJsonFilePath: buildContext.packageJsonFilePath });
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
newContent = await runPrettier({
filePath,
sourceCode: newContent
});
}
await fs.writeFile(filePath, Buffer.from(newContent, "utf8"));
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");

View File

@ -1,8 +1,7 @@
import type { Param0 } from "tsafe";
import { type CxArg, clsx_withTransform } from "../tools/clsx_withTransform";
import { clsx } from "../tools/clsx";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { assert, is } from "tsafe/assert";
export function createGetKcClsx<ClassKey extends string>(params: {
defaultClasses: Record<ClassKey, string | undefined>;

View File

@ -33,7 +33,7 @@ export type KcContext =
| KcContext.LoginResetPassword
| KcContext.LoginVerifyEmail
| KcContext.Terms
| KcContext.LoginDeviceVerifyUserCode
| KcContext.LoginOauth2DeviceVerifyUserCode
| KcContext.LoginOauthGrant
| KcContext.LoginOtp
| KcContext.LoginUsername
@ -277,7 +277,7 @@ export declare namespace KcContext {
__localizationRealmOverridesTermsText?: string;
};
export type LoginDeviceVerifyUserCode = Common & {
export type LoginOauth2DeviceVerifyUserCode = Common & {
pageId: "login-oauth2-device-verify-user-code.ftl";
url: {
oauth2DeviceVerificationAction: string;

View File

@ -290,7 +290,7 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "terms.ftl"
}),
id<KcContext.LoginDeviceVerifyUserCode>({
id<KcContext.LoginOauth2DeviceVerifyUserCode>({
...kcContextCommonMock,
pageId: "login-oauth2-device-verify-user-code.ftl",
url: loginUrl

View File

@ -11,7 +11,6 @@ export type TemplateProps<KcContext, I18n> = {
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
socialProvidersNode?: ReactNode;
infoNode?: ReactNode;

View File

@ -1,11 +1,11 @@
import "keycloakify/tools/Object.fromEntries";
import { assert } from "tsafe/assert";
import { assert, is } from "tsafe/assert";
import { extractLastParenthesisContent } from "keycloakify/tools/extractLastParenthesisContent";
import messages_defaultSet_fallbackLanguage from "../messages_defaultSet/en";
import { fetchMessages_defaultSet } from "../messages_defaultSet";
import type { KcContext } from "../../KcContext";
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
import { id } from "tsafe/id";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
import {
type LanguageTag as LanguageTag_defaultSet,
@ -16,6 +16,9 @@ import type { GenericI18n_noJsx } from "./GenericI18n_noJsx";
export type KcContextLike = {
themeName: string;
realm: {
internationalizationEnabled: boolean;
};
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
@ -91,14 +94,16 @@ export function createGetI18n<
return cachedResult;
}
const kcContextLocale = params.kcContext.realm.internationalizationEnabled ? params.kcContext.locale : undefined;
{
const currentLanguageTag = kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
const currentLanguageTag = kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
const html = document.querySelector("html");
assert(html !== null);
html.lang = currentLanguageTag;
const isRtl = (() => {
const { rtl } = kcContext.locale ?? {};
const { rtl } = kcContextLocale ?? {};
if (rtl !== undefined) {
return rtl;
@ -154,22 +159,20 @@ 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;
}
// cspell: disable-next-line
// from "Espagnol (Español)" we want to extract "Español"
const match = supportedEntry.label.match(/[^(]+\(([^)]+)\)/);
const lastParenthesisContent = extractLastParenthesisContent(supportedEntry.label);
if (match !== null) {
return match[1];
if (lastParenthesisContent !== undefined) {
return lastParenthesisContent;
}
return supportedEntry.label;
@ -180,7 +183,7 @@ export function createGetI18n<
};
const currentLanguage: I18n["currentLanguage"] = (() => {
const languageTag = id<string>(kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
const languageTag = id<string>(kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
return {
languageTag,
@ -191,8 +194,8 @@ export function createGetI18n<
const enabledLanguages: I18n["enabledLanguages"] = (() => {
const enabledLanguages: I18n["enabledLanguages"] = [];
if (kcContext.locale !== undefined) {
for (const entry of kcContext.locale.supported ?? []) {
if (kcContextLocale !== undefined) {
for (const entry of kcContextLocale.supported ?? []) {
const languageTag = id<string>(entry.languageTag) as LanguageTag;
enabledLanguages.push({

View File

@ -47,11 +47,25 @@ export function createUseI18n<
function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element {
const { htmlString, msgKey } = params;
const htmlString_sanitized = kcSanitize(htmlString);
const Element = (() => {
if (htmlString_sanitized.includes("<") && htmlString_sanitized.includes(">")) {
for (const tagName of ["div", "section", "article", "ul", "ol"]) {
if (htmlString_sanitized.includes(`<${tagName}`)) {
return "div";
}
}
}
return "span";
})();
return (
<div
<Element
data-kc-msg={msgKey}
dangerouslySetInnerHTML={{
__html: kcSanitize(htmlString)
__html: htmlString_sanitized
}}
/>
);
@ -83,7 +97,7 @@ export function createUseI18n<
})();
add_style: {
const attributeName = "data-kc-i18n";
const attributeName = "data-kc-msg";
// Check if already exists in head
if (document.querySelector(`style[${attributeName}]`) !== null) {
@ -92,7 +106,7 @@ export function createUseI18n<
const styleElement = document.createElement("style");
styleElement.attributes.setNamedItem(document.createAttribute(attributeName));
styleElement.textContent = `[data-kc-msg] { display: inline-block; }`;
styleElement.textContent = `div[${attributeName}] { display: inline-block; }`;
document.head.prepend(styleElement);
}

View File

@ -52,28 +52,26 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
</li>
<li>
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {totp.policy.getAlgorithmKey()}
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
)}
</ul>
</li>
</>
) : (

View File

@ -115,62 +115,61 @@ export default function LoginPasskeysConditionalAuthenticate(
</div>
</>
)}
<div id="kc-form">
<div id="kc-form-wrapper">
{realm.password && (
<form
id="kc-form-passkey"
action={url.loginAction}
method="post"
style={{ display: "none" }}
onSubmit={event => {
try {
// @ts-expect-error
event.target.login.disabled = true;
} catch {}
return true;
}}
>
{!usernameHidden && (
<div className={kcClsx("kcFormGroupClass")}>
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
{msg("passkey-autofill-select")}
</label>
<input
tabIndex={1}
id="username"
aria-invalid={messagesPerField.existsError("username")}
className={kcClsx("kcInputClass")}
name="username"
defaultValue={login.username ?? ""}
//autoComplete="username webauthn"
type="text"
autoFocus
autoComplete="off"
/>
{messagesPerField.existsError("username") && (
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("username")}
</span>
)}
</div>
)}
</form>
)}
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
<input
id={authButtonId}
type="button"
autoFocus
value={msgStr("passkey-doAuthenticate")}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
/>
</div>
</div>
</div>
</>
)}
<div id="kc-form">
<div id="kc-form-wrapper">
{realm.password && (
<form
id="kc-form-login"
action={url.loginAction}
method="post"
style={{ display: "none" }}
onSubmit={event => {
try {
// @ts-expect-error
event.target.login.disabled = true;
} catch {}
return true;
}}
>
{!usernameHidden && (
<div className={kcClsx("kcFormGroupClass")}>
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
{msg("passkey-autofill-select")}
</label>
<input
tabIndex={1}
id="username"
aria-invalid={messagesPerField.existsError("username")}
className={kcClsx("kcInputClass")}
name="username"
defaultValue={login.username ?? ""}
autoComplete="username webauthn"
type="text"
autoFocus
/>
{messagesPerField.existsError("username") && (
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
{messagesPerField.get("username")}
</span>
)}
</div>
)}
</form>
)}
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
<input
id={authButtonId}
type="button"
autoFocus
value={msgStr("passkey-doAuthenticate")}
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
/>
</div>
</div>
</div>
</div>
</Template>
);

View File

@ -70,9 +70,9 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
type="checkbox"
id="kcRecoveryCodesConfirmationCheck"
name="kcRecoveryCodesConfirmationCheck"
onChange={function () {
//@ts-expect-error: This is code from the original theme, we trust it.
document.getElementById("saveRecoveryAuthnCodesBtn").disabled = !this.checked;
onChange={event => {
//@ts-expect-error: This is inherited from the original code
document.getElementById("saveRecoveryAuthnCodesBtn").disabled = !event.target.checked;
}}
/>
<label htmlFor="kcRecoveryCodesConfirmationCheck">{msg("recovery-codes-confirmation-message")}</label>

View File

@ -1,5 +1,4 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { assert, is } from "tsafe/assert";
export type StatefulObservable<T> = {
current: T;

View File

@ -1,5 +1,4 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { assert, is } from "tsafe/assert";
import { structuredCloneButFunctions } from "./structuredCloneButFunctions";
/** NOTE: Array a copied over, not merged. */

View File

@ -0,0 +1,43 @@
/**
* "Hello (world)" => "world"
* "Hello (world) (foo)" => "foo"
* "Hello (world (foo))" => "world (foo)"
*/
export function extractLastParenthesisContent(str: string): string | undefined {
const chars: string[] = [];
for (const char of str) {
chars.push(char);
}
const extractedChars: string[] = [];
let openingCount = 0;
loop_through_char: for (let i = chars.length - 1; i >= 0; i--) {
const char = chars[i];
if (i === chars.length - 1) {
if (char !== ")") {
return undefined;
}
continue;
}
switch (char) {
case ")":
openingCount++;
break;
case "(":
if (openingCount === 0) {
return extractedChars.join("");
}
openingCount--;
break;
}
extractedChars.unshift(char);
}
return undefined;
}

View File

@ -212,6 +212,73 @@ export function keycloakify(params: keycloakify.Params) {
force: true
}
);
},
transformIndexHtml: html => {
const doReadKcContextFromUrl =
process.env.NODE_ENV === "development" &&
process.env[
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES.READ_KC_CONTEXT_FROM_URL
] === "true";
if (!doReadKcContextFromUrl) {
return html;
}
const scriptContent = `
(()=>{
const kcContext = (()=>{
const paramName= "kcContext";
read_from_url_case: {
const url = new URL(window.location.href);
const paramValue = url.searchParams.get(paramName);
if( paramValue === null ){
break read_from_url_case;
}
url.searchParams.delete(paramName);
window.history.replaceState({}, "", url);
const kcContext = JSON.parse(decodeURIComponent(paramValue));
sessionStorage.setItem(paramName, JSON.stringify(kcContext));
return kcContext;
}
read_from_session_storage_case: {
const paramValue = sessionStorage.getItem(paramName);
if( paramValue === null ){
break read_from_session_storage_case;
}
return JSON.parse(paramValue);
}
return undefined;
})();
if( kcContext === undefined ){
return;
}
window.kcContext = kcContext;
})();
`;
return html.replace(/<head>/, `<head><script>${scriptContent}</script>`);
}
} satisfies Plugin;

View File

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

View File

@ -1,18 +0,0 @@
import React from "react";
import type { Meta, StoryObj } from "@storybook/react";
import { createKcPageStory } from "../KcPageStory";
const { KcPageStory } = createKcPageStory({ pageId: "login-oauth2-device-verify-user-code.ftl" });
const meta = {
title: "login/login-oauth2-device-verify-user-code.ftl",
component: KcPageStory
} satisfies Meta<typeof KcPageStory>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <KcPageStory />
};

View File

@ -6172,6 +6172,14 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50"
type "^1.0.1"
d@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de"
integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==
dependencies:
es5-ext "^0.10.64"
type "^2.7.2"
data-urls@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde"
@ -6743,7 +6751,7 @@ es-to-primitive@^1.2.1:
is-date-object "^1.0.1"
is-symbol "^1.0.2"
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
version "0.10.62"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
@ -6752,6 +6760,16 @@ es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@
es6-symbol "^3.1.3"
next-tick "^1.1.0"
es5-ext@^0.10.62, es5-ext@^0.10.64:
version "0.10.64"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
dependencies:
es6-iterator "^2.0.3"
es6-symbol "^3.1.3"
esniff "^2.0.1"
next-tick "^1.1.0"
es5-shim@^4.5.13:
version "4.6.7"
resolved "https://registry.yarnpkg.com/es5-shim/-/es5-shim-4.6.7.tgz#bc67ae0fc3dd520636e0a1601cc73b450ad3e955"
@ -6920,6 +6938,16 @@ eslint-visitor-keys@^3.3.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz#c7f0f956124ce677047ddbc192a68f999454dedc"
integrity sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==
esniff@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308"
integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
dependencies:
d "^1.0.1"
es5-ext "^0.10.62"
event-emitter "^0.3.5"
type "^2.7.2"
esprima@^4.0.1, esprima@~4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
@ -6994,14 +7022,14 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
evt@^2.5.7:
version "2.5.7"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.5.7.tgz#55c5f8ff910f4b7531bfac91e963d4cb3231f253"
integrity sha512-dr7Wd16ry5F8WNU1xXLKpFpO3HsoAGg8zC48e08vDdzMzGWCP9/QFGt1PQptEEDh8SwYP3EL8M+d/Gb0kgUp6g==
evt@^2.5.8:
version "2.5.8"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.5.8.tgz#4cc4e2db92d89cc43e00a0755703317f5f9b6d4c"
integrity sha512-wjurRtEqepH03fZSzkCUAw2tNNtEoeoEYXf7EqG8uU52wQ3D/+ctyQE8hf+YR/5DXZlwl2jNGLMm4hAd/wyD6g==
dependencies:
minimal-polyfills "^2.2.3"
run-exclusive "^2.2.19"
tsafe "^1.6.6"
tsafe "^1.8.5"
execa@^5.1.1:
version "5.1.1"
@ -9350,13 +9378,13 @@ memfs@^3.1.2, memfs@^3.2.2:
dependencies:
fs-monkey "^1.0.3"
memoizee@^0.4.15:
version "0.4.15"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.15.tgz#e6f3d2da863f318d02225391829a6c5956555b72"
integrity sha512-UBWmJpLZd5STPm7PMUlOw/TSy972M+z8gcyQ5veOnSDRREz/0bmpyTfKt3/51DhEBqCZQn1udM/5flcSPYhkdQ==
memoizee@^0.4.17:
version "0.4.17"
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949"
integrity sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==
dependencies:
d "^1.0.1"
es5-ext "^0.10.53"
d "^1.0.2"
es5-ext "^0.10.64"
es6-weak-map "^2.0.3"
event-emitter "^0.3.5"
is-promise "^2.2.2"
@ -10629,14 +10657,14 @@ postcss@^8.4.38:
picocolors "^1.0.0"
source-map-js "^1.2.0"
powerhooks@^1.0.10:
version "1.0.10"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-1.0.10.tgz#9d532b23294cba69d5872d859bdf843b91cb9c41"
integrity sha512-fJmKSVJk9Bkotl9rK1ryVPSqhY4xOZKxM6deyTaMbICK8jHh/CQztN2mNTxlKu6vkJBIFj1cRd/7N6slh3uM6w==
powerhooks@^1.0.19:
version "1.0.19"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-1.0.19.tgz#e93ce44f55d31ca26a4511883a49cfe5341d47af"
integrity sha512-kmZX0LLvJyaIphwf2FjKABAQwQFrRqsEA41N4CB8EzfDobyXle545t4teajVo/NPboy0HEaioyv55FWN7d58/Q==
dependencies:
evt "^2.5.7"
memoizee "^0.4.15"
tsafe "^1.6.6"
evt "^2.5.8"
memoizee "^0.4.17"
tsafe "^1.8.5"
prelude-ls@~1.1.2:
version "1.1.2"
@ -12574,15 +12602,10 @@ ts-pnp@^1.1.6:
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92"
integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw==
tsafe@^1.6.6:
version "1.6.6"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.6.tgz#fd93e64d6eb13ef83ed1650669cc24bad4f5df9f"
integrity sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==
tsafe@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.7.5.tgz#0d3a31202b5ef87c7ba997e66e03fd80801278ef"
integrity sha512-tbNyyBSbwfbilFfiuXkSOj82a6++ovgANwcoqBAcO9/REPoZMEQoE8kWPeO0dy5A2D/2Lajr8Ohue5T0ifIvLQ==
tsafe@^1.8.5:
version "1.8.5"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.8.5.tgz#cdf9fa3111974ac480d7ee519f8241815e5d22ea"
integrity sha512-LFWTWQrW6rwSY+IBNFl2ridGfUzVsPwrZ26T4KUJww/py8rzaQ/SY+MIz6YROozpUCaRcuISqagmlwub9YT9kw==
tsc-alias@^1.8.10:
version "1.8.10"