Compare commits

...

85 Commits

Author SHA1 Message Date
6af13e1405 Bump version 2025-01-03 22:39:01 +01:00
f59fa4238c Link to the documentation for implementing non builtin pages 2025-01-03 22:38:45 +01:00
248effc57d Bump version 2025-01-03 02:47:31 +01:00
9e540b2c1f Fix assets import from public in .svelte files 2025-01-03 02:47:31 +01:00
ab7b5ff490 Remove ringerhq 2025-01-03 01:06:29 +01:00
486f944e0f Bump version 2025-01-02 10:25:16 +01:00
6cc3d4c442 Fix conflict in --path shorthand with --project, --path shorthand is now -t (target) 2025-01-02 10:25:16 +01:00
083290c6d4 update broken link 2024-12-30 02:04:09 +01:00
cd1b55b850 Fix test 2024-12-27 01:49:38 +01:00
482ba6c639 Bump version 2024-12-27 01:41:29 +01:00
e2921b7e37 Fix auto add of postinstall script 2024-12-27 01:38:11 +01:00
c87b6153bb Fix some errors implementing the new account SPA feature 2024-12-27 01:36:29 +01:00
488dd2c6b9 Migrate to extention model for Account SPA 2024-12-26 15:55:59 +01:00
1ac678a368 Remove dead code 2024-12-26 15:24:12 +01:00
5866c802e5 Always initialize email with the latest keycloak version 2024-12-26 15:24:03 +01:00
fe892c840b Bump version 2024-12-24 17:40:31 +01:00
9685dfb55a Fix git integration bug 2024-12-24 17:39:54 +01:00
c1dc899bc1 Better naming convention 'uiModules' -> 'extensionModules' 2024-12-24 16:43:42 +01:00
d2da43c617 Implement --revert for own command 2024-12-24 01:10:32 +01:00
6de5fd4f96 Optimization and potentially prevend leaving the repo in a broken state 2024-12-23 18:49:00 +01:00
cc3d0d61dd Consistent naming scheme 'eject' -> 'own' 2024-12-23 18:34:42 +01:00
4403f00274 Reneame the 'eject-file' command to 'own' 2024-12-23 17:55:40 +01:00
eddfb8e634 Bump version 2024-12-22 21:58:07 +01:00
4f2790f6d3 Fixes on the admin initialization cmd 2024-12-22 21:57:52 +01:00
96690e1354 Generate the postinstall script as the first entry of the package.json 2024-12-22 21:25:51 +01:00
982f216a01 Do not take into account react in the resolution of ui modules peer dependencies for supporting React 19 2024-12-22 21:21:20 +01:00
13c21e8910 Implement initialize-admin-theme command 2024-12-22 17:09:15 +01:00
94b7d2b85b Bump version 2024-12-21 19:40:07 +01:00
9a4f89e69d Sort out version of keycloak not supported depending on which theme is implemented (start-keycloak cmd) 2024-12-21 19:39:10 +01:00
a5ba03cca0 Reload when .properties files are updated 2024-12-21 19:02:13 +01:00
5203813e7b Rebuild the theme when properties files changes 2024-12-21 15:22:19 +01:00
0e461fd072 Fix bugs in svg assets commenting for mirror files 2024-12-21 14:25:47 +01:00
326411ca5d Correctly generate i18n messages for admin UI 2024-12-21 12:09:29 +01:00
c39c450e90 Support generating eject comments for .svg files 2024-12-20 13:22:15 +01:00
3191954dda Support generating eject comment for .properties file 2024-12-20 13:03:58 +01:00
20c6d2ea86 Bump version 2024-12-19 19:07:24 +01:00
f43544e134 ensure no diff if config hasn't changed 2024-12-19 19:06:54 +01:00
474a863708 Correctly patch the security-admin-console client so that it can be run with HMR 2024-12-18 20:57:42 +01:00
0bacdca8fe Bump version 2024-12-17 18:04:22 +01:00
f023d6bca7 Fixes windows issues #747 2024-12-17 18:04:06 +01:00
150b01f1f3 Bump version 2024-12-17 10:44:38 +01:00
2b2bb20658 #746 2024-12-17 10:44:24 +01:00
70570faed6 Bump version 2024-12-16 18:04:01 +01:00
5d3b7c9a82 Add necessary token claim to access admin in dev mode 2024-12-16 18:04:01 +01:00
95b9b12a3b Try to fix error on windows 2024-12-16 18:04:01 +01:00
0e027055cb Bump version 2024-12-15 19:48:42 +01:00
e47b002535 #744 2024-12-15 19:48:23 +01:00
8dd6dcd1fc Merge pull request #745 from keycloakify/keycloak_config_persistance
Keycloak config persistance
2024-12-15 19:45:52 +01:00
10cfa1cf41 Update default realm configs 2024-12-15 19:45:05 +01:00
3938584aeb Update default realm configs 2024-12-15 18:43:53 +01:00
163b060dc5 Additional teaks 2024-12-15 18:15:36 +01:00
67f8ae41fc Update prepare realm script 2024-12-15 17:42:45 +01:00
b6e9fe2585 Update default realm config for kc 26 2024-12-15 13:28:05 +01:00
5b83bd8fa9 Update dump realm local script 2024-12-15 13:27:49 +01:00
d0f43b6318 Add logging and debug for backup configuration process 2024-12-15 13:11:01 +01:00
df338ed6a0 Improve ordering to minimize diff 2024-12-15 12:28:09 +01:00
295994d02a Use KC_BOOTSTRAP_ADMIN_ in newer keycloak 2024-12-15 11:57:45 +01:00
f9e15f93c4 Fix spelling mistake 2024-12-15 11:49:33 +01:00
2659cf391c Fix schema validation error 2024-12-15 11:47:59 +01:00
76416ddd5b Put persisted realm configs in .keycloakify 2024-12-15 11:45:00 +01:00
8e8a0ccf54 Store https://my-theme.keycloakify.dev as a constant 2024-12-15 11:38:50 +01:00
db0ec954df Fix zod schema error 2024-12-15 11:34:41 +01:00
dc942aa5de Implement cache for fetching available docker images tags 2024-12-15 08:53:54 +01:00
029cfcb591 Fix fetching of keycloak versions 2024-12-14 18:37:54 +01:00
b1b6919395 Assuming latest supported 2024-12-14 14:44:30 +01:00
9185740d35 Keycloak config persistance implemented (to test) 2024-12-14 14:36:11 +01:00
8d59fe7b67 Change structure 2024-12-13 12:16:41 +01:00
92b505dd56 Load custom extention for logging realm change 2024-12-13 12:07:21 +01:00
c0e6661d3d Add function to dump the realm config 2024-12-13 11:31:01 +01:00
0cae2c68d8 Add utils to edit the realm 2024-12-13 09:07:11 +01:00
1e43343529 Update keycloak 26 realm default config (fmt) 2024-12-12 11:19:06 +01:00
0a74dca7c2 Prettier ignore realm default config 2024-12-12 11:16:01 +01:00
a66a373256 Update dump-keycloak-realm internal script https://github.com/keycloak/keycloak/issues/33800 2024-12-10 04:12:56 +01:00
606cf7ad02 Bump version 2024-12-09 05:08:57 +01:00
5225749c7b React 19 compat #741 2024-12-09 05:06:47 +01:00
819e3833ad Bump version 2024-12-08 19:43:00 +01:00
b0ba37fcc4 Smarter appBuild script 2024-12-08 19:42:43 +01:00
f4829b557f Bump version 2024-12-06 00:38:23 +01:00
60a9b5a693 Improve i18n api typing 2024-12-06 00:38:09 +01:00
c323b94a8c Bump version 2024-12-04 00:09:14 +01:00
4bbc0241ec Do not crash when parser can't be inferred 2024-12-04 00:04:49 +01:00
5a7dacfcdd Bump version 2024-12-02 00:41:30 +01:00
7e05e1bf0c Use random port for dev server 2024-12-02 00:41:12 +01:00
1530ca32c8 Bump version 2024-12-01 00:07:28 +01:00
ed054f131a Merge pull request #736 from keycloakify/hmr_in_start_keycloak
Implement hot module replacement for developing Account SPA and Admin UI
2024-12-01 00:01:57 +01:00
106 changed files with 6390 additions and 1984 deletions

View File

@ -1,4 +1,3 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

View File

@ -12,4 +12,5 @@ node_modules/
/sample_react_project/
/sample_custom_react_project/
/keycloakify_starter_test/
/.storybook/static/keycloak-resources/
/.storybook/static/keycloak-resources/
/src/bin/start-keycloak/*.json

View File

@ -46,7 +46,7 @@ Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyon
> 📣 **Keycloakify 26 Released**
> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/features/compiler-options/keycloakversiontargets).
## Sponsors

View File

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

View File

@ -0,0 +1,39 @@
import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive";
import { cacheDirPath } from "../shared/cacheDirPath";
import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions";
import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath";
import { existsAsync } from "../../src/bin/tools/fs.existsAsync";
import * as fs from "fs/promises";
import {
KEYCLOAKIFY_LOGGING_VERSION,
KEYCLOAKIFY_LOGIN_JAR_BASENAME
} from "../../src/bin/shared/constants";
import { join as pathJoin } from "path";
export async function downloadKeycloakifyLogging(params: { distDirPath: string }) {
const { distDirPath } = params;
const jarFilePath = pathJoin(
distDirPath,
"src",
"bin",
"start-keycloak",
KEYCLOAKIFY_LOGIN_JAR_BASENAME
);
if (await existsAsync(jarFilePath)) {
return;
}
const { archiveFilePath } = await downloadAndExtractArchive({
cacheDirPath,
fetchOptions: getProxyFetchOptions({
npmConfigGetCwd: getThisCodebaseRootDirPath()
}),
url: `https://github.com/keycloakify/keycloakify-logging/releases/download/${KEYCLOAKIFY_LOGGING_VERSION}/keycloakify-logging-${KEYCLOAKIFY_LOGGING_VERSION}.jar`,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
await fs.cp(archiveFilePath, jarFilePath);
}

View File

@ -7,6 +7,7 @@ import { createAccountV1Dir } from "./createAccountV1Dir";
import chalk from "chalk";
import { run } from "../shared/run";
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
(async () => {
console.log(chalk.cyan("Building Keycloakify..."));
@ -148,9 +149,6 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
fs.cpSync(dirBasename, destDirPath, { recursive: true });
}
await createPublicKeycloakifyDevResourcesDir();
await createAccountV1Dir();
transformCodebase({
srcDirPath: join("stories"),
destDirPath: join("dist", "stories"),
@ -163,6 +161,12 @@ import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
}
});
await createPublicKeycloakifyDevResourcesDir();
await createAccountV1Dir();
await downloadKeycloakifyLogging({
distDirPath: join(process.cwd(), "dist")
});
console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
);

View File

@ -67,7 +67,9 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
)
);
run(`npx webpack --config ${webpackConfigJsFilePath}`);
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
cwd: pathDirname(webpackConfigJsFilePath)
});
fs.readdirSync(webpackOutputDirPath)
.filter(fileBasename => !fileBasename.endsWith(".txt"))

View File

@ -1,65 +1,14 @@
import { CONTAINER_NAME } from "../src/bin/shared/constants";
import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer";
import { join as pathJoin, relative as pathRelative } from "path";
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
import { cacheDirPath } from "./shared/cacheDirPath";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
import { join as pathJoin } from "path";
import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert, is } from "tsafe/assert";
import { run } from "./shared/run";
(async () => {
{
const dCompleted = new Deferred<void>();
const child = child_process.spawn(
"docker",
[
...["exec", CONTAINER_NAME],
...["/opt/keycloak/bin/kc.sh", "export"],
...["--dir", "/tmp"],
...["--realm", "myrealm"],
...["--users", "realm_file"]
],
{ shell: true }
);
let output = "";
const onExit = (code: number | null) => {
dCompleted.reject(new Error(`Exited with code ${code}`));
};
child.on("exit", onExit);
child.stdout.on("data", data => {
const outputStr = data.toString("utf8");
if (outputStr.includes("Export finished successfully")) {
child.removeListener("exit", onExit);
child.kill();
dCompleted.resolve();
}
output += outputStr;
});
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(error.message));
console.log(output);
process.exit(1);
}
}
const keycloakMajorVersionNumber = SemVer.parse(
child_process
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
@ -68,19 +17,29 @@ import { run } from "./shared/run";
.split(":")[1]
).major;
const targetFilePath = pathRelative(
process.cwd(),
pathJoin(
__dirname,
"..",
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
)
const parsedRealmJson = await dumpContainerConfig({
buildContext: {
cacheDirPath
},
keycloakMajorVersionNumber,
realmName: "myrealm"
});
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
"realmConfig",
"defaultConfig",
`realm-kc-${keycloakMajorVersionNumber}.json`
);
run(`docker cp ${CONTAINER_NAME}:/tmp/myrealm-realm.json ${targetFilePath}`);
await writeRealmJsonFile({
parsedRealmJson,
realmJsonFilePath,
keycloakMajorVersionNumber
});
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
})();

View File

@ -45,7 +45,10 @@ const commonThirdPartyDeps = [
.replace(/"!\.\/dist\//g, '"!./');
modifiedPackageJsonContent = JSON.stringify(
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
{
...JSON.parse(modifiedPackageJsonContent),
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
},
null,
4
);

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";

View File

@ -1,68 +0,0 @@
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

@ -11,12 +11,7 @@ import {
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
import {
join as pathJoin,
relative as pathRelative,
dirname as pathDirname,
basename as pathBasename
} from "path";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
@ -67,9 +62,7 @@ export async function command(params: { buildContext: BuildContext }) {
})();
if (themeType === "admin") {
console.log(
"Use `npx keycloakify eject-file` command instead, see documentation"
);
console.log("Use `npx keycloakify own` command instead, see documentation");
process.exit(-1);
}
@ -79,85 +72,16 @@ export async function command(params: { buildContext: BuildContext }) {
(assert(buildContext.implementedThemeTypes.account.isImplemented),
buildContext.implementedThemeTypes.account.type === "Single-Page")
) {
const srcDirPath = pathJoin(
pathDirname(buildContext.packageJsonFilePath),
"node_modules",
"@keycloakify",
`keycloak-account-ui`,
"src"
);
console.log(
[
`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)))}`,
``
].join("\n")
chalk.yellow(
[
"You are implementing a Single-Page Account theme.",
"The eject-page command isn't applicable in this context"
].join("\n")
)
);
eject_entrypoint: {
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const;
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath);
if (fs.existsSync(targetFilePath)) {
break eject_entrypoint;
}
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath);
{
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx");
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
const componentName = pathBasename(kcUiTsxFileRelativePath).replace(
/.tsx$/,
""
);
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")
);
}
const routesTsxFilePath = pathRelative(
process.cwd(),
pathJoin(srcDirPath, "routes.tsx")
);
console.log(
[
`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(), themeSrcDirPath)}\``,
`then update the import of routes in ${kcUiTsxFileRelativePath}.`
].join("\n")
);
}
process.exit(0);
process.exit(1);
return;
}
@ -168,12 +92,14 @@ export async function command(params: { buildContext: BuildContext }) {
const templateValue = "Template.tsx (Layout common to every page)";
const userProfileFormFieldsValue =
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
const otherPageValue = "The page you're looking for isn't listed here";
const { value: pageIdOrComponent } = await cliSelect<
| LoginThemePageId
| AccountThemePageId
| typeof templateValue
| typeof userProfileFormFieldsValue
| typeof otherPageValue
>({
values: (() => {
switch (themeType) {
@ -181,10 +107,11 @@ export async function command(params: { buildContext: BuildContext }) {
return [
templateValue,
userProfileFormFieldsValue,
...LOGIN_THEME_PAGE_IDS
...LOGIN_THEME_PAGE_IDS,
otherPageValue
];
case "account":
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
}
assert<Equals<typeof themeType, never>>(false);
})()
@ -192,6 +119,17 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1);
});
if (pageIdOrComponent === otherPageValue) {
console.log(
[
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
].join(" ")
);
process.exit(0);
}
console.log(`${pageIdOrComponent}`);
const componentBasename = (() => {

View File

@ -1,32 +0,0 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { assert, type Equals } from "tsafe/assert";
export function copyBoilerplate(params: {
accountThemeType: "Single-Page" | "Multi-Page";
accountThemeSrcDirPath: string;
}) {
const { accountThemeType, accountThemeSrcDirPath } = params;
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"src",
(() => {
switch (accountThemeType) {
case "Single-Page":
return "single-page";
case "Multi-Page":
return "multi-page";
}
assert<Equals<typeof accountThemeType, never>>(false);
})()
),
accountThemeSrcDirPath,
{ recursive: true }
);
}

View File

@ -7,6 +7,7 @@ import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeIm
import { command as updateKcGenCommand } from "../update-kc-gen";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -50,24 +51,24 @@ export async function command(params: { buildContext: BuildContext }) {
switch (accountThemeType) {
case "Multi-Page":
{
const { initializeAccountTheme_multiPage } = await import(
"./initializeAccountTheme_multiPage"
);
await initializeAccountTheme_multiPage({
accountThemeSrcDirPath
});
}
fs.cpSync(
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"initialize-account-theme",
"multi-page-boilerplate"
),
accountThemeSrcDirPath,
{ recursive: true }
);
break;
case "Single-Page":
{
const { initializeAccountTheme_singlePage } = await import(
"./initializeAccountTheme_singlePage"
);
const { initializeSpa } = await import("../shared/initializeSpa");
await initializeAccountTheme_singlePage({
accountThemeSrcDirPath,
await initializeSpa({
themeType: "account",
buildContext
});
}

View File

@ -1,21 +0,0 @@
import { relative as pathRelative } from "path";
import chalk from "chalk";
import { copyBoilerplate } from "./copyBoilerplate";
export async function initializeAccountTheme_multiPage(params: {
accountThemeSrcDirPath: string;
}) {
const { accountThemeSrcDirPath } = params;
copyBoilerplate({
accountThemeType: "Multi-Page",
accountThemeSrcDirPath
});
console.log(
[
chalk.green("The Multi-Page account theme has been initialized."),
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`
].join("\n")
);
}

View File

@ -1,140 +0,0 @@
import { relative as pathRelative, dirname as pathDirname } from "path";
import type { BuildContext } from "../shared/buildContext";
import * as fs from "fs";
import chalk from "chalk";
import {
getLatestsSemVersionedTag,
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
} from "../shared/getLatestsSemVersionedTag";
import { SemVer } from "../tools/SemVer";
import fetch from "make-fetch-happen";
import { z } from "zod";
import { assert, type Equals, is } from "tsafe/assert";
import { id } from "tsafe/id";
import { npmInstall } from "../tools/npmInstall";
import { copyBoilerplate } from "./copyBoilerplate";
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
fetchOptions: BuildContext["fetchOptions"];
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function initializeAccountTheme_singlePage(params: {
accountThemeSrcDirPath: string;
buildContext: BuildContextLike;
}) {
const { accountThemeSrcDirPath, buildContext } = params;
const OWNER = "keycloakify";
const REPO = "keycloak-account-ui";
const [semVersionedTag] = await getLatestsSemVersionedTag({
owner: OWNER,
repo: REPO,
count: 1,
doIgnoreReleaseCandidates: false,
buildContext
});
const dependencies = await fetch(
`https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`,
buildContext.fetchOptions
)
.then(r => r.json())
.then(
(() => {
type Dependencies = {
dependencies: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zDependencies = (() => {
type TargetType = Dependencies;
const zTargetType = z.object({
dependencies: z.record(z.string()),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return o => zDependencies.parse(o);
})()
);
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = SemVer.stringify(
semVersionedTag.version
);
const parsedPackageJson = (() => {
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);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
parsedPackageJson.dependencies = {
...parsedPackageJson.dependencies,
...dependencies.dependencies
};
parsedPackageJson.devDependencies = {
...parsedPackageJson.devDependencies,
...dependencies.devDependencies
};
if (Object.keys(parsedPackageJson.devDependencies).length === 0) {
delete parsedPackageJson.devDependencies;
}
fs.writeFileSync(
buildContext.packageJsonFilePath,
JSON.stringify(parsedPackageJson, undefined, 4)
);
npmInstall({ packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath) });
copyBoilerplate({
accountThemeType: "Single-Page",
accountThemeSrcDirPath
});
console.log(
[
chalk.green(
"The Single-Page account theme has been successfully initialized."
),
`Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`,
`Dependencies added to your project's package.json: `,
chalk.bold(JSON.stringify(dependencies, null, 2))
].join("\n")
);
}

View File

@ -0,0 +1,10 @@
import { i18nBuilder } from "keycloakify/account";
import type { ThemeName } from "../kc.gen";
/** @see: https://docs.keycloakify.dev/i18n */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
type I18n = typeof ofTypeI18n;
export { useI18n, type I18n };

View File

@ -1,12 +0,0 @@
import { i18nBuilder } from "keycloakify/account";
import type { ThemeName } from "../kc.gen";
const { useI18n, ofTypeI18n } = i18nBuilder
.withThemeName<ThemeName>()
.withExtraLanguages({})
.withCustomTranslations({})
.build();
type I18n = typeof ofTypeI18n;
export { useI18n, type I18n };

View File

@ -1,7 +0,0 @@
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
import type { KcEnvName } from "../kc.gen";
export type KcContext = KcContextLike & {
themeType: "account";
properties: Record<KcEnvName, string>;
};

View File

@ -1,11 +0,0 @@
import { lazy } from "react";
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
import type { KcContext } from "./KcContext";
const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi"));
export default function KcPage(props: { kcContext: KcContext }) {
const { kcContext } = props;
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
}

View File

@ -0,0 +1,39 @@
import type { BuildContext } from "./shared/buildContext";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { initializeSpa } from "./shared/initializeSpa";
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
import { command as updateKcGenCommand } from "./update-kc-gen";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-admin-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
exitIfUncommittedChanges({
projectDirPath: buildContext.projectDirPath
});
await initializeSpa({
themeType: "admin",
buildContext
});
await updateKcGenCommand({
buildContext: {
...buildContext,
implementedThemeTypes: {
...buildContext.implementedThemeTypes,
admin: {
isImplemented: true
}
}
}
});
}

View File

@ -1,13 +1,11 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import fetch from "make-fetch-happen";
import { SemVer } from "./tools/SemVer";
import { assert } from "tsafe/assert";
import { getSupportedDockerImageTags } from "./start-keycloak/getSupportedDockerImageTags";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@ -39,40 +37,18 @@ export async function command(params: { buildContext: BuildContext }) {
console.log("Initialize with the base email theme from which version of Keycloak?");
let { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary
startingFromMajor: 17,
excludeMajorVersions: [],
doOmitPatch: false,
buildContext
});
const getUrl = (keycloakVersion: string) => {
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
};
keycloakVersion = await (async () => {
const keycloakVersionParsed = SemVer.parse(keycloakVersion);
while (true) {
const url = getUrl(SemVer.stringify(keycloakVersionParsed));
const response = await fetch(url, buildContext.fetchOptions);
if (response.ok) {
break;
}
assert(keycloakVersionParsed.patch !== 0);
keycloakVersionParsed.patch--;
}
return SemVer.stringify(keycloakVersionParsed);
})();
const { extractedDirPath } = await downloadAndExtractArchive({
url: getUrl(keycloakVersion),
url: await (async () => {
const { latestMajorTags } = await getSupportedDockerImageTags({
buildContext
});
const keycloakVersion = latestMajorTags[0];
assert(keycloakVersion !== undefined);
return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
})(),
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",

View File

@ -11,7 +11,11 @@ import * as fs from "fs";
import { join as pathJoin } from "path";
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
import {
type ThemeType,
WELL_KNOWN_DIRECTORY_BASE_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
} from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
@ -116,6 +120,7 @@ export function generateFtlFilesCodeFactory(params: {
.replace("{{themeVersion}}", buildContext.themeVersion)
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
.replace("{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}}", KEYCLOAKIFY_SPA_DEV_SERVER_PORT)
.replace(
"{{userDefinedExclusions}}",
buildContext.kcContextExclusionsFtlCode ?? ""

View File

@ -101,7 +101,7 @@ redirect_to_dev_server: {
break redirect_to_dev_server;
}
const devSeverPort = kcContext.properties.KEYCLOAKIFY_SPA_DEV_SERVER_PORT;
const devSeverPort = kcContext.properties.{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}};
if( !devSeverPort ){
break redirect_to_dev_server;
@ -115,7 +115,7 @@ redirect_to_dev_server: {
console.log(kcContext);
redirectUrl.searchParams.set("kcContext", encodeURIComponent(JSON.stringify(kcContext)) );
redirectUrl.searchParams.set("kcContext", encodeURIComponent(JSON.stringify(kcContext)));
window.location.href = redirectUrl.toString();

View File

@ -37,9 +37,10 @@ import {
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
import * as child_process from "child_process";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import propertiesParser from "properties-parser";
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
import { listInstalledModules } from "../../tools/listInstalledModules";
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_generateMessageProperties & {
@ -237,9 +238,9 @@ export async function generateResources(params: {
let languageTags: string[] | undefined = undefined;
i18n_messages_generation: {
i18n_multi_page: {
if (isSpa) {
break i18n_messages_generation;
break i18n_multi_page;
}
assert(themeType !== "admin");
@ -256,21 +257,43 @@ export async function generateResources(params: {
writeMessagePropertiesFiles;
}
bring_in_spas_messages: {
let isLegacyAccountSpa = false;
// NOTE: Eventually remove this block.
i18n_single_page_account_legacy: {
if (!isSpa) {
break bring_in_spas_messages;
break i18n_single_page_account_legacy;
}
assert(themeType !== "login");
if (themeType !== "account") {
break i18n_single_page_account_legacy;
}
const accountUiDirPath = child_process
.execSync(`npm list @keycloakify/keycloak-${themeType}-ui --parseable`, {
cwd: pathDirname(buildContext.packageJsonFilePath)
})
.toString("utf8")
.trim();
const [moduleMeta] = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName === "@keycloakify/keycloak-account-ui"
});
const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
assert(
moduleMeta !== undefined,
`@keycloakify/keycloak-account-ui is supposed to be installed`
);
{
const [majorStr] = moduleMeta.version.split(".");
if (majorStr.length === 6) {
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
// 26.0.7 it would be 260007.
break i18n_single_page_account_legacy;
} else {
// 25.0.4-rc.5 or later
isLegacyAccountSpa = true;
}
}
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
if (!fs.existsSync(messageDirPath_defaults)) {
throw new Error(
@ -278,8 +301,10 @@ export async function generateResources(params: {
);
}
isLegacyAccountSpa = true;
const messagesDirPath_dest = pathJoin(
getThemeTypeDirPath({ themeName, themeType }),
getThemeTypeDirPath({ themeName, themeType: "account" }),
"messages"
);
@ -291,7 +316,7 @@ export async function generateResources(params: {
apply_theme_changes: {
const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"account",
"messages"
);
@ -339,6 +364,165 @@ export async function generateResources(params: {
);
}
i18n_single_page: {
if (!isSpa) {
break i18n_single_page;
}
if (isLegacyAccountSpa) {
break i18n_single_page;
}
assert(themeType === "account" || themeType === "admin");
const messagesDirPath_theme = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"i18n"
);
assert(
fs.existsSync(messagesDirPath_theme),
`${messagesDirPath_theme} is supposed to exist`
);
const propertiesByLang: Record<
string,
{
base: Buffer;
override: Buffer | undefined;
overrideByThemeName: Record<string, Buffer>;
}
> = {};
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
type ParsedBasename = { lang: string } & (
| {
isOverride: false;
}
| {
isOverride: true;
themeName: string | undefined;
}
);
const parsedBasename = ((): ParsedBasename | undefined => {
const match = basename.match(/^messages_([^.]+)\.properties$/);
if (match === null) {
return undefined;
}
const discriminator = match[1];
const split = discriminator.split("_override");
if (split.length === 1) {
return {
lang: discriminator,
isOverride: false
};
}
assert(split.length === 2);
if (split[1] === "") {
return {
lang: split[0],
isOverride: true,
themeName: undefined
};
}
const match2 = split[1].match(/^_(.+)$/);
assert(match2 !== null);
return {
lang: split[0],
isOverride: true,
themeName: match2[1]
};
})();
if (parsedBasename === undefined) {
return;
}
propertiesByLang[parsedBasename.lang] ??= {
base: createObjectThatThrowsIfAccessed<Buffer>({
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
}),
override: undefined,
overrideByThemeName: {}
};
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
if (parsedBasename.isOverride === false) {
propertiesByLang[parsedBasename.lang].base = buffer;
return;
}
if (parsedBasename.themeName === undefined) {
propertiesByLang[parsedBasename.lang].override = buffer;
return;
}
propertiesByLang[parsedBasename.lang].overrideByThemeName[
parsedBasename.themeName
] = buffer;
});
languageTags = Object.keys(propertiesByLang);
writeMessagePropertiesFilesByThemeType[themeType] = ({
messageDirPath,
themeName
}) => {
if (!fs.existsSync(messageDirPath)) {
fs.mkdirSync(messageDirPath, { recursive: true });
}
Object.entries(propertiesByLang).forEach(
([lang, { base, override, overrideByThemeName }]) => {
const messages = propertiesParser.parse(base.toString("utf8"));
if (override !== undefined) {
const overrideMessages = propertiesParser.parse(
override.toString("utf8")
);
Object.entries(overrideMessages).forEach(
([key, value]) => (messages[key] = value)
);
}
if (themeName in overrideByThemeName) {
const overrideMessages = propertiesParser.parse(
overrideByThemeName[themeName].toString("utf8")
);
Object.entries(overrideMessages).forEach(
([key, value]) => (messages[key] = value)
);
}
const editor = propertiesParser.createEditor();
Object.entries(messages).forEach(([key, value]) => {
editor.set(key, value);
});
fs.writeFileSync(
pathJoin(messageDirPath, `messages_${lang}.properties`),
Buffer.from(editor.toString(), "utf8")
);
}
);
};
}
keycloak_static_resources: {
if (isSpa) {
break keycloak_static_resources;

View File

@ -5,9 +5,6 @@ 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;
@ -137,47 +134,11 @@ 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,
keycloakVersion:
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
port,
realmJsonFilePath
}
@ -230,7 +191,7 @@ program
program
.command({
name: "initialize-account-theme",
description: "Initialize the account theme."
description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
})
.task({
skip,
@ -241,6 +202,20 @@ program
}
});
program
.command({
name: "initialize-admin-theme",
description: "Initialize an Admin Console custom UI."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./initialize-admin-theme");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
program
.command({
name: "copy-keycloak-resources-to-public",
@ -273,13 +248,30 @@ program
program
.command({
name: "postinstall",
description: "Initialize all the Keycloakify UI modules installed in the project."
name: "sync-extensions",
description: [
"Synchronizes all installed Keycloakify extension modules with your project.",
"",
"Example of extension modules: '@keycloakify/keycloak-account-ui', '@keycloakify/keycloak-admin-ui', '@keycloakify/keycloak-ui-shared'",
"",
"This command ensures that:",
"- All required files from installed extensions are copied into your project.",
"- The copied files are correctly ignored by Git to help you distinguish between your custom source files",
" and those provided by the extensions.",
"- Peer dependencies declared by the extensions are automatically added to your package.json.",
"",
"You can safely run this command multiple times. It will only update the files and dependencies if needed,",
"ensuring your project stays in sync with the installed extensions.",
"",
"Typical usage:",
"- Should be run as a postinstall script of your project.",
""
].join("\n")
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./postinstall");
const { command } = await import("./sync-extensions");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
@ -287,36 +279,66 @@ program
program
.command<{
file: string;
path: string;
revert: boolean;
}>({
name: "eject-file",
name: "own",
description: [
"WARNING: Not usable yet, will be used for future features",
"Take ownership over a given file"
"Manages ownership of auto-generated files provided by Keycloakify extensions.",
"",
"This command allows you to take ownership of a specific file or directory generated",
"by an extension. Once owned, you can freely modify and version-control the file.",
"",
"You can also use the --revert flag to relinquish ownership and restore the file",
"or directory to its original auto-generated state.",
"",
"For convenience, the exact command to take ownership of any file is included as a comment",
"in the header of each extension-generated file.",
"",
"Examples:",
"$ npx keycloakify own --path admin/KcPage.tsx"
].join("\n")
})
.option({
key: "path",
name: (() => {
const long = "path";
const short = "t";
optionsKeys.push(long, short);
return { long, short };
})(),
description: [
"Specifies the relative path of the file or directory to take ownership of.",
"This path should be relative to your theme directory.",
"Example: `--path 'admin/KcPage.tsx'`"
].join(" ")
})
.option({
key: "file",
key: "revert",
name: (() => {
const name = "file";
const long = "revert";
const short = "r";
optionsKeys.push(name);
optionsKeys.push(long, short);
return name;
return { long, short };
})(),
description: [
"Relative path of the file relative to the directory of your keycloak theme source",
"Example `--file src/login/page/Login.tsx`"
].join(" ")
"Restores a file or directory to its original auto-generated state,",
"removing your ownership claim and reverting any modifications."
].join(" "),
defaultValue: false
})
.task({
skip,
handler: async ({ projectDirPath, file }) => {
const { command } = await import("./eject-file");
handler: async ({ projectDirPath, path, revert }) => {
const { command } = await import("./own");
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { file }
cliCommandOptions: { path, isRevert: revert }
});
}
});

208
src/bin/own.ts Normal file
View File

@ -0,0 +1,208 @@
import type { BuildContext } from "./shared/buildContext";
import { getExtensionModuleFileSourceCodeReadyToBeCopied } from "./sync-extensions/getExtensionModuleFileSourceCodeReadyToBeCopied";
import type { ExtensionModuleMeta } from "./sync-extensions/extensionModuleMeta";
import { command as command_syncExtensions } from "./sync-extensions/sync-extension";
import {
readManagedGitignoreFile,
writeManagedGitignoreFile
} from "./sync-extensions/managedGitignoreFile";
import { getExtensionModuleMetas } from "./sync-extensions/extensionModuleMeta";
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
import * as fsPr from "fs/promises";
import { isInside } from "./tools/isInside";
import chalk from "chalk";
export async function command(params: {
buildContext: BuildContext;
cliCommandOptions: {
path: string;
isRevert: boolean;
};
}) {
const { buildContext, cliCommandOptions } = params;
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
const { targetFileRelativePathsByExtensionModuleMeta } = await (async () => {
const fileOrDirectoryRelativePath = pathRelative(
buildContext.themeSrcDirPath,
getAbsoluteAndInOsFormatPath({
cwd: buildContext.themeSrcDirPath,
pathIsh: cliCommandOptions.path
})
);
const arr = extensionModuleMetas
.map(extensionModuleMeta => ({
extensionModuleMeta,
fileRelativePaths: extensionModuleMeta.files
.map(({ fileRelativePath }) => fileRelativePath)
.filter(
fileRelativePath =>
fileRelativePath === fileOrDirectoryRelativePath ||
isInside({
dirPath: fileOrDirectoryRelativePath,
filePath: fileRelativePath
})
)
}))
.filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0);
const targetFileRelativePathsByExtensionModuleMeta = new Map<
ExtensionModuleMeta,
string[]
>();
for (const { extensionModuleMeta, fileRelativePaths } of arr) {
targetFileRelativePathsByExtensionModuleMeta.set(
extensionModuleMeta,
fileRelativePaths
);
}
return { targetFileRelativePathsByExtensionModuleMeta };
})();
if (targetFileRelativePathsByExtensionModuleMeta.size === 0) {
console.log(
chalk.yellow(
"There is no Keycloakify extension modules files matching the provided path."
)
);
process.exit(1);
}
const { ownedFilesRelativePaths: ownedFilesRelativePaths_current } =
await readManagedGitignoreFile({
buildContext
});
await (cliCommandOptions.isRevert ? command_revert : command_own)({
extensionModuleMetas,
targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current,
buildContext
});
}
type Params_subcommands = {
extensionModuleMetas: ExtensionModuleMeta[];
targetFileRelativePathsByExtensionModuleMeta: Map<ExtensionModuleMeta, string[]>;
ownedFilesRelativePaths_current: string[];
buildContext: BuildContext;
};
async function command_own(params: Params_subcommands) {
const {
extensionModuleMetas,
targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current,
buildContext
} = params;
await writeManagedGitignoreFile({
buildContext,
extensionModuleMetas,
ownedFilesRelativePaths: [
...ownedFilesRelativePaths_current,
...Array.from(targetFileRelativePathsByExtensionModuleMeta.values())
.flat()
.filter(
fileRelativePath =>
!ownedFilesRelativePaths_current.includes(fileRelativePath)
)
]
});
const writeActions: (() => Promise<void>)[] = [];
for (const [
extensionModuleMeta,
fileRelativePaths
] of targetFileRelativePathsByExtensionModuleMeta.entries()) {
const extensionModuleDirPath = await getInstalledModuleDirPath({
moduleName: extensionModuleMeta.moduleName,
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});
for (const fileRelativePath of fileRelativePaths) {
if (ownedFilesRelativePaths_current.includes(fileRelativePath)) {
console.log(
chalk.grey(`You already have ownership over '${fileRelativePath}'.`)
);
continue;
}
writeActions.push(async () => {
const sourceCode = await getExtensionModuleFileSourceCodeReadyToBeCopied({
buildContext,
fileRelativePath,
isOwnershipAction: true,
extensionModuleName: extensionModuleMeta.moduleName,
extensionModuleDirPath,
extensionModuleVersion: extensionModuleMeta.version
});
await fsPr.writeFile(
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
sourceCode
);
console.log(chalk.green(`Ownership over '${fileRelativePath}' claimed.`));
});
}
}
if (writeActions.length === 0) {
console.log(chalk.yellow("No new file claimed."));
return;
}
await Promise.all(writeActions.map(action => action()));
}
async function command_revert(params: Params_subcommands) {
const {
extensionModuleMetas,
targetFileRelativePathsByExtensionModuleMeta,
ownedFilesRelativePaths_current,
buildContext
} = params;
const ownedFilesRelativePaths_toRemove = Array.from(
targetFileRelativePathsByExtensionModuleMeta.values()
)
.flat()
.filter(fileRelativePath => {
if (!ownedFilesRelativePaths_current.includes(fileRelativePath)) {
console.log(
chalk.grey(`Ownership over '${fileRelativePath}' wasn't claimed.`)
);
return false;
}
console.log(
chalk.green(`Ownership over '${fileRelativePath}' relinquished.`)
);
return true;
});
if (ownedFilesRelativePaths_toRemove.length === 0) {
console.log(chalk.yellow("No file relinquished."));
return;
}
await writeManagedGitignoreFile({
buildContext,
extensionModuleMetas,
ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter(
fileRelativePath =>
!ownedFilesRelativePaths_toRemove.includes(fileRelativePath)
)
});
await command_syncExtensions({ buildContext });
}

View File

@ -1,82 +0,0 @@
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

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

View File

@ -81,3 +81,9 @@ export const CUSTOM_HANDLER_ENV_NAMES = {
export const KEYCLOAK_THEME = "keycloak-theme";
export const KEYCLOAKIFY_SPA_DEV_SERVER_PORT = "KEYCLOAKIFY_SPA_DEV_SERVER_PORT";
export const KEYCLOAKIFY_LOGGING_VERSION = "1.0.3";
export const KEYCLOAKIFY_LOGIN_JAR_BASENAME = `keycloakify-logging-${KEYCLOAKIFY_LOGGING_VERSION}.jar`;
export const TEST_APP_URL = "https://my-theme.keycloakify.dev";

View File

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

View File

@ -1,201 +0,0 @@
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import type { ReturnType } from "tsafe";
import type { Param0 } from "tsafe";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs";
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { SemVer } from "../tools/SemVer";
import { same } from "evt/tools/inDepth/same";
import type { BuildContext } from "./buildContext";
import fetch from "make-fetch-happen";
type GetLatestsSemVersionedTag = ReturnType<
typeof getLatestsSemVersionedTagFactory
>["getLatestsSemVersionedTag"];
type Params = Param0<GetLatestsSemVersionedTag>;
type R = ReturnType<GetLatestsSemVersionedTag>;
let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined =
undefined;
const CACHE_VERSION = 1;
type Cache = {
version: typeof CACHE_VERSION;
entries: {
time: number;
params: Params;
result: R;
}[];
};
export type BuildContextLike = {
cacheDirPath: string;
fetchOptions: BuildContext["fetchOptions"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getLatestsSemVersionedTag({
buildContext,
...params
}: Params & {
buildContext: BuildContextLike;
}): Promise<R> {
const cacheFilePath = pathJoin(
buildContext.cacheDirPath,
"latest-sem-versioned-tags.json"
);
const cacheLookupResult = (() => {
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
hasCachedResult: false as const,
currentCache: {
version: CACHE_VERSION,
entries: currentCacheEntries
}
});
if (!fs.existsSync(cacheFilePath)) {
return getResult_currentCache([]);
}
let cache_json;
try {
cache_json = fs.readFileSync(cacheFilePath).toString("utf8");
} catch {
return getResult_currentCache([]);
}
let cache_json_parsed: unknown;
try {
cache_json_parsed = JSON.parse(cache_json);
} catch {
return getResult_currentCache([]);
}
const zSemVer = (() => {
type TargetType = SemVer;
const zTargetType = z.object({
major: z.number(),
minor: z.number(),
patch: z.number(),
rc: z.number().optional(),
parsedFrom: z.string()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
const zCache = (() => {
type TargetType = Cache;
const zTargetType = z.object({
version: z.literal(CACHE_VERSION),
entries: z.array(
z.object({
time: z.number(),
params: z.object({
owner: z.string(),
repo: z.string(),
count: z.number(),
doIgnoreReleaseCandidates: z.boolean()
}),
result: z.array(
z.object({
tag: z.string(),
version: zSemVer
})
)
})
)
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
let cache: Cache;
try {
cache = zCache.parse(cache_json_parsed);
} catch {
return getResult_currentCache([]);
}
const cacheEntry = cache.entries.find(e => same(e.params, params));
if (cacheEntry === undefined) {
return getResult_currentCache(cache.entries);
}
if (Date.now() - cacheEntry.time > 3_600_000) {
return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry));
}
return {
hasCachedResult: true as const,
cachedResult: cacheEntry.result
};
})();
if (cacheLookupResult.hasCachedResult) {
return cacheLookupResult.cachedResult;
}
const { currentCache } = cacheLookupResult;
getLatestsSemVersionedTag_stateless ??= (() => {
const octokit = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit({
...(githubToken === undefined ? {} : { auth: githubToken }),
request: {
fetch: (url: string, options?: any) =>
fetch(url, {
...options,
...buildContext.fetchOptions
})
}
});
return octokit;
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
octokit
});
return getLatestsSemVersionedTag;
})();
const result = await getLatestsSemVersionedTag_stateless(params);
currentCache.entries.push({
time: Date.now(),
params,
result
});
{
const dirPath = pathDirname(cacheFilePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2));
return result;
}

View File

@ -0,0 +1,70 @@
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../buildContext";
export type BuildContextLike = {
projectDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function addSyncExtensionsToPostinstallScript(params: {
parsedPackageJson: { scripts?: Record<string, string | undefined> };
buildContext: BuildContextLike;
}) {
const { parsedPackageJson, buildContext } = params;
const cmd_base = "keycloakify sync-extensions";
const projectCliOptionValue = (() => {
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);
const relativePath = pathRelative(
packageJsonDirPath,
buildContext.projectDirPath
);
if (relativePath === "") {
return undefined;
}
return relativePath.split(pathSep).join("/");
})();
const generateCmd = (params: { cmd_preexisting: string | undefined }) => {
const { cmd_preexisting } = params;
let cmd = cmd_preexisting === undefined ? "" : `${cmd_preexisting} && `;
cmd += cmd_base;
if (projectCliOptionValue !== undefined) {
cmd += ` -p ${projectCliOptionValue}`;
}
return cmd;
};
{
const scripts = (parsedPackageJson.scripts ??= {});
for (const scriptName of ["postinstall", "prepare"]) {
const cmd_preexisting = scripts[scriptName];
if (cmd_preexisting === undefined) {
continue;
}
if (!cmd_preexisting.includes(cmd_base)) {
scripts[scriptName] = generateCmd({ cmd_preexisting });
return;
}
}
}
parsedPackageJson.scripts = {
postinstall: generateCmd({ cmd_preexisting: undefined }),
...parsedPackageJson.scripts
};
}

View File

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

View File

@ -0,0 +1,149 @@
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
import type { BuildContext } from "../buildContext";
import * as fs from "fs";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import {
addSyncExtensionsToPostinstallScript,
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
} from "./addSyncExtensionsToPostinstallScript";
import { getIsPrettierAvailable, runPrettier } from "../../tools/runPrettier";
import { npmInstall } from "../../tools/npmInstall";
import * as child_process from "child_process";
import { z } from "zod";
import chalk from "chalk";
export type BuildContextLike = BuildContextLike_addSyncExtensionsToPostinstallScript & {
themeSrcDirPath: string;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function initializeSpa(params: {
themeType: "account" | "admin";
buildContext: BuildContextLike;
}) {
const { themeType, buildContext } = params;
{
const themeTypeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
if (
fs.existsSync(themeTypeSrcDirPath) &&
fs.readdirSync(themeTypeSrcDirPath).length > 0
) {
console.warn(
chalk.red(
`There is already a ${pathRelative(
process.cwd(),
themeTypeSrcDirPath
)} directory in your project. Aborting.`
)
);
process.exit(-1);
}
}
const parsedPackageJson = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string | undefined>;
dependencies?: Record<string, string | undefined>;
devDependencies?: Record<string, string | undefined>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
zParsedPackageJson.parse(parsedPackageJson);
assert(is<ParsedPackageJson>(parsedPackageJson));
return parsedPackageJson;
})();
addSyncExtensionsToPostinstallScript({
parsedPackageJson,
buildContext
});
const uiSharedMajor = (() => {
const dependencies = {
...parsedPackageJson.devDependencies,
...parsedPackageJson.dependencies
};
const version = dependencies["@keycloakify/keycloak-ui-shared"];
if (version === undefined) {
return undefined;
}
const match = version.match(/^[^~]?(\d+)\./);
if (match === null) {
return undefined;
}
return match[1];
})();
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
const version = (
JSON.parse(
child_process
.execSync(`npm show ${moduleName} versions --json`)
.toString("utf8")
.trim()
) as string[]
)
.reverse()
.filter(version => !version.includes("-"))
.find(version =>
uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`)
);
assert(version !== undefined);
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
if (parsedPackageJson.devDependencies !== undefined) {
delete parsedPackageJson.devDependencies[moduleName];
}
{
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: buildContext.packageJsonFilePath
});
}
fs.writeFileSync(
buildContext.packageJsonFilePath,
Buffer.from(sourceCode, "utf8")
);
}
await npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});
}

View File

@ -1,72 +0,0 @@
import {
getLatestsSemVersionedTag,
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
} from "./getLatestsSemVersionedTag";
import cliSelect from "cli-select";
import { assert } from "tsafe/assert";
import { SemVer } from "../tools/SemVer";
import type { BuildContext } from "./buildContext";
export type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function promptKeycloakVersion(params: {
startingFromMajor: number | undefined;
excludeMajorVersions: number[];
doOmitPatch: boolean;
buildContext: BuildContextLike;
}) {
const { startingFromMajor, excludeMajorVersions, doOmitPatch, buildContext } = params;
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
const semVersionedTags = await getLatestsSemVersionedTag({
count: 50,
owner: "keycloak",
repo: "keycloak",
doIgnoreReleaseCandidates: true,
buildContext
});
semVersionedTags.forEach(semVersionedTag => {
if (
startingFromMajor !== undefined &&
semVersionedTag.version.major < startingFromMajor
) {
return;
}
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
return;
}
const currentSemVersionedTag = semVersionedTagByMajor.get(
semVersionedTag.version.major
);
if (
currentSemVersionedTag !== undefined &&
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
) {
return;
}
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
});
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
({ version }) =>
`${version.major}.${version.minor}${doOmitPatch ? "" : `.${version.patch}`}`
);
const { value } = await cliSelect<string>({
values: lastMajorVersions
}).catch(() => {
process.exit(-1);
});
const keycloakVersion = value.split(" ")[0];
return { keycloakVersion };
}

View File

@ -1,18 +1,17 @@
import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert";
import { assert, is, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { BuildContext } from "../shared/buildContext";
import chalk from "chalk";
import { sep as pathSep, join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import * as fs from "fs";
import { dirname as pathDirname, relative as pathRelative } from "path";
import { z } from "zod";
export type BuildContextLike = {
projectDirPath: string;
keycloakifyBuildDirPath: string;
bundler: BuildContext["bundler"];
projectBuildDirPath: string;
packageJsonFilePath: string;
};
@ -23,58 +22,36 @@ export async function appBuild(params: {
}): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params;
switch (buildContext.bundler) {
case "vite":
return appBuild_vite({ buildContext });
case "webpack":
return appBuild_webpack({ buildContext });
}
}
const { parsedPackageJson } = (() => {
type ParsedPackageJson = {
scripts?: Record<string, string>;
};
async function appBuild_vite(params: {
buildContext: BuildContextLike;
}): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params;
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
assert(buildContext.bundler === "vite");
const zTargetType = z.object({
scripts: z.record(z.string()).optional()
});
const dIsSuccess = new Deferred<boolean>();
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
console.log(chalk.blue("$ npx vite build"));
return id<z.ZodType<TargetType>>(zTargetType);
})();
const parsedPackageJson = JSON.parse(
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
);
const child = child_process.spawn("npx", ["vite", "build"], {
cwd: buildContext.projectDirPath,
shell: true
});
zParsedPackageJson.parse(parsedPackageJson);
child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) {
return;
}
assert(is<ParsedPackageJson>(parsedPackageJson));
process.stdout.write(data);
});
return { parsedPackageJson };
})();
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => dIsSuccess.resolve(code === 0));
const isSuccess = await dIsSuccess.pr;
return { isAppBuildSuccess: isSuccess };
}
async function appBuild_webpack(params: {
buildContext: BuildContextLike;
}): Promise<{ isAppBuildSuccess: boolean }> {
const { buildContext } = params;
assert(buildContext.bundler === "webpack");
const entries = Object.entries(
(JSON.parse(fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8"))
.scripts ?? {}) as Record<string, string>
).filter(([, scriptCommand]) => scriptCommand.includes("keycloakify build"));
const entries = Object.entries(parsedPackageJson.scripts ?? {}).filter(
([, scriptCommand]) => scriptCommand.includes("keycloakify build")
);
if (entries.length === 0) {
console.log(
@ -127,6 +104,76 @@ async function appBuild_webpack(params: {
process.exit(-1);
}
common_case: {
if (appBuildSubCommands.length !== 1) {
break common_case;
}
const [appBuildSubCommand] = appBuildSubCommands;
const isNpmRunBuild = (() => {
for (const packageManager of ["npm", "yarn", "pnpm", "bun", "deno"]) {
for (const doUseRun of [true, false]) {
if (
`${packageManager}${doUseRun ? " run " : " "}build` ===
appBuildSubCommand
) {
return true;
}
}
}
return false;
})();
if (!isNpmRunBuild) {
break common_case;
}
const { scripts } = parsedPackageJson;
assert(scripts !== undefined);
const buildCmd = scripts.build;
if (buildCmd !== "tsc && vite build") {
break common_case;
}
if (scripts.prebuild !== undefined) {
break common_case;
}
if (scripts.postbuild !== undefined) {
break common_case;
}
const dIsSuccess = new Deferred<boolean>();
console.log(chalk.blue("$ npx vite build"));
const child = child_process.spawn("npx", ["vite", "build"], {
cwd: buildContext.projectDirPath,
shell: true
});
child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) {
return;
}
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", code => dIsSuccess.resolve(code === 0));
const isSuccess = await dIsSuccess.pr;
return { isAppBuildSuccess: isSuccess };
}
let commandCwd = pathDirname(buildContext.packageJsonFilePath);
for (const subCommand of appBuildSubCommands) {

View File

@ -0,0 +1,267 @@
import fetch from "make-fetch-happen";
import type { BuildContext } from "../shared/buildContext";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import { z } from "zod";
import { SemVer } from "../tools/SemVer";
import { exclude } from "tsafe/exclude";
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs/promises";
import { existsAsync } from "../tools/fs.existsAsync";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import type { ReturnType } from "tsafe";
export type BuildContextLike = {
fetchOptions: BuildContext["fetchOptions"];
cacheDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>;
export async function getSupportedDockerImageTags(params: {
buildContext: BuildContextLike;
}): Promise<{
allSupportedTags: string[];
latestMajorTags: string[];
}> {
const { buildContext } = params;
{
const result = await getCachedValue({ cacheDirPath: buildContext.cacheDirPath });
if (result !== undefined) {
return result;
}
}
const tags_queryResponse: string[] = [];
await (async function callee(url: string) {
const r = await fetch(url, buildContext.fetchOptions);
await Promise.all([
(async () => {
tags_queryResponse.push(
...z
.object({
tags: z.array(z.string())
})
.parse(await r.json()).tags
);
})(),
(async () => {
const link = r.headers.get("link");
if (link === null) {
return;
}
const split = link.split(";").map(s => s.trim());
assert(split.length === 2);
assert(split[1] === 'rel="next"');
const match = split[0].match(/^<(.+)>$/);
assert(match !== null);
const nextUrl = new URL(url).origin + match[1];
await callee(nextUrl);
})()
]);
})("https://quay.io/v2/keycloak/keycloak/tags/list");
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
const allSupportedTags_withVersion = tags_queryResponse
.map(tag => ({
tag,
version: (() => {
if (tag.includes("-")) {
return undefined;
}
let version: SemVer;
try {
version = SemVer.parse(tag);
} catch {
return undefined;
}
if (tag.split(".").length !== 3) {
return undefined;
}
if (!supportedKeycloakMajorVersions.includes(version.major)) {
return undefined;
}
return version;
})()
}))
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
.filter(exclude(undefined))
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
const latestTagByMajor: Record<number, SemVer | undefined> = {};
for (const { version } of allSupportedTags_withVersion) {
const version_current = latestTagByMajor[version.major];
if (
version_current === undefined ||
SemVer.compare(version_current, version) === -1
) {
latestTagByMajor[version.major] = version;
}
}
const latestMajorTags = Object.entries(latestTagByMajor)
.sort(([a], [b]) => parseInt(b) - parseInt(a))
.map(([, version]) => version)
.map(version => {
assert(version !== undefined);
if (!supportedKeycloakMajorVersions.includes(version.major)) {
return undefined;
}
return SemVer.stringify(version);
})
.filter(exclude(undefined));
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
const result = {
latestMajorTags,
allSupportedTags
};
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
return result;
}
const { getCachedValue, setCachedValue } = (() => {
type Result = ReturnType<typeof getSupportedDockerImageTags>;
const zResult = (() => {
type TargetType = Result;
const zTargetType = z.object({
allSupportedTags: z.array(z.string()),
latestMajorTags: z.array(z.string())
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
type Cache = {
keycloakifyVersion: string;
time: number;
result: Result;
};
const zCache = (() => {
type TargetType = Cache;
const zTargetType = z.object({
keycloakifyVersion: z.string(),
time: z.number(),
result: zResult
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();
let inMemoryCachedResult: Cache["result"] | undefined = undefined;
function getCacheFilePath(params: { cacheDirPath: string }) {
const { cacheDirPath } = params;
return pathJoin(cacheDirPath, "supportedDockerImageTags.json");
}
async function getCachedValue(params: { cacheDirPath: string }) {
const { cacheDirPath } = params;
if (inMemoryCachedResult !== undefined) {
return inMemoryCachedResult;
}
const cacheFilePath = getCacheFilePath({ cacheDirPath });
if (!(await existsAsync(cacheFilePath))) {
return undefined;
}
let cache: Cache | undefined;
try {
cache = zCache.parse(JSON.parse(await fs.readFile(cacheFilePath, "utf8")));
} catch {
return undefined;
}
if (cache.keycloakifyVersion !== readThisNpmPackageVersion()) {
return undefined;
}
if (Date.now() - cache.time > 3_600 * 24) {
return undefined;
}
inMemoryCachedResult = cache.result;
return cache.result;
}
async function setCachedValue(params: {
cacheDirPath: string;
result: Cache["result"];
}) {
const { cacheDirPath, result } = params;
inMemoryCachedResult = result;
const cacheFilePath = getCacheFilePath({ cacheDirPath });
{
const dirPath = pathDirname(cacheFilePath);
if (!(await existsAsync(dirPath))) {
await fs.mkdir(dirPath, { recursive: true });
}
}
await fs.writeFile(
cacheFilePath,
JSON.stringify(
zCache.parse({
keycloakifyVersion: readThisNpmPackageVersion(),
time: Date.now(),
result
}),
null,
2
)
);
}
return {
getCachedValue,
setCachedValue
};
})();

View File

@ -0,0 +1,118 @@
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
export type ParsedRealmJson = {
realm: string;
loginTheme?: string;
accountTheme?: string;
adminTheme?: string;
emailTheme?: string;
eventsListeners: string[];
users: {
id: string;
email: string;
username: string;
credentials: {
type: string /* "password" or something else */;
}[];
clientRoles?: Record<string, string[]>;
}[];
roles: {
client: Record<
string,
{
name: string;
containerId: string; // client id
}[]
>;
};
clients: {
id: string;
clientId: string; // example: realm-management
baseUrl?: string;
redirectUris?: string[];
webOrigins?: string[];
attributes?: {
"post.logout.redirect.uris"?: string;
};
protocol?: string;
protocolMappers?: {
id: string;
name: string;
protocol: string; // "openid-connect" or something else
protocolMapper: string; // "oidc-hardcoded-claim-mapper" or something else
consentRequired: boolean;
config?: Record<string, string>;
}[];
}[];
};
export const zParsedRealmJson = (() => {
type TargetType = ParsedRealmJson;
const zTargetType = z.object({
realm: z.string(),
loginTheme: z.string().optional(),
accountTheme: z.string().optional(),
adminTheme: z.string().optional(),
emailTheme: z.string().optional(),
eventsListeners: z.array(z.string()),
users: z.array(
z.object({
id: z.string(),
email: z.string(),
username: z.string(),
credentials: z.array(
z.object({
type: z.string()
})
),
clientRoles: z.record(z.array(z.string())).optional()
})
),
roles: z.object({
client: z.record(
z.array(
z.object({
name: z.string(),
containerId: z.string()
})
)
)
}),
clients: z.array(
z.object({
id: z.string(),
clientId: z.string(),
baseUrl: z.string().optional(),
redirectUris: z.array(z.string()).optional(),
webOrigins: z.array(z.string()).optional(),
attributes: z
.object({
"post.logout.redirect.uris": z.string().optional()
})
.optional(),
protocol: z.string().optional(),
protocolMappers: z
.array(
z.object({
id: z.string(),
name: z.string(),
protocol: z.string(),
protocolMapper: z.string(),
consentRequired: z.boolean(),
config: z.record(z.string()).optional()
})
)
.optional()
})
)
});
type InferredType = z.infer<typeof zTargetType>;
assert<Equals<TargetType, InferredType>>;
return id<z.ZodType<TargetType>>(zTargetType);
})();

View File

@ -0,0 +1,3 @@
export type { ParsedRealmJson } from "./ParsedRealmJson";
export { readRealmJsonFile } from "./readRealmJsonFile";
export { writeRealmJsonFile } from "./writeRealmJsonFile";

View File

@ -0,0 +1,20 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import * as fs from "fs";
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
export function readRealmJsonFile(params: {
realmJsonFilePath: string;
}): ParsedRealmJson {
const { realmJsonFilePath } = params;
const parsedRealmJson = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
) as unknown;
zParsedRealmJson.parse(parsedRealmJson);
assert(is<ParsedRealmJson>(parsedRealmJson));
return parsedRealmJson;
}

View File

@ -0,0 +1,29 @@
import * as fsPr from "fs/promises";
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
import { canonicalStringify } from "../../../tools/canonicalStringify";
import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "../defaultConfig";
export async function writeRealmJsonFile(params: {
realmJsonFilePath: string;
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
}): Promise<void> {
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
let sourceCode = canonicalStringify({
data: parsedRealmJson,
referenceData: getDefaultConfig({
keycloakMajorVersionNumber
})
});
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode: sourceCode,
filePath: realmJsonFilePath
});
}
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
}

View File

@ -0,0 +1,74 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
const { keycloakMajorVersionNumber } = params;
return pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
"realmConfig",
"defaultConfig",
`realm-kc-${keycloakMajorVersionNumber}.json`
);
}
export const { getSupportedKeycloakMajorVersions } = (() => {
let cache: number[] | undefined = undefined;
function getSupportedKeycloakMajorVersions(): number[] {
if (cache !== undefined) {
return cache;
}
cache = fs
.readdirSync(
pathDirname(
getDefaultRealmJsonFilePath({ keycloakMajorVersionNumber: 0 })
)
)
.map(fileBasename => {
const match = fileBasename.match(/^realm-kc-(\d+)\.json$/);
if (match === null) {
return undefined;
}
const n = parseInt(match[1]);
assert(!isNaN(n));
return n;
})
.filter(exclude(undefined))
.sort((a, b) => b - a);
return cache;
}
return { getSupportedKeycloakMajorVersions };
})();
export function getDefaultConfig(params: {
keycloakMajorVersionNumber: number;
}): ParsedRealmJson {
const { keycloakMajorVersionNumber } = params;
assert(
getSupportedKeycloakMajorVersions().includes(keycloakMajorVersionNumber),
`We do not have a default config for Keycloak ${keycloakMajorVersionNumber}`
);
return readRealmJsonFile({
realmJsonFilePath: getDefaultRealmJsonFilePath({
keycloakMajorVersionNumber
})
});
}

View File

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

View File

@ -73,7 +73,7 @@
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["delete-account", "view-profile", "manage-account"]
"account": ["view-profile", "manage-account", "delete-account"]
}
},
"clientRole": false,
@ -398,6 +398,26 @@
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
"webAuthnPolicyRpId": "",
"webAuthnPolicyAttestationConveyancePreference": "not specified",
"webAuthnPolicyAuthenticatorAttachment": "not specified",
"webAuthnPolicyRequireResidentKey": "not specified",
"webAuthnPolicyUserVerificationRequirement": "not specified",
"webAuthnPolicyCreateTimeout": 0,
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
"webAuthnPolicyAcceptableAaguids": [],
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
"webAuthnPolicyPasswordlessRpId": "",
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
"webAuthnPolicyPasswordlessCreateTimeout": 0,
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
"users": [
{
"id": "00a62e75-bcc1-419a-a292-63ee5d161ed3",
@ -422,30 +442,43 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"create-client",
"view-identity-providers",
"manage-realm",
"query-groups",
"manage-clients",
"query-users",
"realm-admin",
"view-authorization",
"view-events",
"view-clients",
"view-realm",
"manage-events",
"query-realms",
"query-clients",
"manage-identity-providers",
"manage-users",
"view-users",
"impersonation",
"manage-authorization"
],
"broker": ["read-token"],
"account": [
"view-profile",
"manage-account-links",
"view-applications",
"manage-consent",
"delete-account",
"manage-account",
"view-consent"
]
},
"notBefore": 0,
"groups": []
}
],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
"webAuthnPolicyRpId": "",
"webAuthnPolicyAttestationConveyancePreference": "not specified",
"webAuthnPolicyAuthenticatorAttachment": "not specified",
"webAuthnPolicyRequireResidentKey": "not specified",
"webAuthnPolicyUserVerificationRequirement": "not specified",
"webAuthnPolicyCreateTimeout": 0,
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
"webAuthnPolicyAcceptableAaguids": [],
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
"webAuthnPolicyPasswordlessRpId": "",
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
"webAuthnPolicyPasswordlessCreateTimeout": 0,
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
"scopeMappings": [
{
"clientScope": "offline_access",
@ -505,8 +538,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -518,6 +555,7 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256"
},
"authenticationFlowBindingOverrides": {},
@ -636,7 +674,7 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false"
@ -694,8 +732,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -707,12 +749,31 @@
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -757,7 +818,8 @@
"consentRequired": false,
"config": {
"id.token.claim": "true",
"access.token.claim": "true"
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
@ -1205,6 +1267,7 @@
"consentRequired": false,
"config": {
"multivalued": "true",
"userinfo.token.claim": "true",
"user.attribute": "foo",
"id.token.claim": "true",
"access.token.claim": "true",
@ -1271,11 +1334,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1291,14 +1354,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-property-mapper",
"saml-role-list-mapper"
]
}
},
@ -1347,14 +1410,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper"
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},
@ -1394,6 +1457,12 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEpAIBAAKCAQEA+VQAcuaRivrzLVI8H/tt8PKbtRznTQKmmxOdLRR37leY/ph7sFnEmZt6K02Rvut7R0dxUFtTdiEHUKxhyM8CADMznGUjDYj/EXQzLfZ3LEwbwmR39zp+fZL/H24UDO03zt23Ov9C8Aly0ufXZ1Ic1c33KW6UtUEK/3M52pU8Y0daWdjx7nBj1eRlzWfVG+BYotTTWEnFJuEoZPFQMiXqeA5ob1zZdXjL5JDuGEiBsYjtiiaKbKL5545+FmEBnoCmWXqGu0qWxI2TzvV2dohxfl5KjNzRoKt40ydraiVk5rtBpoNDpeEApuphbokH5dJVwJ5cvWu1CSTnYPW2jXeG4wIDAQABAoIBAQDHV6AcPbhz8/xlafBkabQXBwHzJi7QZaQrLN1n44uX5jWOqP+LmdoULjjZUmWKzd98t+QjKUFrmzCsEYcE9G1XF5jWHA6Qjc3ReKRKxVm28wrmu0knQ39KizKrQGmLhEYwgRg0dU5heExzz6VrGD2xu8E3QRBocp6GauwAlXz4qcnTPHOl8OBPeDHAc0RUdaL5+jRLgKQzf9nnnKB19imBKP++zwrwFrkOZti2ZPs1I7j/ym27mHUbi8TDI2VepDX4QwjjC5a+v3vTsVAGE+1tUAZtqpxpIP9hiUkLH3ajyvp3typhnmZHklqsSZdwtRcK94WiMzL3TkiY70y8abMhAoGBAP8I4EQRXxcKfBn23eaRw8Cd4PFrOouz4zFbYLrBODsvXfku/jnQOMFD0If4IzT6y0FGgBd+t/yqnFJi98oZOKm3P8w+NZBXTbFLH8rgmsElXyS0+9LVMjVa7+UlqZB1eRZbUeLREp03Fsz1y2rflnoWgUnpDIlyhmJqGhCsJdebAoGBAPpFmJ9P42mUTeDWpCyCxgg0zpp6rlpAP8StqZkcvr7kYjhbWrJfJuxrTXtzTTA1zZ59L9EvEAxuug/gl9BkuZ11Uzg8ZLOr4gSuAJZlAORaxJlcoylmNMYIL1fP/K0dxhdO0eHZOpPVpBmGctgev2HBtWp9ZwzQ3DddKimZfNZZAoGAfNOOWSKbhT6HgXnYIHtl8YgUynUuYaR5ZfYQwTfDWwyTFVzP5+IndUjI71Qff1XlWBy2o0lNqmijPJveJlfz6PWdT01/kBd7GnTnqbgHZtPw3pmKzCW3fm/1DRZDCUbGLpAh4z9rufF1wnnnx3aKQ1VykId1sGySo+bEvTZVC1MCgYAlv6uWk/ksKpdYi2d14z+1aymieVClAj3cD4meM4y9xDrgXz8d2mZHkKO+NBT3aZYbCqzUs3GLPoRH8stTPm4UxuaHe+yAgTN1Gz2xcYih6OLwct2VV/oryH5Dk3Z8Mhp314amtxozxCydQP8/g9vABfS0HDgX4cTlgOLkJWeD+QKBgQDuRtsstQ4Q3yK44himPi1JQMMvbYAqyGgRxWH8G1Kr41DV2sQ4wt9CbYxeh6RwMsE+YYNMkTAw1kksUTugWdcDnYpcSVG7xHLJk8WMti0WTqI/7KlkoRehXXv18WJNEXaCr5mJTtJL9wuQcd8nhkEDrrCZubZiJzX9IDnEqZc4Mg=="
],
"certificate": [
"MIICnTCCAYUCBgGTy58etTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPlUAHLmkYr68y1SPB/7bfDym7Uc500CppsTnS0Ud+5XmP6Ye7BZxJmbeitNkb7re0dHcVBbU3YhB1CsYcjPAgAzM5xlIw2I/xF0My32dyxMG8Jkd/c6fn2S/x9uFAztN87dtzr/QvAJctLn12dSHNXN9ylulLVBCv9zOdqVPGNHWlnY8e5wY9XkZc1n1RvgWKLU01hJxSbhKGTxUDIl6ngOaG9c2XV4y+SQ7hhIgbGI7Yomimyi+eeOfhZhAZ6Apll6hrtKlsSNk871dnaIcX5eSozc0aCreNMna2olZOa7QaaDQ6XhAKbqYW6JB+XSVcCeXL1rtQkk52D1to13huMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH/nsEi88hFiNPCWYvTB3lERZpeUCbpDzAXQT/4TONmOw8zi7Cd2OlX8BGBFqjh/fESHv+adlzsY1mUdMvpVaYgHr3gYi8sBSrq5TMUfSYaWp4WCD7utiXXGprG08GCdbye1lpyyNnniWp12Bgjao+rtGamL/M1d6+WZTC+XL+H30u4VHURAiFBsAEoX6tlGV8ynhYOr/b8B43jy0/R0JfrzLjwSKEcA6RfKM7ozbZ0QZuQDALULymPIesrV4mvZ2Qwg4YgpAKaki9Sse45yiIhsIY0p5RnuNZRZnCbukyeBzIyDJobEBGhpui/KT2dqXBlRgRuOhCUf7OGCcPVHKNQ=="
],
"priority": ["100"]
}
},
@ -1403,6 +1472,12 @@
"providerId": "rsa-enc-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEogIBAAKCAQEAn82AU+InXwYlE8u9lMwhQghZB7oQ71Hg3PdFqS9ICGzw1u1JcENooCsZse55V6nqptdYF1oZA8QrxnhHzCVCGIqFHtXSoPGHVtozO3Fe1cVIVFm1D9TNS3JHe1C8SBQQT4hGItO5cjDyfGdK3x09RkoAcelrzH5uQ78zd0FKHkzbsTMsP2V8V94c35+ViIUjyGhH2T2BpIyGRLignL+6d0wHbw463L1Ewj/J9z8BtNLCH9PaVLWiGQARjlWyL9vtWBig9XXL0Z9tZUuoLihjh4StkXt2lQ++DKxUklsAjyenRAG5d72T2rY8MO5a1Z2ZSt8+s86D5esrAEIFZc9mqwIDAQABAoIBAAmmCcqGzCPDpjd0xMSYMqXfBSkfReh9RBtzXqRhc3L2yO/hMd7yYv3QvGNu56qwWreqJup6CSqeDJqWJpef5EbBDlqXRHltO+O1lwROyxATMlPNes4y5hZZFxHOBSBA/d8fdkSiDf9kDzANuIqSJGH7E93M3zJgq92xTLU1nvkHR/VYJQv+j+Pjye7MWvjIePfhwFeBqEWlWPTlw/080Mpfp8Hhbl6JeKjx2inkSphp43v4wR1Wmp+E2JIHF4P4sVXPPuPf3JDwg5uGOrROw1ziloD3jTI+LnQ+kRm6R2EbqRqqVsehXT7mZy2puQNqVc4vVqWQdxIErMBazYEpZOECgYEA+8PEcDiIPr2PTYZk+/jErRVYwsxyLgDJexPak7onLxLBJRNRnp1Uk6b1LXM6af5qp+Y510kyAe1k+9xkQLx1gW8rMka9rvVsM+1A2ACvF99V23sRw29CVxeFV/zNn83MinYPX5biUl6MkOX2PvWUhdwRGhKByjiYcAeBOsXkz3ECgYEAon2yYXGzph8Vb8Fetv0wFFbjQOixuL02OjVp/nU1XVE8Aw9BJ7uzA6GQ7akPG0HsaUq7AEHP1uUOsJWQTNQ8WYD9LDuDOl/JFqkG+zrmdUdm0mAIYyH1/GBqgaTLvMq78qqosua8BBJojEyoXDz69UBHpu7cwtUgmzRNQSYqgdsCgYASvD3JEBvrd1XLsh2ftqKEMtt5G5e/nqVfuFmCts6lrSKcbLSdNh4OItWJ/VIygxFSz0osoDDNfeoO6Ba5zox8BlbTlfoVpAPaVWSG7n4ZK7CK9bybq5LnQkPVCWYP51O6VhDMz0CmWozhV4ucoc/cqkTHiOsJrm6Bn71ZL1LYsQKBgFNb8qgk4YnGhoPHiuSLbR/yFzGUbqAciXZBMrg0vwS5iPT03XMZytOBDk2uHi7YmgTGLrsKCCrxZaDXiaiwdKliD/+iJEdNHmc+nXNDGzltQOWKGKNqp7wqZllOBqs6wkLSpCrrTec03mejZ/ex3Pj2WgvcnGpjVg/pO/zBLKtjAoGACzGQNEF93fabHQJTsHmb/g+jO2iumjF6ZIWzdFh2KzQABONcoBvy1MJNASFQj3iVy/8kEo4SfmexvMWLBW9igi2z1pHeHY32EuImzuc4xnVDm6dkmDdsO43Ex6CFBx8lM40H4l27mXu+EZRzGClUY8TnmV/FBGmX+LPtOiiwT7s="
],
"certificate": [
"MIICnTCCAYUCBgGTy58fHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/NgFPiJ18GJRPLvZTMIUIIWQe6EO9R4Nz3RakvSAhs8NbtSXBDaKArGbHueVep6qbXWBdaGQPEK8Z4R8wlQhiKhR7V0qDxh1baMztxXtXFSFRZtQ/UzUtyR3tQvEgUEE+IRiLTuXIw8nxnSt8dPUZKAHHpa8x+bkO/M3dBSh5M27EzLD9lfFfeHN+flYiFI8hoR9k9gaSMhkS4oJy/undMB28OOty9RMI/yfc/AbTSwh/T2lS1ohkAEY5Vsi/b7VgYoPV1y9GfbWVLqC4oY4eErZF7dpUPvgysVJJbAI8np0QBuXe9k9q2PDDuWtWdmUrfPrPOg+XrKwBCBWXPZqsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATwmKBzLiZiUjyB9BWUR4BCXh46DxsiM0BCublewlUFY6FBTn7ea6q3G+X3QP2WM6xa0oAmQz9dq1KChbIoC2WPbceAbwd5XZZfziWsRCv6+xPswtpHPIrsenz8TR4K4P73aeCC+vTVs/y+2tGPEVbnSkcNnOP71hRQGlt0LvjKlEetJSRyYz5depSdJOjl4F3ehpxQtTK/48xUVAytu9ZotJj6AUA7jWFlP5GHgoB+mPk6QTHNWddnc7BQx2FMvg151vxu722ywLh5Dh7WzgFhJNwkX4xpwzhfo0Q1gSygGTdZaJCGj5jfF+KwdiKpN04UxJ8OrRgJqklQgrSVnsgQ=="
],
"priority": ["100"],
"algorithm": ["RSA-OAEP"]
}
@ -1413,6 +1488,8 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": ["132fb843-59e9-4f36-ad55-5ce2d3a13fb3"],
"secret": ["ETyyqapnrkUsNXLQ-tBVKw"],
"priority": ["100"]
}
},
@ -1422,6 +1499,10 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"kid": ["5110d380-c930-49d9-b91b-87f338f6170b"],
"secret": [
"uCpQrJvP5OBuTxXfDb4JRL0bCKpXUgfGn5vb8UvL-Sfs_sZ9rtvBmd6vuFWARqyezjJQtpoNlMv7sXgxkN-yxQ"
],
"priority": ["100"],
"algorithm": ["HS256"]
}
@ -1454,7 +1535,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "f7f2b89b-43cb-491d-8e7c-f1814024a6da",
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1480,7 +1561,7 @@
]
},
{
"id": "17cdac6f-d2a3-4907-8d44-a42827610b63",
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1514,7 +1595,7 @@
]
},
{
"id": "53a3e43f-9468-401f-8051-40f982d12f85",
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1540,7 +1621,7 @@
]
},
{
"id": "26286808-3b7b-43df-b32e-af55a37af2e9",
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1566,7 +1647,7 @@
]
},
{
"id": "8a6a752a-9a9a-4d38-b1f8-edf0a9433490",
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1592,7 +1673,7 @@
]
},
{
"id": "a6f6804c-4160-4a84-8a1f-c2747a2d3f27",
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1618,7 +1699,7 @@
]
},
{
"id": "740baa9e-8328-4035-9e1a-8fc1616d1f0f",
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1644,7 +1725,7 @@
]
},
{
"id": "e60187a8-3e16-4a0c-9daa-f3a4a1fcfdba",
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1671,7 +1752,7 @@
]
},
{
"id": "d959d0c2-4004-4633-b280-f80d6423f574",
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1697,7 +1778,7 @@
]
},
{
"id": "ba02689d-b9e8-4a4b-8fdd-0d1386b198fc",
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1739,7 +1820,7 @@
]
},
{
"id": "f09ac92a-e091-4e84-9cd1-cb905ca57b89",
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1781,7 +1862,7 @@
]
},
{
"id": "aaf72b22-cec4-4714-93d6-f54d5a986ab8",
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1815,7 +1896,7 @@
]
},
{
"id": "c4a54bb3-f009-4231-a82b-376c2515e07e",
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1833,7 +1914,7 @@
]
},
{
"id": "f55ded54-683a-4f5a-a101-9cfbd7b96781",
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1860,7 +1941,7 @@
]
},
{
"id": "931d5a82-378f-4533-8c69-2239a4acd047",
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1886,7 +1967,7 @@
]
},
{
"id": "22b05374-f480-4ca8-aca8-9db8b6dd1729",
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1912,7 +1993,7 @@
]
},
{
"id": "c0371832-e4b7-485e-bf23-6babe4c6ac83",
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -1931,7 +2012,7 @@
]
},
{
"id": "4d0445da-073e-465e-b25b-af522915c73f",
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -1973,7 +2054,7 @@
]
},
{
"id": "740d467f-4203-425b-8203-9bfd3eed25ae",
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2015,7 +2096,7 @@
]
},
{
"id": "cf1a9af9-dadd-4cb9-a26e-fbbba216f8e1",
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2035,14 +2116,14 @@
],
"authenticatorConfig": [
{
"id": "4e65eb4b-9f0a-4ab8-98b2-6daf50cd1bf8",
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "5e8dc1c5-1489-4d39-bb75-9c499583b91b",
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"
@ -2132,8 +2213,8 @@
"attributes": {
"cibaBackchannelTokenDeliveryMode": "poll",
"cibaAuthRequestedUserHint": "login_hint",
"oauth2DevicePollingInterval": "5",
"clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5",
"clientSessionIdleTimeout": "0",
"userProfileEnabled": "true",
"clientOfflineSessionIdleTimeout": "0",

View File

@ -73,7 +73,7 @@
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["delete-account", "view-profile", "manage-account"]
"account": ["view-profile", "manage-account", "delete-account"]
}
},
"clientRole": false,
@ -435,13 +435,46 @@
"type": "password",
"userLabel": "My password",
"createdDate": 1716214710762,
"secretData": "{\"value\":\"OaI4sKqQn+NZtS6N/bcqoZ8Q+ucpBby1n4XmzVmioKw=\",\"salt\":\"temixVCSbpA7Genml2KTAw==\",\"additionalParameters\":{}}",
"secretData": "{\"value\":\"QzJjOdXU0L9Pdxdx1V5xUs7BY9beGlmN8NpR2qiWxbkjrQ434Q1GwSiJKekZQ/zrLDtNZ7sAbVu+SS+XIe9Zaw==\",\"salt\":\"x8cABpa0Hk/nJ2BPKdFXTg==\",\"additionalParameters\":{}}",
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
}
],
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"create-client",
"view-identity-providers",
"manage-realm",
"query-groups",
"manage-clients",
"query-users",
"realm-admin",
"view-authorization",
"view-events",
"view-clients",
"view-realm",
"manage-events",
"query-realms",
"query-clients",
"manage-identity-providers",
"manage-users",
"view-users",
"impersonation",
"manage-authorization"
],
"broker": ["read-token"],
"account": [
"view-profile",
"manage-account-links",
"view-applications",
"manage-consent",
"delete-account",
"manage-account",
"view-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -507,8 +540,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -643,7 +680,6 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false",
@ -704,8 +740,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -724,6 +764,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1284,11 +1342,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1304,14 +1362,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"saml-role-list-mapper"
"saml-role-list-mapper",
"oidc-usermodel-property-mapper"
]
}
},
@ -1361,13 +1419,13 @@
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper"
"saml-role-list-mapper"
]
}
},
@ -1485,7 +1543,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "e134634e-f219-4df4-867c-8110688d8e56",
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1511,7 +1569,7 @@
]
},
{
"id": "a611a8eb-9626-4aa4-8b54-ee565ea6e5dc",
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1545,7 +1603,7 @@
]
},
{
"id": "d87cbb31-5c69-45c8-888d-f9649ebbbf97",
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1571,7 +1629,7 @@
]
},
{
"id": "752ba282-a369-4592-92e8-b4287192dbbf",
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1597,7 +1655,7 @@
]
},
{
"id": "2349282e-40ff-431a-984d-53911511e3d3",
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1623,7 +1681,7 @@
]
},
{
"id": "4ff5463d-26d9-4219-ba85-41464401098f",
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1649,7 +1707,7 @@
]
},
{
"id": "87bb6c6d-cca8-4832-b5ab-67ecb9454a42",
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1675,7 +1733,7 @@
]
},
{
"id": "1fc3d028-0e0a-43a4-aaf9-ba7f7d60b409",
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1702,7 +1760,7 @@
]
},
{
"id": "036aae59-641f-4799-9124-c7e5034af6c1",
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1728,7 +1786,7 @@
]
},
{
"id": "2e8b9f28-93b8-4368-84b0-1a8326daafe0",
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1770,7 +1828,7 @@
]
},
{
"id": "0b826105-8493-45ce-87b3-7d917d190b39",
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1812,7 +1870,7 @@
]
},
{
"id": "bf6d9edd-48d8-4392-bbc8-4b17a6866074",
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1846,7 +1904,7 @@
]
},
{
"id": "97e31722-dd11-42be-aa99-88788fa2dde6",
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1864,7 +1922,7 @@
]
},
{
"id": "3f45cf34-231f-4ea1-8e58-d636c451a76b",
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1891,7 +1949,7 @@
]
},
{
"id": "9bef2f7c-f989-4871-aaa7-18e2cfa73f22",
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1917,7 +1975,7 @@
]
},
{
"id": "0bfaa325-acde-4443-8bd8-1dc2ae759c5f",
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1943,7 +2001,7 @@
]
},
{
"id": "37ddbe8c-abf3-4654-bd6d-ffabbeefbb98",
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -1962,7 +2020,7 @@
]
},
{
"id": "5d7b4bc9-e93b-40da-aeb6-ba0c38392f1a",
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2004,7 +2062,7 @@
]
},
{
"id": "ee7a56e4-c827-4f24-8b8b-8476050b0b64",
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2046,7 +2104,7 @@
]
},
{
"id": "360f0031-4c3b-4272-84ca-2172d430b4bc",
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2066,14 +2124,14 @@
],
"authenticatorConfig": [
{
"id": "53630acd-a33a-40e3-8786-cf85464c6f9e",
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "c0d2b6a0-caad-4e90-b040-17cacdaf70bb",
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -73,7 +73,7 @@
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["delete-account", "view-profile", "manage-account"]
"account": ["view-profile", "manage-account", "delete-account"]
}
},
"clientRole": false,
@ -407,7 +407,7 @@
"otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": ["totpAppGoogleName", "totpAppFreeOTPName"],
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
"webAuthnPolicyRpId": "",
@ -452,6 +452,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"create-client",
"view-identity-providers",
"manage-realm",
"query-groups",
"manage-clients",
"query-users",
"realm-admin",
"view-authorization",
"view-events",
"view-clients",
"view-realm",
"manage-events",
"query-realms",
"query-clients",
"manage-identity-providers",
"manage-users",
"view-users",
"impersonation",
"manage-authorization"
],
"broker": ["read-token"],
"account": [
"view-profile",
"manage-account-links",
"view-applications",
"manage-consent",
"delete-account",
"manage-account",
"view-groups",
"view-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -517,8 +551,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -653,7 +691,6 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false",
@ -714,8 +751,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -734,6 +775,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1294,11 +1353,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1314,14 +1373,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper"
"saml-user-property-mapper"
]
}
},
@ -1370,14 +1429,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper"
]
}
},
@ -1495,7 +1554,7 @@
"defaultLocale": "en",
"authenticationFlows": [
{
"id": "19317acb-fe8e-4c79-82bc-90e159273075",
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
"alias": "Account verification options",
"description": "Method with which to verity the existing account",
"providerId": "basic-flow",
@ -1521,7 +1580,7 @@
]
},
{
"id": "122857d2-33da-4086-8acb-cb0e303aaf1b",
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
"alias": "Authentication Options",
"description": "Authentication options.",
"providerId": "basic-flow",
@ -1555,7 +1614,7 @@
]
},
{
"id": "abf5dd35-4791-4268-a10c-5f4b6a06b84a",
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
"alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1581,7 +1640,7 @@
]
},
{
"id": "a18daeec-a33c-4a43-b014-10c84ec69b81",
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
"alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1607,7 +1666,7 @@
]
},
{
"id": "e9f032a7-32f7-457c-becf-011a1a35cc6a",
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
"alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow",
@ -1633,7 +1692,7 @@
]
},
{
"id": "9db65b7c-98ca-4003-beea-611038831ffe",
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
"alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow",
@ -1659,7 +1718,7 @@
]
},
{
"id": "7bd0854c-d7ae-43d7-a1ae-7b759a34cb1d",
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
"alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow",
@ -1685,7 +1744,7 @@
]
},
{
"id": "2de1a450-fe98-443a-9c6c-d24d8a7ebcb3",
"id": "61c7966c-ad72-49f5-84dd-376152348092",
"alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow",
@ -1712,7 +1771,7 @@
]
},
{
"id": "7b3efad5-4b7d-4385-a41c-fecc73afdcc4",
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
"alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account",
"providerId": "basic-flow",
@ -1738,7 +1797,7 @@
]
},
{
"id": "de93418e-8f28-4099-b15e-ad36ec194796",
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
"alias": "browser",
"description": "browser based authentication",
"providerId": "basic-flow",
@ -1780,7 +1839,7 @@
]
},
{
"id": "0dd3345c-6e82-4c3a-a39a-d49ae1f5c409",
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
"alias": "clients",
"description": "Base authentication for clients",
"providerId": "client-flow",
@ -1822,7 +1881,7 @@
]
},
{
"id": "87fb4dd0-5326-47a1-b670-982f4872ff89",
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
"alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow",
@ -1856,7 +1915,7 @@
]
},
{
"id": "344723b3-4ab1-4999-abdd-32398e82327b",
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
"alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow",
@ -1874,7 +1933,7 @@
]
},
{
"id": "f3341938-caf9-4c8a-9cd5-eb34609809ab",
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
"alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow",
@ -1901,7 +1960,7 @@
]
},
{
"id": "ba7b7357-e324-4b71-9bda-f8512a760e02",
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
"alias": "forms",
"description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow",
@ -1927,7 +1986,7 @@
]
},
{
"id": "134971e6-bf63-432c-806e-74ca4fb09963",
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
"alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow",
@ -1953,7 +2012,7 @@
]
},
{
"id": "6ea9e2cf-5684-4c65-8c07-930d1cbb0b46",
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
"alias": "registration",
"description": "registration flow",
"providerId": "basic-flow",
@ -1972,7 +2031,7 @@
]
},
{
"id": "67e3c8c7-1b5e-4119-84a2-e90876293150",
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
"alias": "registration form",
"description": "registration form",
"providerId": "form-flow",
@ -2014,7 +2073,7 @@
]
},
{
"id": "fc6d48ec-a1f1-41b1-9310-54f58861d5aa",
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
"alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow",
@ -2056,7 +2115,7 @@
]
},
{
"id": "80b1d464-c2ec-4eb1-82e8-32cbede779a8",
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
"alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow",
@ -2076,14 +2135,14 @@
],
"authenticatorConfig": [
{
"id": "86b1d5fa-450c-40d8-899c-725861ac39fc",
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
"alias": "create unique user config",
"config": {
"require.password.update.after.registration": "false"
}
},
{
"id": "ea724f02-029a-493d-b4d3-08972be21cfb",
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
"alias": "review profile config",
"config": {
"update.profile.on.first.login": "missing"

View File

@ -73,7 +73,7 @@
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["delete-account", "view-profile", "manage-account"]
"account": ["view-profile", "manage-account", "delete-account"]
}
},
"clientRole": false,
@ -408,9 +408,9 @@
"otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false,
"otpSupportedApplications": [
"totpAppGoogleName",
"totpAppFreeOTPName",
"totpAppMicrosoftAuthenticatorName"
"totpAppMicrosoftAuthenticatorName",
"totpAppGoogleName"
],
"webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"],
@ -456,6 +456,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"create-client",
"view-identity-providers",
"manage-realm",
"query-groups",
"manage-clients",
"query-users",
"realm-admin",
"view-authorization",
"view-events",
"view-clients",
"view-realm",
"manage-events",
"query-realms",
"query-clients",
"manage-identity-providers",
"manage-users",
"view-users",
"impersonation",
"manage-authorization"
],
"broker": ["read-token"],
"account": [
"view-profile",
"manage-account-links",
"view-applications",
"manage-consent",
"delete-account",
"manage-account",
"view-groups",
"view-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -521,8 +555,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -657,7 +695,6 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false",
@ -718,8 +755,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -738,6 +779,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"introspection.token.claim": "true",
"lightweight.claim": "true"
}
},
{
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
"name": "locale",
@ -1298,11 +1357,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1318,13 +1377,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper"
]
}
@ -1374,14 +1433,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper"
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper"
]
}
},

File diff suppressed because it is too large Load Diff

View File

@ -55,7 +55,7 @@
"composites": {
"realm": ["offline_access", "uma_authorization"],
"client": {
"account": ["delete-account", "view-profile", "manage-account"]
"account": ["view-profile", "delete-account", "manage-account"]
}
},
"clientRole": false,
@ -459,6 +459,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"query-clients",
"manage-identity-providers",
"create-client",
"view-users",
"query-groups",
"view-realm",
"manage-authorization",
"view-authorization",
"query-users",
"impersonation",
"realm-admin",
"manage-users",
"view-identity-providers",
"manage-realm",
"manage-clients",
"query-realms",
"view-events",
"manage-events",
"view-clients"
],
"broker": ["read-token"],
"account": [
"manage-account",
"view-consent",
"view-groups",
"delete-account",
"view-applications",
"manage-account-links",
"view-profile",
"manage-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -505,7 +539,6 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
@ -532,8 +565,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -649,7 +686,11 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
"redirectUris": [
"https://my-theme.keycloakify.dev/*",
"http://localhost*",
"http://127.0.0.1*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
@ -664,8 +705,7 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false"
@ -725,8 +765,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -745,6 +789,24 @@
"fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0,
"protocolMappers": [
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
},
{
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
"name": "locale",
@ -1336,12 +1398,12 @@
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
},
"smtpServer": {},
"loginTheme": "",
"accountTheme": "keycloakify-starter",
"loginTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1358,13 +1420,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper"
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper"
]
}
},
@ -1433,14 +1495,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper"
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper"
]
}
}

View File

@ -468,6 +468,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"manage-clients",
"manage-users",
"view-identity-providers",
"view-users",
"impersonation",
"manage-identity-providers",
"query-users",
"query-realms",
"realm-admin",
"view-events",
"view-realm",
"manage-events",
"manage-authorization",
"manage-realm",
"query-clients",
"query-groups",
"view-clients",
"create-client",
"view-authorization"
],
"broker": ["read-token"],
"account": [
"manage-consent",
"manage-account-links",
"view-applications",
"view-consent",
"manage-account",
"view-profile",
"view-groups",
"delete-account"
]
},
"notBefore": 0,
"groups": []
}
@ -514,7 +548,6 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
@ -541,8 +574,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": [],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -658,7 +695,11 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
"redirectUris": [
"https://my-theme.keycloakify.dev/*",
"http://localhost*",
"http://127.0.0.1*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
@ -673,8 +714,7 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false"
@ -840,8 +880,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -875,6 +919,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
@ -1451,12 +1513,12 @@
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
},
"smtpServer": {},
"loginTheme": "keycloak",
"accountTheme": "keycloakify-starter",
"loginTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1501,14 +1563,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper"
"oidc-address-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper"
]
}
},
@ -1541,13 +1603,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper"
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper"
]
}
},

View File

@ -538,10 +538,10 @@
"emailVerified": true,
"attributes": {
"additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
"gender": ["prefer_not_to_say"],
"favorite_pet": ["cats"],
"favourite_pet": ["cat"],
"gender": ["prefer_not_to_say"],
"bio": ["Hello I'm Test User and I do not exist."],
"favourite_pet": ["cat"],
"phone_number": ["1111111111"],
"locale": ["en"],
"favorite_media": ["movies", "series"]
@ -562,6 +562,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"manage-users",
"create-client",
"view-users",
"view-realm",
"query-realms",
"impersonation",
"view-events",
"realm-admin",
"manage-authorization",
"manage-events",
"view-authorization",
"manage-clients",
"query-users",
"query-groups",
"manage-realm",
"query-clients",
"manage-identity-providers",
"view-clients",
"view-identity-providers"
],
"broker": ["read-token"],
"account": [
"delete-account",
"view-applications",
"manage-account",
"view-consent",
"view-groups",
"view-profile",
"manage-account-links",
"manage-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -636,7 +670,7 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["*"],
"redirectUris": ["http://localhost*", "http://127.0.0.1*", "*"],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
@ -798,8 +832,7 @@
"attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false"
@ -892,8 +925,12 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -927,6 +964,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false",
"claim.value": "[\"*\"]",
"lightweight.claim": "true"
}
}
],
"defaultClientScopes": [
@ -1551,11 +1606,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"accountTheme": "",
"adminTheme": "",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1581,14 +1636,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper"
"oidc-address-mapper",
"saml-role-list-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper"
]
}
},
@ -1618,13 +1673,13 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"oidc-address-mapper",
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper"
]
}
@ -1678,6 +1733,12 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEowIBAAKCAQEAso89qpvLhf9DIcCb2JAbxItRLSIvP/NCZhMdAExTHyrhM5B27ZQ6MZ7dJQbnMu7QJ7yiClsD1XnDN7Wlj07sY2As3lY3v9kjODBeADYlPuN1m7/fXFHX3qfRT+PwVSaAhMykmqvWp86UTg7t7rNjVBnXPPXItmRLIF+jZUMWQduwNznr6Jh54ZdIwEy4hvX1bpNw0nPl4KXiOi2elvg+rk7BhFywGwQ/HUCGkrcq0XS/aNOy1ChmqDbtq817mYpVeteCDe8xP3MPrZ/s2LiEt4Ip1cNo0dY+a4JwOzwL42h3GaR+80iK3pZNo+Mr0KBOY9GXvdV/MvcPHLQ7VujUGQIDAQABAoIBAAHV0OQwmDxUazqiVGe61Bzmcqs5q03SC1K/FmCi/YVikdskvGLaOmk5UQa4+1uDEq7J30onH9ML8+qeFRQek0rn2ZDfxtBpDqsx7LwTUmQtqc8z6buKQs37db5ctnhlk34UmAotQyDz5wMmCkzWWVUWCT02PdMev5qW/mKuIxaCWLHUFiMJaGrYCCwB/Ra8KLcadKgRbytSUth9qILC4krFfmWtzIx1P6nM1pzQ1nydxNnNPJKjoWtLRJ5b701Y5/h2vAAg6Mr+jKe1DPa9QmAqhQudjGbZ31av+0f1/I+XkflpZfokfU+MrAqNYRTYkevRYgc3wakK5mfVYUiMuOECgYEA7fk55O2OJFsR0Vjy4Dx4eSIwgwobvwEuHxlyWn0RC7nFb00eh6OPuc5sHrOk8bK3P367q67sEhxGyBF16nwxgX/T+c8gTC8QRuwNymosA4Je/zJHbKvyzLGOouCP5gYwq/wUmVWzNApVC7LBfxbsqYyivHABc5xgPmTgecY0VWkCgYEAwBXcUKoyq1KZegyNJcTuwuvBXoYVveFGm6QKKKwzojCCKaR3XXtdSon1qYfuKT0MLxgEDyyBks9DgfCodSsTmajX90Yolhyz3ptcOmRURqTRoJhM4g6qA+Ybd3uy8vAz32RdS+4rCTgnMG/5Xpn5B4ojOnhRcnA2TPCJgWz6QzECgYEAhj1FjD75JMb+mRJNB3L1HpfLt8+28RsQUli/ag4M1Il5txxQsYDxbYXk9biuvezrc/Tglqs43cp3nxpCYwClyIA8KjnN5UvTKb601M7pfx1GyzwokEO61f7/ECAO7FnnkMzFLe3rBdsiOFQg1LkwzT/Y+OVR3E6E+A1dlzPYh6kCgYBIP3CwfnO0cMr9Vv8394x+kEIZFYHT+4mdPOP9TFfXZztuAkhLRv1d7eoSq+fuZuHQTM4qDullmMOhei1CdMNYhmNExIS7gWw+DF1yMQ5py9B1ARPZ6v4TnVczZ7l1GtfH7G4TAy/4tcA3vcYjyPIb3d9GPL8VthMWeVqe7ahr4QKBgEwA7ASbs4NxfBsStEGQYQYAeWOoKnTc50FeYz38O4KrOirtTFPNsJcyCiTE0o4cqu/OebSA5irrauV7SEDl/gfH54g3ZWusQbLt2uMnZYtkd2+Ka3T9XM0QfQW/vYl3eJtdQj89TqzLzyP0AgvAyIgeG3RMH8ojqCh3YKY0FTv/"
],
"certificate": [
"MIICnTCCAYUCBgGTy2TGBjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKPPaqby4X/QyHAm9iQG8SLUS0iLz/zQmYTHQBMUx8q4TOQdu2UOjGe3SUG5zLu0Ce8ogpbA9V5wze1pY9O7GNgLN5WN7/ZIzgwXgA2JT7jdZu/31xR196n0U/j8FUmgITMpJqr1qfOlE4O7e6zY1QZ1zz1yLZkSyBfo2VDFkHbsDc56+iYeeGXSMBMuIb19W6TcNJz5eCl4jotnpb4Pq5OwYRcsBsEPx1AhpK3KtF0v2jTstQoZqg27avNe5mKVXrXgg3vMT9zD62f7Ni4hLeCKdXDaNHWPmuCcDs8C+NodxmkfvNIit6WTaPjK9CgTmPRl73VfzL3Dxy0O1bo1BkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAggzxmYvHqUaCPLxxSidLQMgpu1pTozg3rTq8dcxhcHINI//A/z7qQyDA/QQN5cuSpYvdt2MRWoNop+uRNKqSr3C8aRErbY0j4acl7yG/ghNfQUZ9KxDBxKrd0HLFUibdZobg10+Ih/qXo3Mi2VtkqyZQRl/iy0O3ITgqb7YJUEx5tuEWyGbn+SerFvqZNcmsLziOJefm1n4uqroHgIfmgY6Deh+wZK0DwO3WZ6ThjhMp5GFi1oNeZ9xoExNEXrYp07b2xTQFF57oypc7prf733lqGjPRLfoVJP6qcsjvAlOA7f8TG9sKwGuRsPfadYY9PxmdHxl2k7PHDJeDhA7VdQ=="
],
"priority": ["100"]
}
},
@ -1687,6 +1748,12 @@
"providerId": "rsa-enc-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEowIBAAKCAQEAxoEvnv+YHCqUWANGuku5QYscAZyUE0WHSlcAzZ0bQugPow63piQsuxPz0cpPIuLab6adssXUqKEFheT1H0BqtmT9L/7iOKB6MRuInN4aRzzTH9q02TKPkcpSAzAHTGcsJBMMawlbnIdMu5+mevMPxqeVVxvrnKG27S8H3W5jqIkQw8bo646Hr3l5Dxq/jY7slcSXXXe4ZdefeCvnSqea+fy5c+r/r546nX4FTGiklu6KLQaDc9SfGccrZDmljY7DX1kHrmvIdLShcuukTHc0hi2qbgMcUte/7/svSJLUWOZObKxetd4y1OA49v36xrMqGhwGDdwrWf0VuMBN8eHOCQIDAQABAoIBABz/hUXnFRZURWHKxLvKpnBZPTOiZzfzfxfl4tOmq54CtDoVQyXNq2J+6oOPWC/X+ky3hy+1BQ5x9hJrx+qTU04m2EfOe8da8M7DX28kZlauyjF2loG+MvP7ctn4BluWcip+RTZOYn2DfxBPpRcunR409V+JesoMY7fSwtrfA/Gm0PrXgBK7OuE0nxqFFWnsLOc+HxZECS5r0n1MHEBHe774HkqGcK91j8S+QU+/diTnK+N/ClnKWnabMK8bUO5wAUuKwf2deYkGP91pCEJlVnVZyaXshEM+uxTuMRUlq9h1QAIUatvdQwfOKqZ9XvmTVC8b79qLwmezjoDxNCKbaMMCgYEA71WDpMnA2uS2wCJ/MVwzWGSBDjfeKUPRy33BeUfwLGp4Dro+S1sTrLHgi1HGmvmC8ReZrifUlUHUi3ZHauR6vbNsEoSQ3hplO013kj12EfcBpvKYFg1ODCwevb/JtBTWbDG1P+E9DGiF/2u0aicoJoPolNeNVzgO6YK1OI/S/LMCgYEA1FPTqFPulXxcOK12LgYap8typqJ7zu4fByr42010yrKM+LLNA3bT/i/oRkKc7J1ztKSqlVckADWgK4Y27lI4j1tSgTOxFzwxnTZOeF7ZwGSxq9iy9A84nDiW+m6Hj5RDyBjTSoP2Qqv6d5kTUx+pczZvOVTWRlIEnFETbbxOoFMCgYEA0r1etHx+V4AqtxXpH6KLB5s/1DA3a+hu1BrAgLVqcwGxA27VKW9h7J+YE7UHBzELLpVUWfhyhJa5u6+DhUj4Fw/k6o1WLmvZlZVJ4zhBPeJczw8wAcLnZWp4CybUScBLamt+qGgBZGqpCtZgv1QJU5i09FK0/wa6grz4K3zhEGcCgYAlnGe8xIlZr3rCi2+IvYoROQepHtUhlaqnYWRNrI3IrhIsp7eLKoxo1WGmuHwFqepqEFUrORFmfBlQPGkUlDnyovGdc2OmQwJi39DMn7igzPVwBGXGt7+GZLvRxqx6sX/EPSmIZJHFw6MNdm8m5U/l2bmgBTgjormwWug/IwEmgwKBgEouISIuXsjGxeLmhrOXHKXb6IfKglNJeBM6lTQ6MLaVOso7KdelIntwZNtZwMIi3hlwaUb1X1QmztFbnrvnPhWwJR4ZgMEWanRHthtm0SHzg8EHKT40S91oKabsgHk3wpOvq/iWs+k8qWN4HYp6UO603uLMOfxPYJCFxRtg2TsJ"
],
"certificate": [
"MIICnTCCAYUCBgGTy2TG/jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMaBL57/mBwqlFgDRrpLuUGLHAGclBNFh0pXAM2dG0LoD6MOt6YkLLsT89HKTyLi2m+mnbLF1KihBYXk9R9AarZk/S/+4jigejEbiJzeGkc80x/atNkyj5HKUgMwB0xnLCQTDGsJW5yHTLufpnrzD8anlVcb65yhtu0vB91uY6iJEMPG6OuOh695eQ8av42O7JXEl113uGXXn3gr50qnmvn8uXPq/6+eOp1+BUxopJbuii0Gg3PUnxnHK2Q5pY2Ow19ZB65ryHS0oXLrpEx3NIYtqm4DHFLXv+/7L0iS1FjmTmysXrXeMtTgOPb9+sazKhocBg3cK1n9FbjATfHhzgkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdUIlJ91E0UkFS45AByjFufRnQbAi1smnHkC3WSN39bhcFT7Hgip97qtABODR58zVHSTS0XcMiL4mMObH3Vyz9J3gmwWZnbokAuo9tYeyrhPh/gqXv3LGtGhTpWlUJ7JEJxH7RVI4UZZyG6Y6FR+3zwiZ0j1p3QsZclfcNmacoi/Ano+4TfloOnY4k8yP7G6LWUTJHpcRNWVVozM3RwekYgpJRAtXDoYfm9p2hRQ090e7NvbblSuVQ/FXhUn4g0wz91WdCWlwXZfvNaRjbynPCHejJpszqiyjPkx3aRKTWqer0ZocKNmY8+RO27XIsXmwOYcjdpX2TCFDv6O+VLfNdw=="
],
"priority": ["100"],
"algorithm": ["RSA-OAEP"]
}
@ -1697,6 +1764,8 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": ["95db7eb8-b57b-475e-90cd-58841a9388d3"],
"secret": ["dp6bv53YrC2PZuJCxa3aNA"],
"priority": ["100"]
}
},
@ -1706,6 +1775,10 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"kid": ["d0254883-059e-4fdd-bf03-704c76650aab"],
"secret": [
"bcW7E4rcbgSKZIQysWOSuhezRGYs5Kzmp3ZESthdTUMyFivK8RbBAdBE4PhFPk5B9TuByDO2RWvd8F7F5YhGJitf6cfYB1BfDuAk-2iBAtdZA98g7a2h4jpwzh-GIgtoRbGbH9qnquUn52f5qteo34g5WifKE2bWjOELza9FrTo"
],
"priority": ["100"],
"algorithm": ["HS512"]
}

View File

@ -563,6 +563,40 @@
"disableableCredentialTypes": [],
"requiredActions": [],
"realmRoles": ["default-roles-myrealm"],
"clientRoles": {
"realm-management": [
"manage-users",
"create-client",
"view-users",
"view-realm",
"query-realms",
"impersonation",
"view-events",
"realm-admin",
"manage-authorization",
"view-authorization",
"manage-events",
"manage-clients",
"query-users",
"query-groups",
"manage-realm",
"query-clients",
"manage-identity-providers",
"view-identity-providers",
"view-clients"
],
"broker": ["read-token"],
"account": [
"delete-account",
"view-applications",
"manage-account",
"view-consent",
"view-groups",
"view-profile",
"manage-account-links",
"manage-consent"
]
},
"notBefore": 0,
"groups": []
}
@ -638,7 +672,11 @@
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["*"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
@ -805,8 +843,7 @@
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
"post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false"
@ -894,14 +931,20 @@
"id": "fce8a109-6f32-4814-9a20-2ff2435d2da6",
"clientId": "security-admin-console",
"name": "${client_security-admin-console}",
"description": "",
"rootUrl": "${authAdminUrl}",
"adminUrl": "",
"baseUrl": "/admin/myrealm/console/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": ["/admin/myrealm/console/*"],
"webOrigins": ["+"],
"redirectUris": [
"http://localhost*",
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
@ -914,9 +957,14 @@
"protocol": "openid-connect",
"attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"client.use.lightweight.access.token.enabled": "true",
"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": true,
@ -937,6 +985,24 @@
"claim.name": "locale",
"jsonType.label": "String"
}
},
{
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
"name": "allowed-origins",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"introspection.token.claim": "true",
"claim.value": "[\"*\"]",
"userinfo.token.claim": "true",
"id.token.claim": "false",
"lightweight.claim": "true",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false"
}
}
],
"defaultClientScopes": [
@ -1561,11 +1627,11 @@
},
"smtpServer": {},
"loginTheme": "keycloakify-starter",
"accountTheme": "keycloakify-starter",
"adminTheme": "",
"accountTheme": "",
"adminTheme": "keycloakify-starter",
"emailTheme": "",
"eventsEnabled": false,
"eventsListeners": ["jboss-logging"],
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
"enabledEventTypes": [],
"adminEventsEnabled": false,
"adminEventsDetailsEnabled": false,
@ -1591,14 +1657,14 @@
"subComponents": {},
"config": {
"allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper"
"oidc-full-name-mapper"
]
}
},
@ -1629,13 +1695,13 @@
"config": {
"allowed-protocol-mapper-types": [
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper"
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper"
]
}
},
@ -1688,6 +1754,12 @@
"providerId": "rsa-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEoQIBAAKCAQEAxTFMvRiNiQjY9zajvLsah6Vy4pn8U7smsnBcHS9SkLJ1j9O8+90B90tIZk4IqEE4gdJA/mbbeUnou1vWuc0k69diQMFelzdIaDqJaFFeOS+J1DoApjThjGIz7FIgmGi6qoN8xnrPVD/6oMYAuxTvQaJH7mENiIG0198dvaufV1mFPg+krTsh7Womo2CJeZmNuAXv7RDQYxwPYDCFZLbppez48D7+2D+1V6Stk6Xwz8IDQZvljxDF6W2P9rhPWV1C5tcJpC/9RPyGDo+ke8UN3fM6X7YOgpbMztVrg8J0aTqPXZ7dt6QFUqVOufo+5wYL2jCafpYNV8cmaGlY+Q3d5QIDAQABAoH/DIPcaZaJTLG4FeUKGOaT40nesEiINRY99aeIkp+hdGj1EgTEn49TyLENGnhrrdbIvOJDeD6Z6dbpJBDvfFevxa589EnVKaGaaW5U91FDyVYH2YPU411dAeOp0z1xwxXzlJqX3h42ZJnvLAp/2l1Xo64vGCoTJtYlppAvpe2MjANxPNObAc65Phdi/sConAlwMeBylWXJ574uryFrJ64W/sUuIUMSunGGz0db4Y1hfkX9U2YnxB3DdXCBH09jQJyKDSj6feNXR87+1KhqcFMd5DUiGSAOqRBzuBMsDf1QDJd8A/DDlK7e/PA1Yk/Dii4hsf+LCeOdmhlifuyROqJBAoGBAOEm4gLvaBWwnUhmr4sW8xywIhGGbU+MX6vm/KkGtScres7pPhmfy6ARUzCxxyBqIE+nhCRNBpOEPhP7dv8naJhZZ4fRvNzuXpUMT2X3bc5yNzdhaOxBJl95YQbrYUHhjcIw2kdXnIkpdbB/RqmY0F5BUTYECrd0tKWbjuL5RIRNAoGBAOA1wTXrYyVorouxV+mGNb62Py+utHJQKSa5cxF9nbbwWJd+FdreiBOJddjATmH8ovKjueQFVqK7koDveOb+pgRY2bpT88/NW8UF6a2wMiI0p6pxrR+hgzas480YiOCWr6XlsprqsSKBbEu4W97GicleZ6P5Iso/gBr9aHj9EWv5AoGAYhRzHj42RESUr4Zz8A5GR3f+z02U7rNCtfrAk80lOvP44ou+jqEKrib961d2XAt/GdPqf3nCZJ6WAFRp6Qq8yKkhrYvTTxbTwvAC4nNftTASF6DqeQiEc9DHUKFW08Ey5KYtYCitOx8BcqpvGNBF7NldTD+Ef5hqXT4fh4Z4r30CgYEAy2OYGMymTRowNKK06C+Kc62plhy6rnRPUESswLIeLwTKqOqE8t4pvOdWk0CoGjVusAOcLuA03jyfwvz5xTo96fWb1W4w31IgLJOXjqsmX2c6reCfNvFyMVgW8keOa4XmYu0C34uFEpMrZWkhVe7usVBFXjczuxptoI4+hnqzoikCgYBICBVR9Z7n2LvmWH19/Nnns8dsMn5peL7H6Mey76Lo9RMEMp4qhiJTqVZzWgxEyVjr0KFCHmdmwkTOm6A1yYmkqqXDdiJ9v4J4fXe0lRAoUoYPTOWynrCyd6uqq+3zlzTKW8jY9luywHq6msn07D636PvveeZ93DNCcO8Whw36rQ=="
],
"certificate": [
"MIICnTCCAYUCBgGTulJBzTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxMjEwMDExM1oXDTM0MTIxMjEwMDI1M1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMUxTL0YjYkI2Pc2o7y7GoelcuKZ/FO7JrJwXB0vUpCydY/TvPvdAfdLSGZOCKhBOIHSQP5m23lJ6Ltb1rnNJOvXYkDBXpc3SGg6iWhRXjkvidQ6AKY04YxiM+xSIJhouqqDfMZ6z1Q/+qDGALsU70GiR+5hDYiBtNffHb2rn1dZhT4PpK07Ie1qJqNgiXmZjbgF7+0Q0GMcD2AwhWS26aXs+PA+/tg/tVekrZOl8M/CA0Gb5Y8Qxeltj/a4T1ldQubXCaQv/UT8hg6PpHvFDd3zOl+2DoKWzM7Va4PCdGk6j12e3bekBVKlTrn6PucGC9owmn6WDVfHJmhpWPkN3eUCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATZXyOluloTj6Q/Mv0JjstfdvPQbzGFzWtULB1ttOJqQVL+IJoF8V79HIvfP9U5OYaOdYk9dDurQcd2hXvEtX+zQlLYGniRfJlFI7d+m6MDXa7/g1r+OmcvaiXX7O3ol7eJdymPKS79+PSWFsHk0JjfgRJ11jajOscYPoQ+IvxXgwuy6v7VHigsLnGnmmo+KWiKO6Cna6eilm6/awYXaoym4ky9S4T5+WaJwd/tH/n5VY77zyXaXfANd1hU/+4Ux/eaGVnoMAM4ud2emd4qCN2tQQ3HusIVl+5V+S8Uq1y54mBpXv6CAODDGDJeFa+cGPJUSLdv/ZT2F8yfDlDc4J6g=="
],
"priority": ["100"]
}
},
@ -1697,6 +1769,12 @@
"providerId": "rsa-enc-generated",
"subComponents": {},
"config": {
"privateKey": [
"MIIEogIBAAKCAQEAungL4osLyP8bE6MSKj8ZMJTG8WBh3K2/xB5BJYCYc7P1CIORZI9o/vKQx1QnP+CXkIKnnR2kzIzC0rnTqlIOkaZfhmSn50jG5vNBS9qPT+WU7Ue3qKxuWJFwcaFU5SEJawJHqnDPK+pktkkxkudeMHz6iaKPs+wKcbfrRJ6+3a3FqQQdHEQg4IjVU8pBZmag1c7JHayiM56OT5y6jmE5JvY60959iPrZPXSTMU3hNoiVwdyK6QwdK+/0wrO681VhIP+u2pe92nQ+hsgMSSQJegLx1UsEEyU87syblG+p3zAKSS+kt2nviV/a2cYiiME0LdlQ3lnKsQ4t1Y6yZBiS2QIDAQABAoIBABhozI18TC+kjWPVrfQPzHlakGxahJUBvZ+rojWJjutefE4AAxFZ4JG3KRKexoCLIuwM3monzkHkj0BMiRO7qCKS1+Bc3snc8gSbhUmrs6Tu1b7162nOIKfBainFx7oyx+vVIZKDL+t8xHBERpQHa4IHajiIKi2QUZGvVMHn0e5srkPK0eSMjb5Z5j61aFb8InQzs7tczr99ke4VavOPT1gmRWGnbTavUbw/zIQ9sxAuMiD2v0nrGlOLZrMhaqzsT6PjIWVCSZrWex1pin9gA4XwGZ39E7+zFWgg+2OX0dEvehVDluAQR0K4PBUknuL1LFFW8dpvCrUSTmGGQOSVuB0CgYEA+bQjbjTNiMTEfoxx/WvVDgtLRL/x9RVyeYTPia2TGNBwpEcU64lLMOwUt5X/QuGXayPr0EGAxMA8kwq/E8Wj2t9+SuqkGK9SIwvghi2fOh0KWghuQbKYMogG5hsJAI8+/mBIOJJ8pyh0RX58vaTlYctbThO22aVahhZQ2weaW58CgYEAvyu4vIe44/7F19Hjh2BW+9lHsHA2zwHvC5T1kFaEdBYEwGsLMW6leCsiEMfpc2Uq3k9+buZgVpTE5APs9cSJX1aUXEG5QHQmYDxAAMiTyvpj0o2cKbDi1A5QZCRo23lC+uDyR7g2zLDJuHek0uyCtd83hbgyxIVFUnfvI9EmfocCgYBtpcZxHEqspgrKrw1XBMTXl+oDVG4A+tv7tHAVutx+5vivim8LRox3/RLT0s/2JG2DJJDmL/1FaEyxHOTu37il4cHpT8Oi+0mMDikXgm0K7bmf81fHDY97kPPGk1SOpFg7BzhvbxPBqyfzZCmOdRwsp0l+rXV7ePqZKq9ynpIPbQKBgFO/LZC5zE9k/vrK4egeVjzCNNugbQJGkJf8S49Nt3y7YJ2Cx0aCeE6qZqP/T8/Tk/IL1RF0LuP/DDnvVlFcJen0Hc5EpIkN2Pnzqv4s4EHdavmEO9MvwE6xbppQMPdkqekJvlmY47jMAbKkBzq3jZNrFAGqbeMVlwbHr6V7LGflAoGANFbzOnUMJwUfIdoI9uEG2QOTAcBb7vzt9MurO67wiTexOYadOSlcV1lQX3RKR9mCFJwy4kud0TN0gD++Ggl10eNB6f8JOF95e5+tWrtz88xZ5EalBOMfh+ATdKq8Q9MBSWZvO9bizhW1dhZZds/QmHgEItdwsTKDAq1PEiXhD0c="
],
"certificate": [
"MIICnTCCAYUCBgGTulJDCDANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxMjEwMDExM1oXDTM0MTIxMjEwMDI1M1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALp4C+KLC8j/GxOjEio/GTCUxvFgYdytv8QeQSWAmHOz9QiDkWSPaP7ykMdUJz/gl5CCp50dpMyMwtK506pSDpGmX4Zkp+dIxubzQUvaj0/llO1Ht6isbliRcHGhVOUhCWsCR6pwzyvqZLZJMZLnXjB8+omij7PsCnG360Sevt2txakEHRxEIOCI1VPKQWZmoNXOyR2sojOejk+cuo5hOSb2OtPefYj62T10kzFN4TaIlcHciukMHSvv9MKzuvNVYSD/rtqXvdp0PobIDEkkCXoC8dVLBBMlPO7Mm5Rvqd8wCkkvpLdp74lf2tnGIojBNC3ZUN5ZyrEOLdWOsmQYktkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPhPdLFcXdQT4k06oXB06ZSJ8AkZNXLvQFWCHXI34OmrS2yTse+dLqrqehnC3kPwxElVmawoUVc1sbsk7fUnspfM+Xw20PaABZu4MO2m5TB98f1hEkezP9fSqgPeuWJgTL8ZW5kkZyiD3IaZoqyxzYXaFxKHhU455g+k2+DO+N6FreVKcYz12Q5EMaxZ6U1neZAo3vicNxM3/TA5V8sPK8+oKvon7v5OyjpOH0goJo9v/klKeUk36h4u2h1S67IhVSU7tfzVFYrpns1JhrwGZ2xavVqEoqX8zFp3GKz3yVXkwHRHlrzYkZoGn21rm5boXIP3wEB7yXZbXWTiUko/IFw=="
],
"priority": ["100"],
"algorithm": ["RSA-OAEP"]
}
@ -1707,6 +1785,8 @@
"providerId": "aes-generated",
"subComponents": {},
"config": {
"kid": ["c36222c6-6a43-4d32-9d44-d5d355e5cabd"],
"secret": ["rzL4qUQ7wTEkZDbgt595VA"],
"priority": ["100"]
}
},
@ -1716,6 +1796,10 @@
"providerId": "hmac-generated",
"subComponents": {},
"config": {
"kid": ["06532a54-c310-41c1-829c-58776ce2ab4a"],
"secret": [
"9v1ZjFhEFH6UpY6ncFkaCbqJYHMyI4tA0cvx4GuQ5KtMXYbimitSSVDqxIKwa-gBC_8bY2O4FQfpmp1Qn1-L4fFmPFfIF3ZKsO16263BwpADo_FNSBTte8Le4gJLylqFULdsn3ye17FHyq5Jjms_OTt3opzcDLNduCuK22GBBsU"
],
"priority": ["100"],
"algorithm": ["HS512"]
}
@ -2385,7 +2469,7 @@
"clientSessionMaxLifespan": "0",
"organizationsEnabled": "false"
},
"keycloakVersion": "26.0.6",
"keycloakVersion": "26.0.7",
"userManagedAccessAllowed": false,
"organizationsEnabled": false,
"clientProfiles": {

View File

@ -0,0 +1,194 @@
import { CONTAINER_NAME } from "../../shared/constants";
import child_process from "child_process";
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert, is } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson";
export type BuildContextLike = {
cacheDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function dumpContainerConfig(params: {
realmName: string;
keycloakMajorVersionNumber: number;
buildContext: BuildContextLike;
}): Promise<ParsedRealmJson> {
const { realmName, keycloakMajorVersionNumber, buildContext } = params;
// https://github.com/keycloak/keycloak/issues/33800
const doesUseLockedH2Database = keycloakMajorVersionNumber >= 25;
if (doesUseLockedH2Database) {
const dCompleted = new Deferred<void>();
const cmd = `docker exec ${CONTAINER_NAME} sh -c "cp -rp /opt/keycloak/data/h2 /tmp"`;
child_process.exec(cmd, error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
dCompleted.resolve();
});
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
{
const dCompleted = new Deferred<void>();
const child = child_process.spawn(
"docker",
[
...["exec", CONTAINER_NAME],
...["/opt/keycloak/bin/kc.sh", "export"],
...["--dir", "/tmp"],
...["--realm", realmName],
...["--users", "realm_file"],
...(!doesUseLockedH2Database
? []
: [
...["--db", "dev-file"],
...[
"--db-url",
'"jdbc:h2:file:/tmp/h2/keycloakdb;NON_KEYWORDS=VALUE"'
]
])
],
{ shell: true }
);
let output = "";
const onExit = (code: number | null) => {
dCompleted.reject(
new Error(`docker exec kc.sh export command failed with code ${code}`)
);
};
child.once("exit", onExit);
child.stdout.on("data", data => {
const outputStr = data.toString("utf8");
if (outputStr.includes("Export finished successfully")) {
child.removeListener("exit", onExit);
// NOTE: On older Keycloak versions the process keeps running after the export is done.
const timer = setTimeout(() => {
child.removeListener("exit", onExit2);
child.kill();
dCompleted.resolve();
}, 1500);
const onExit2 = () => {
clearTimeout(timer);
dCompleted.resolve();
};
child.once("exit", onExit2);
}
output += outputStr;
});
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(error.message));
console.log(output);
throw error;
}
}
if (doesUseLockedH2Database) {
const dCompleted = new Deferred<void>();
const cmd = `docker exec ${CONTAINER_NAME} sh -c "rm -rf /tmp/h2"`;
child_process.exec(cmd, error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
dCompleted.resolve();
});
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
const targetRealmConfigJsonFilePath_tmp = pathJoin(
buildContext.cacheDirPath,
"realm.json"
);
{
const dCompleted = new Deferred<void>();
const cmd = `docker cp ${CONTAINER_NAME}:/tmp/${realmName}-realm.json ${pathBasename(targetRealmConfigJsonFilePath_tmp)}`;
child_process.exec(
cmd,
{
cwd: pathDirname(targetRealmConfigJsonFilePath_tmp)
},
error => {
if (error !== null) {
dCompleted.reject(error);
return;
}
dCompleted.resolve();
}
);
try {
await dCompleted.pr;
} catch (error) {
assert(is<Error>(error));
console.log(chalk.red(`Docker command failed: ${cmd}`));
console.log(chalk.red(error.message));
throw error;
}
}
return readRealmJsonFile({
realmJsonFilePath: targetRealmConfigJsonFilePath_tmp
});
}

View File

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

View File

@ -0,0 +1,365 @@
import { assert } from "tsafe/assert";
import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "./defaultConfig";
import type { BuildContext } from "../../shared/buildContext";
import { objectKeys } from "tsafe/objectKeys";
import { TEST_APP_URL } from "../../shared/constants";
import { sameFactory } from "evt/tools/inDepth/same";
export type BuildContextLike = {
themeNames: BuildContext["themeNames"];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>;
export function prepareRealmConfig(params: {
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
buildContext: BuildContextLike;
}): {
realmName: string;
clientName: string;
username: string;
} {
const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
const { username } = addOrEditTestUser({
parsedRealmJson,
keycloakMajorVersionNumber
});
const { clientId } = addOrEditClient({
parsedRealmJson,
keycloakMajorVersionNumber
});
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
enableCustomThemes({
parsedRealmJson,
themeName: buildContext.themeNames[0],
implementedThemeTypes: buildContext.implementedThemeTypes
});
enable_custom_events_listeners: {
const name = "keycloakify-logging";
if (parsedRealmJson.eventsListeners.includes(name)) {
break enable_custom_events_listeners;
}
parsedRealmJson.eventsListeners.push(name);
parsedRealmJson.eventsListeners.sort();
}
return {
realmName: parsedRealmJson.realm,
clientName: clientId,
username
};
}
function enableCustomThemes(params: {
parsedRealmJson: ParsedRealmJson;
themeName: string;
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
}) {
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
for (const themeType of objectKeys(implementedThemeTypes)) {
if (!implementedThemeTypes[themeType].isImplemented) {
continue;
}
parsedRealmJson[`${themeType}Theme` as const] = themeName;
}
}
function addOrEditTestUser(params: {
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
}): { username: string } {
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
const [defaultUser_default] = parsedRealmJson_default.users;
assert(defaultUser_default !== undefined);
const defaultUser_preexisting = parsedRealmJson.users.find(
user => user.username === defaultUser_default.username
);
const newUser = structuredClone(
defaultUser_preexisting ??
(() => {
const firstUser = parsedRealmJson.users[0];
if (firstUser === undefined) {
return undefined;
}
const firstUserCopy = structuredClone(firstUser);
firstUserCopy.id = defaultUser_default.id;
return firstUserCopy;
})() ??
defaultUser_default
);
newUser.username = defaultUser_default.username;
newUser.email = defaultUser_default.email;
delete_existing_password_credential_if_any: {
const i = newUser.credentials.findIndex(
credential => credential.type === "password"
);
if (i === -1) {
break delete_existing_password_credential_if_any;
}
newUser.credentials.splice(i, 1);
}
{
const credential = defaultUser_default.credentials.find(
credential => credential.type === "password"
);
assert(credential !== undefined);
newUser.credentials.push(credential);
}
{
const nameByClientId = Object.fromEntries(
parsedRealmJson.clients.map(client => [client.id, client.clientId] as const)
);
const newClientRoles: NonNullable<
ParsedRealmJson["users"][number]["clientRoles"]
> = {};
for (const clientRole of Object.values(parsedRealmJson.roles.client).flat()) {
const clientName = nameByClientId[clientRole.containerId];
assert(clientName !== undefined);
(newClientRoles[clientName] ??= []).push(clientRole.name);
}
const { same: sameSet } = sameFactory({
takeIntoAccountArraysOrdering: false
});
for (const [clientName, roles] of Object.entries(newClientRoles)) {
keep_previous_ordering_if_possible: {
const roles_previous = newUser.clientRoles?.[clientName];
if (roles_previous === undefined) {
break keep_previous_ordering_if_possible;
}
if (!sameSet(roles_previous, roles)) {
break keep_previous_ordering_if_possible;
}
continue;
}
(newUser.clientRoles ??= {})[clientName] = roles;
}
}
if (defaultUser_preexisting === undefined) {
parsedRealmJson.users.push(newUser);
} else {
const i = parsedRealmJson.users.indexOf(defaultUser_preexisting);
assert(i !== -1);
parsedRealmJson.users[i] = newUser;
}
return { username: newUser.username };
}
function addOrEditClient(params: {
parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number;
}): { clientId: string } {
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
const testClient_default = (() => {
const clients = parsedRealmJson_default.clients.filter(client => {
return JSON.stringify(client).includes(TEST_APP_URL);
});
assert(clients.length === 1);
return clients[0];
})();
const clientIds_builtIn = parsedRealmJson_default.clients
.map(client => client.clientId)
.filter(clientId => clientId !== testClient_default.clientId);
const testClient_preexisting = (() => {
const clients = parsedRealmJson.clients
.filter(client => !clientIds_builtIn.includes(client.clientId))
.filter(client => client.protocol === "openid-connect");
{
const client = clients.find(
client => client.clientId === testClient_default.clientId
);
if (client !== undefined) {
return client;
}
}
{
const client = clients.find(
client =>
client.redirectUris?.find(redirectUri =>
redirectUri.startsWith(TEST_APP_URL)
) !== undefined
);
if (client !== undefined) {
return client;
}
}
const [client] = clients;
if (client === undefined) {
return undefined;
}
return client;
})();
let testClient: typeof testClient_default;
if (testClient_preexisting !== undefined) {
testClient = testClient_preexisting;
} else {
testClient = structuredClone(testClient_default);
delete testClient.protocolMappers;
parsedRealmJson.clients.push(testClient);
}
testClient.redirectUris = [
`${TEST_APP_URL}/*`,
"http://localhost*",
"http://127.0.0.1*"
]
.sort()
.reverse();
(testClient.attributes ??= {})["post.logout.redirect.uris"] = "+";
testClient.webOrigins = ["*"];
return { clientId: testClient.clientId };
}
function editAccountConsoleAndSecurityAdminConsole(params: {
parsedRealmJson: ParsedRealmJson;
}) {
const { parsedRealmJson } = params;
for (const clientId of ["account-console", "security-admin-console"] as const) {
const client = parsedRealmJson.clients.find(
client => client.clientId === clientId
);
assert(client !== undefined);
{
const arr = (client.redirectUris ??= []);
for (const value of ["http://localhost*", "http://127.0.0.1*"]) {
if (!arr.includes(value)) {
arr.push(value);
}
}
client.redirectUris?.sort().reverse();
}
(client.attributes ??= {})["post.logout.redirect.uris"] = "+";
client.webOrigins = ["*"];
admin_specific: {
if (clientId !== "security-admin-console") {
break admin_specific;
}
const protocolMapper_preexisting = client.protocolMappers?.find(
protocolMapper => {
if (protocolMapper.protocolMapper !== "oidc-hardcoded-claim-mapper") {
return false;
}
if (protocolMapper.protocol !== "openid-connect") {
return false;
}
if (protocolMapper.config === undefined) {
return false;
}
if (protocolMapper.config["claim.name"] !== "allowed-origins") {
return false;
}
return true;
}
);
let protocolMapper: NonNullable<typeof protocolMapper_preexisting>;
const config = {
"introspection.token.claim": "true",
"claim.value": '["*"]',
"userinfo.token.claim": "true",
"id.token.claim": "false",
"lightweight.claim": "true",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false"
};
if (protocolMapper_preexisting !== undefined) {
protocolMapper = protocolMapper_preexisting;
} else {
protocolMapper = {
id: "8fd0d584-7052-4d04-a615-d18a71050873",
name: "allowed-origins",
protocol: "openid-connect",
protocolMapper: "oidc-hardcoded-claim-mapper",
consentRequired: false,
config
};
(client.protocolMappers ??= []).push(protocolMapper);
}
assert(protocolMapper.config !== undefined);
if (config !== protocolMapper.config) {
Object.assign(protocolMapper.config, config);
}
}
}
}

View File

@ -0,0 +1,155 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { getDefaultConfig } from "./defaultConfig";
import {
prepareRealmConfig,
type BuildContextLike as BuildContextLike_prepareRealmConfig
} from "./prepareRealmConfig";
import * as fs from "fs";
import {
join as pathJoin,
dirname as pathDirname,
relative as pathRelative,
sep as pathSep
} from "path";
import { existsAsync } from "../../tools/fs.existsAsync";
import {
readRealmJsonFile,
writeRealmJsonFile,
type ParsedRealmJson
} from "./ParsedRealmJson";
import {
dumpContainerConfig,
type BuildContextLike as BuildContextLike_dumpContainerConfig
} from "./dumpContainerConfig";
import * as runExclusive from "run-exclusive";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import chalk from "chalk";
export type BuildContextLike = BuildContextLike_dumpContainerConfig &
BuildContextLike_prepareRealmConfig & {
projectDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>;
export async function getRealmConfig(params: {
keycloakMajorVersionNumber: number;
realmJsonFilePath_userProvided: string | undefined;
buildContext: BuildContextLike;
}): Promise<{
realmJsonFilePath: string;
clientName: string;
realmName: string;
username: string;
onRealmConfigChange: () => Promise<void>;
}> {
const { keycloakMajorVersionNumber, realmJsonFilePath_userProvided, buildContext } =
params;
const realmJsonFilePath = pathJoin(
buildContext.projectDirPath,
".keycloakify",
`realm-kc-${keycloakMajorVersionNumber}.json`
);
const parsedRealmJson = await (async () => {
if (realmJsonFilePath_userProvided !== undefined) {
return readRealmJsonFile({
realmJsonFilePath: realmJsonFilePath_userProvided
});
}
if (await existsAsync(realmJsonFilePath)) {
return readRealmJsonFile({
realmJsonFilePath
});
}
return getDefaultConfig({ keycloakMajorVersionNumber });
})();
const { clientName, realmName, username } = prepareRealmConfig({
parsedRealmJson,
buildContext,
keycloakMajorVersionNumber
});
{
const dirPath = pathDirname(realmJsonFilePath);
if (!(await existsAsync(dirPath))) {
fs.mkdirSync(dirPath, { recursive: true });
}
}
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
const { onRealmConfigChange } = (() => {
const run = runExclusive.build(async () => {
const start = Date.now();
console.log(
chalk.grey(`Changes detected to the '${realmName}' config, backing up...`)
);
let parsedRealmJson: ParsedRealmJson;
try {
parsedRealmJson = await dumpContainerConfig({
buildContext,
realmName,
keycloakMajorVersionNumber
});
} catch (error) {
console.log(chalk.red(`Failed to backup '${realmName}' config:`));
return;
}
await writeRealmJsonFile({
realmJsonFilePath,
parsedRealmJson,
keycloakMajorVersionNumber
});
console.log(
[
chalk.grey(
`Save changed to \`.${pathSep}${pathRelative(buildContext.projectDirPath, realmJsonFilePath)}\``
),
chalk.grey(
`Next time you'll be running \`keycloakify start-keycloak\`, the realm '${realmName}' will be restored to this state.`
),
chalk.green(
`✓ '${realmName}' config backed up completed in ${Date.now() - start}ms`
)
].join("\n")
);
});
const { waitForDebounce } = waitForDebounceFactory({
delay: 1_000
});
async function onRealmConfigChange() {
await waitForDebounce();
run();
}
return { onRealmConfigChange };
})();
return {
realmJsonFilePath,
clientName,
realmName,
username,
onRealmConfigChange
};
}

View File

@ -1,7 +1,11 @@
import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { CONTAINER_NAME, KEYCLOAKIFY_SPA_DEV_SERVER_PORT } from "../shared/constants";
import {
CONTAINER_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
TEST_APP_URL
} from "../shared/constants";
import { SemVer } from "../tools/SemVer";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
@ -9,8 +13,7 @@ import {
join as pathJoin,
relative as pathRelative,
sep as pathSep,
basename as pathBasename,
dirname as pathDirname
basename as pathBasename
} from "path";
import * as child_process from "child_process";
import chalk from "chalk";
@ -28,6 +31,9 @@ import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
import { startViteDevServer } from "./startViteDevServer";
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
import { getRealmConfig } from "./realmConfig";
export async function command(params: {
buildContext: BuildContext;
@ -91,9 +97,32 @@ export async function command(params: {
const { cliCommandOptions, buildContext } = params;
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
buildContext
});
const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
return { dockerImageTag: cliCommandOptions.keycloakVersion };
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
const tag = allSupportedTags.find(tag =>
tag.startsWith(cliCommandOptions_keycloakVersion)
);
if (tag === undefined) {
console.log(
chalk.red(
[
`We could not find a Keycloak Docker image for ${cliCommandOptions_keycloakVersion}`,
`Example of valid values: --keycloak-version 26, --keycloak-version 26.0.7`
].join("\n")
)
);
process.exit(1);
}
return { dockerImageTag: tag };
}
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
@ -108,50 +137,165 @@ export async function command(params: {
"On which version of Keycloak do you want to test your theme?"
),
chalk.gray(
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)"
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)"
)
].join("\n")
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 18,
excludeMajorVersions: [22],
doOmitPatch: true,
buildContext
});
const tag_userSelected = await (async () => {
let tag: string;
console.log(`${keycloakVersion}`);
let latestMajorTags_copy = [...latestMajorTags];
return { dockerImageTag: keycloakVersion };
while (true) {
const { value } = await cliSelect<string>({
values: latestMajorTags_copy
}).catch(() => {
process.exit(-1);
});
tag = value;
{
const doImplementAccountMpa =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Multi-Page";
if (doImplementAccountMpa && tag.startsWith("22.")) {
console.log(
chalk.yellow(
`You are implementing a Multi-Page Account theme. Keycloak 22 is not supported, select another version`
)
);
latestMajorTags_copy = latestMajorTags_copy.filter(
tag => !tag.startsWith("22.")
);
continue;
}
}
const readMajor = (tag: string) => {
const major = parseInt(tag.split(".")[0]);
assert(!isNaN(major));
return major;
};
{
const major = readMajor(tag);
const doImplementAdminTheme =
buildContext.implementedThemeTypes.admin.isImplemented;
const getIsSupported = (major: number) => major >= 23;
if (doImplementAdminTheme && !getIsSupported(major)) {
console.log(
chalk.yellow(
`You are implementing an Admin theme. Only Keycloak 23 and later are supported, select another version`
)
);
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
getIsSupported(readMajor(tag))
);
continue;
}
}
{
const doImplementAccountSpa =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page";
const major = readMajor(tag);
const getIsSupported = (major: number) => major >= 19;
if (doImplementAccountSpa && !getIsSupported(major)) {
console.log(
chalk.yellow(
`You are implementing a Single-Page Account theme. Only Keycloak 19 and later are supported, select another version`
)
);
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
getIsSupported(readMajor(tag))
);
continue;
}
}
break;
}
return tag;
})();
console.log(`${tag_userSelected}`);
return { dockerImageTag: tag_userSelected };
})();
const keycloakMajorVersionNumber = (() => {
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
return SemVer.parse(dockerImageTag).major;
}
const { tag } = buildContext.startKeycloakOptions.dockerImage;
const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
const [wrap] = getSupportedKeycloakMajorVersions()
.map(majorVersionNumber => ({
majorVersionNumber,
index: tag.indexOf(`${majorVersionNumber}`)
index: dockerImageTag.indexOf(`${majorVersionNumber}`)
}))
.filter(({ index }) => index !== -1)
.sort((a, b) => a.index - b.index);
if (wrap === undefined) {
console.warn(
chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
)
);
return 25;
try {
const version = SemVer.parse(dockerImageTag);
console.error(
chalk.yellow(
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
)
);
process.exit(1);
} catch {
// NOTE: Latest version
const [n] = getSupportedKeycloakMajorVersions();
console.warn(
chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming ${n}`
)
);
return n;
}
}
return wrap.majorVersionNumber;
})();
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
await getRealmConfig({
keycloakMajorVersionNumber,
realmJsonFilePath_userProvided: await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
});
}
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
assert(
await existsAsync(
buildContext.startKeycloakOptions.realmJsonFilePath
),
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
);
return buildContext.startKeycloakOptions.realmJsonFilePath;
}
return undefined;
})(),
buildContext
});
{
const { isAppBuildSuccess } = await appBuild({
buildContext
@ -189,154 +333,48 @@ export async function command(params: {
assert(jarFilePath !== undefined);
const extensionJarFilePaths = await Promise.all(
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
switch (extensionJar.type) {
case "path": {
assert(
await existsAsync(extensionJar.path),
`${extensionJar.path} does not exist`
);
return extensionJar.path;
const extensionJarFilePaths = [
...(keycloakMajorVersionNumber <= 20
? (console.log(
chalk.yellow(
"WARNING: With older version of keycloak your changes to the realm configuration are not persisted"
)
),
[])
: [
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
KEYCLOAKIFY_LOGIN_JAR_BASENAME
)
]),
...(await Promise.all(
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
switch (extensionJar.type) {
case "path": {
assert(
await existsAsync(extensionJar.path),
`${extensionJar.path} does not exist`
);
return extensionJar.path;
}
case "url": {
const { archiveFilePath } = await downloadAndExtractArchive({
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
url: extensionJar.url,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
return archiveFilePath;
}
}
case "url": {
const { archiveFilePath } = await downloadAndExtractArchive({
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
url: extensionJar.url,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
return archiveFilePath;
}
}
assert<Equals<typeof extensionJar, never>>(false);
})
);
const getRealmJsonFilePath_defaultForKeycloakMajor = (
keycloakMajorVersionNumber: number
) =>
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
);
const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) {
if (cliCommandOptions.realmJsonFilePath === "none") {
return undefined;
}
return getAbsoluteAndInOsFormatPath({
pathIsh: cliCommandOptions.realmJsonFilePath,
cwd: process.cwd()
});
}
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
assert(
await existsAsync(buildContext.startKeycloakOptions.realmJsonFilePath),
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
);
return buildContext.startKeycloakOptions.realmJsonFilePath;
}
const internalFilePath = await (async () => {
const defaultFilePath = getRealmJsonFilePath_defaultForKeycloakMajor(
keycloakMajorVersionNumber
);
if (fs.existsSync(defaultFilePath)) {
return defaultFilePath;
}
console.log(
`${chalk.yellow(
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
)}`
);
console.log(chalk.cyan("Select what configuration to use:"));
const dirPath = pathDirname(defaultFilePath);
const { value } = await cliSelect<string>({
values: [
...fs
.readdirSync(dirPath)
.filter(fileBasename => fileBasename.endsWith(".json")),
"none"
]
}).catch(() => {
process.exit(-1);
});
if (value === "none") {
return undefined;
}
return pathJoin(dirPath, value);
})();
if (internalFilePath === undefined) {
return undefined;
}
const filePath = pathJoin(
buildContext.cacheDirPath,
pathBasename(internalFilePath)
);
fs.writeFileSync(
filePath,
Buffer.from(
fs
.readFileSync(internalFilePath)
.toString("utf8")
.replace(/keycloakify\-starter/g, buildContext.themeNames[0])
),
"utf8"
);
return filePath;
})();
add_test_user_if_missing: {
if (realmJsonFilePath === undefined) {
break add_test_user_if_missing;
}
const realm: Record<string, unknown> = JSON.parse(
fs.readFileSync(realmJsonFilePath).toString("utf8")
);
if (realm.users !== undefined) {
break add_test_user_if_missing;
}
const realmJsonFilePath_internal = (() => {
const filePath = getRealmJsonFilePath_defaultForKeycloakMajor(
keycloakMajorVersionNumber
);
if (!fs.existsSync(filePath)) {
return getRealmJsonFilePath_defaultForKeycloakMajor(25);
}
return filePath;
})();
const users = JSON.parse(
fs.readFileSync(realmJsonFilePath_internal).toString("utf8")
).users;
realm.users = users;
fs.writeFileSync(realmJsonFilePath, JSON.stringify(realm, null, 2), "utf8");
}
assert<Equals<typeof extensionJar, never>>(false);
})
))
];
async function extractThemeResourcesFromJar() {
await extractArchive({
@ -376,18 +414,16 @@ export async function command(params: {
});
} catch {}
const DEFAULT_PORT = 8080;
const port =
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
const devServerPort = (() => {
const doStartDevServer = (() => {
const hasSpaUi =
buildContext.implementedThemeTypes.admin.isImplemented ||
(buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page");
if (!hasSpaUi) {
return undefined;
return false;
}
if (buildContext.bundler !== "vite") {
@ -401,7 +437,7 @@ export async function command(params: {
)
);
return undefined;
return false;
}
if (keycloakMajorVersionNumber < 25) {
@ -415,17 +451,18 @@ export async function command(params: {
)
);
return undefined;
return false;
}
return port + 1;
return true;
})();
if (devServerPort !== undefined) {
startViteDevServer({
buildContext,
port: devServerPort
});
let devServerPort: number | undefined = undefined;
if (doStartDevServer) {
const { port } = await startViteDevServer({ buildContext });
devServerPort = port;
}
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
@ -433,8 +470,15 @@ export async function command(params: {
const dockerRunArgs: string[] = [
`-p${SPACE_PLACEHOLDER}${port}:8080`,
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
...(keycloakMajorVersionNumber >= 26
? [
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_USERNAME=admin`,
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_PASSWORD=admin`
]
: [
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`
]),
...(devServerPort === undefined
? []
: [
@ -450,7 +494,7 @@ export async function command(params: {
...(realmJsonFilePath === undefined
? []
: [
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json`
]),
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map(
@ -525,7 +569,14 @@ export async function command(params: {
{ shell: true }
);
child.stdout.on("data", data => process.stdout.write(data));
child.stdout.on("data", async data => {
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
await onRealmConfigChange();
return;
}
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data));
@ -572,9 +623,9 @@ export async function command(params: {
`${chalk.green("Your theme is accessible at:")}`,
`${chalk.green("➜")} ${chalk.cyan.bold(
(() => {
const url = new URL("https://my-theme.keycloakify.dev");
const url = new URL(TEST_APP_URL);
if (port !== DEFAULT_PORT) {
if (port !== 8080) {
url.searchParams.set("port", `${port}`);
}
if (kcHttpRelativePath !== undefined) {
@ -583,13 +634,20 @@ export async function command(params: {
kcHttpRelativePath
);
}
if (realmName !== "myrealm") {
url.searchParams.set("realm", realmName);
}
if (clientName !== "myclient") {
url.searchParams.set("client", clientName);
}
return url.href;
})()
)}`,
"",
"You can login with the following credentials:",
`- username: ${chalk.cyan.bold("testuser")}`,
`- username: ${chalk.cyan.bold(username)}`,
`- password: ${chalk.cyan.bold("password123")}`,
"",
`Watching for changes in ${chalk.bold(
@ -646,42 +704,56 @@ 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;
ignore_path_covered_by_hmr: {
if (filePath.endsWith(".properties")) {
break ignore_path_covered_by_hmr;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
filePath
})
) {
break ignore_account_spa;
if (!doStartDevServer) {
break ignore_path_covered_by_hmr;
}
return;
}
ignore_account_spa: {
const doImplementAccountSpa =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type ===
"Single-Page";
ignore_admin: {
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
break ignore_admin;
if (!doImplementAccountSpa) {
break ignore_account_spa;
}
if (
!isInside({
dirPath: pathJoin(
buildContext.themeSrcDirPath,
"account"
),
filePath
})
) {
break ignore_account_spa;
}
return;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
filePath
})
) {
break ignore_admin;
}
ignore_admin: {
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
break ignore_admin;
}
return;
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
filePath
})
) {
break ignore_admin;
}
return;
}
}
console.log(`Detected changes in ${filePath}`);

View File

@ -3,6 +3,7 @@ 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";
import { Deferred } from "evt/tools/Deferred";
export type BuildContextLike = {
projectDirPath: string;
@ -12,13 +13,12 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export function startViteDevServer(params: {
buildContext: BuildContextLike;
port: number;
}): void {
const { buildContext, port } = params;
}): Promise<{ port: number }> {
const { buildContext } = params;
console.log(chalk.blue(`$ npx vite dev --port ${port}`));
console.log(chalk.blue(`$ npx vite dev`));
const child = child_process.spawn("npx", ["vite", "dev", "--port", `${port}`], {
const child = child_process.spawn("npx", ["vite", "dev"], {
cwd: buildContext.projectDirPath,
env: {
...process.env,
@ -36,4 +36,32 @@ export function startViteDevServer(params: {
});
child.stderr.on("data", data => process.stderr.write(data));
const dPort = new Deferred<number>();
{
const onData = (data: Buffer) => {
//Local: http://localhost:8083/
const match = data
.toString("utf8")
.replace(/\x1b[[0-9;]*m/g, "")
.match(/Local:\s*http:\/\/(?:localhost|127\.0\.0\.1):(\d+)\//);
if (match === null) {
return;
}
child.stdout.off("data", onData);
const port = parseInt(match[1]);
assert(!isNaN(port));
dPort.resolve(port);
};
child.stdout.on("data", onData);
}
return dPort.pr.then(port => ({ port }));
}

View File

@ -10,14 +10,15 @@ 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";
getExtensionModuleFileSourceCodeReadyToBeCopied,
type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
} from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
import * as crypto from "crypto";
import { KEYCLOAK_THEME } from "../shared/constants";
import { exclude } from "tsafe/exclude";
import { isAmong } from "tsafe/isAmong";
export type UiModuleMeta = {
export type ExtensionModuleMeta = {
moduleName: string;
version: string;
files: {
@ -28,8 +29,8 @@ export type UiModuleMeta = {
peerDependencies: Record<string, string>;
};
const zUiModuleMeta = (() => {
type ExpectedType = UiModuleMeta;
const zExtensionModuleMeta = (() => {
type ExpectedType = ExtensionModuleMeta;
const zTargetType = z.object({
moduleName: z.string(),
@ -55,7 +56,7 @@ type ParsedCacheFile = {
keycloakifyVersion: string;
prettierConfigHash: string | null;
thisFilePath: string;
uiModuleMetas: UiModuleMeta[];
extensionModuleMetas: ExtensionModuleMeta[];
};
const zParsedCacheFile = (() => {
@ -65,7 +66,7 @@ const zParsedCacheFile = (() => {
keycloakifyVersion: z.string(),
prettierConfigHash: z.union([z.string(), z.null()]),
thisFilePath: z.string(),
uiModuleMetas: z.array(zUiModuleMeta)
extensionModuleMetas: z.array(zExtensionModuleMeta)
});
type InferredType = z.infer<typeof zTargetType>;
@ -75,10 +76,10 @@ const zParsedCacheFile = (() => {
return id<z.ZodType<ExpectedType>>(zTargetType);
})();
const CACHE_FILE_RELATIVE_PATH = pathJoin("ui-modules", "cache.json");
const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
export type BuildContextLike =
BuildContextLike_getUiModuleFileSourceCodeReadyToBeCopied & {
BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
cacheDirPath: string;
packageJsonFilePath: string;
projectDirPath: string;
@ -86,9 +87,9 @@ export type BuildContextLike =
assert<BuildContext extends BuildContextLike ? true : false>();
export async function getUiModuleMetas(params: {
export async function getExtensionModuleMetas(params: {
buildContext: BuildContextLike;
}): Promise<UiModuleMeta[]> {
}): Promise<ExtensionModuleMeta[]> {
const { buildContext } = params;
const cacheFilePath = pathJoin(buildContext.cacheDirPath, CACHE_FILE_RELATIVE_PATH);
@ -105,10 +106,9 @@ export async function getUiModuleMetas(params: {
return configHash;
})();
const installedUiModules = await (async () => {
const installedExtensionModules = await (async () => {
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
packageJsonFilePath: buildContext.packageJsonFilePath,
projectDirPath: buildContext.packageJsonFilePath,
filter: ({ moduleName }) =>
moduleName.includes("keycloakify") && moduleName !== "keycloakify"
});
@ -133,7 +133,7 @@ export async function getUiModuleMetas(params: {
return await fsPr.readFile(cacheFilePath);
})();
const uiModuleMetas_cacheUpToDate: UiModuleMeta[] = await (async () => {
const extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = await (async () => {
const parsedCacheFile: ParsedCacheFile | undefined = await (async () => {
if (cacheContent === undefined) {
return undefined;
@ -176,45 +176,51 @@ export async function getUiModuleMetas(params: {
return [];
}
const uiModuleMetas_cacheUpToDate = parsedCacheFile.uiModuleMetas.filter(
uiModuleMeta => {
const correspondingInstalledUiModule = installedUiModules.find(
installedUiModule =>
installedUiModule.moduleName === uiModuleMeta.moduleName
);
const extensionModuleMetas_cacheUpToDate =
parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
const correspondingInstalledExtensionModule =
installedExtensionModules.find(
installedExtensionModule =>
installedExtensionModule.moduleName ===
extensionModuleMeta.moduleName
);
if (correspondingInstalledUiModule === undefined) {
if (correspondingInstalledExtensionModule === undefined) {
return false;
}
return correspondingInstalledUiModule.version === uiModuleMeta.version;
}
);
return (
correspondingInstalledExtensionModule.version ===
extensionModuleMeta.version
);
});
return uiModuleMetas_cacheUpToDate;
return extensionModuleMetas_cacheUpToDate;
})();
const uiModuleMetas = await Promise.all(
installedUiModules.map(
const extensionModuleMetas = await Promise.all(
installedExtensionModules.map(
async ({
moduleName,
version,
peerDependencies,
dirPath
}): Promise<UiModuleMeta> => {
}): Promise<ExtensionModuleMeta> => {
use_cache: {
const uiModuleMeta_cache = uiModuleMetas_cacheUpToDate.find(
uiModuleMeta => uiModuleMeta.moduleName === moduleName
);
const extensionModuleMeta_cache =
extensionModuleMetas_cacheUpToDate.find(
extensionModuleMeta =>
extensionModuleMeta.moduleName === moduleName
);
if (uiModuleMeta_cache === undefined) {
if (extensionModuleMeta_cache === undefined) {
break use_cache;
}
return uiModuleMeta_cache;
return extensionModuleMeta_cache;
}
const files: UiModuleMeta["files"] = [];
const files: ExtensionModuleMeta["files"] = [];
{
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
@ -224,13 +230,13 @@ export async function getUiModuleMetas(params: {
returnedPathsType: "relative to dirPath",
onFileFound: async fileRelativePath => {
const sourceCode =
await getUiModuleFileSourceCodeReadyToBeCopied({
await getExtensionModuleFileSourceCodeReadyToBeCopied({
buildContext,
fileRelativePath,
isForEjection: false,
uiModuleDirPath: dirPath,
uiModuleName: moduleName,
uiModuleVersion: version
isOwnershipAction: false,
extensionModuleDirPath: dirPath,
extensionModuleName: moduleName,
extensionModuleVersion: version
});
const hash = computeHash(sourceCode);
@ -260,11 +266,16 @@ export async function getUiModuleMetas(params: {
});
}
return id<UiModuleMeta>({
return id<ExtensionModuleMeta>({
moduleName,
version,
files,
peerDependencies
peerDependencies: Object.fromEntries(
Object.entries(peerDependencies).filter(
([moduleName]) =>
!isAmong(["react", "@types/react"], moduleName)
)
)
});
}
)
@ -275,7 +286,7 @@ export async function getUiModuleMetas(params: {
keycloakifyVersion,
prettierConfigHash,
thisFilePath: cacheFilePath,
uiModuleMetas
extensionModuleMetas
});
const cacheContent_new = Buffer.from(
@ -300,7 +311,7 @@ export async function getUiModuleMetas(params: {
await fsPr.writeFile(cacheFilePath, cacheContent_new);
}
return uiModuleMetas;
return extensionModuleMetas;
}
export function computeHash(data: Buffer) {

View File

@ -0,0 +1,135 @@
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 getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
buildContext: BuildContextLike;
fileRelativePath: string;
isOwnershipAction: boolean;
extensionModuleDirPath: string;
extensionModuleName: string;
extensionModuleVersion: string;
}): Promise<Buffer> {
const {
buildContext,
extensionModuleDirPath,
fileRelativePath,
isOwnershipAction,
extensionModuleName,
extensionModuleVersion
} = params;
let sourceCode = (
await fsPr.readFile(
pathJoin(extensionModuleDirPath, KEYCLOAK_THEME, fileRelativePath)
)
).toString("utf8");
sourceCode = addCommentToSourceCode({
sourceCode,
fileRelativePath,
commentLines: (() => {
const path = fileRelativePath.split(pathSep).join("/");
return isOwnershipAction
? [
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
`To relinquish ownership and restore this file to its original content, run the following command:`,
``,
`$ npx keycloakify own --path '${path}' --revert`
]
: [
`WARNING: Before modifying this file, run the following command:`,
``,
`$ npx keycloakify own --path '${path}'`,
``,
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
];
})()
});
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
sourceCode = await runPrettier({
filePath: destFilePath,
sourceCode
});
}
return Buffer.from(sourceCode, "utf8");
}
function addCommentToSourceCode(params: {
sourceCode: string;
fileRelativePath: string;
commentLines: string[];
}): string {
const { sourceCode, fileRelativePath, commentLines } = params;
const toResult = (comment: string) => {
return [comment, ``, sourceCode].join("\n");
};
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
if (!fileRelativePath.endsWith(ext)) {
continue;
}
return toResult(
[`/**`, ...commentLines.map(line => ` * ${line}`), ` */`].join("\n")
);
}
if (fileRelativePath.endsWith(".properties")) {
return toResult(commentLines.map(line => `# ${line}`).join("\n"));
}
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
const comment = [
`<!--`,
...commentLines.map(
line =>
` ${line
.replace("--path", "-t")
.replace("--revert", "-r")
.replace("Before modifying", "Before modifying or replacing")}`
),
`-->`
].join("\n");
if (fileRelativePath.endsWith(".html") && sourceCode.trim().startsWith("<!")) {
const [first, ...rest] = sourceCode.split(">");
const last = rest.join(">");
return [`${first}>`, comment, last].join("\n");
}
if (fileRelativePath.endsWith(".svg") && sourceCode.trim().startsWith("<?")) {
const [first, ...rest] = sourceCode.split("?>");
const last = rest.join("?>");
return [`${first}?>`, comment, last].join("\n");
}
return toResult(comment);
}
return sourceCode;
}

View File

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

View File

@ -1,6 +1,6 @@
import { assert, type Equals, is } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta";
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
import { z } from "zod";
import { id } from "tsafe/id";
import * as fsPr from "fs/promises";
@ -16,29 +16,29 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>();
export type UiModuleMetaLike = {
export type ExtensionModuleMetaLike = {
moduleName: string;
peerDependencies: Record<string, string>;
};
assert<UiModuleMeta extends UiModuleMetaLike ? true : false>();
assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
export async function installUiModulesPeerDependencies(params: {
export async function installExtensionModulesPeerDependencies(params: {
buildContext: BuildContextLike;
uiModuleMetas: UiModuleMetaLike[];
extensionModuleMetas: ExtensionModuleMetaLike[];
}): Promise<void | never> {
const { buildContext, uiModuleMetas } = params;
const { buildContext, extensionModuleMetas } = params;
const { uiModulesPerDependencies } = (() => {
const uiModulesPerDependencies: Record<string, string> = {};
const { extensionModulesPerDependencies } = (() => {
const extensionModulesPerDependencies: Record<string, string> = {};
for (const { peerDependencies } of uiModuleMetas) {
for (const { peerDependencies } of extensionModuleMetas) {
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
peerDependencies
)) {
const versionRange = (() => {
const versionRange_current =
uiModulesPerDependencies[peerDependencyName];
extensionModulesPerDependencies[peerDependencyName];
if (versionRange_current === undefined) {
return versionRange_candidate;
@ -76,11 +76,11 @@ export async function installUiModulesPeerDependencies(params: {
return versionRange;
})();
uiModulesPerDependencies[peerDependencyName] = versionRange;
extensionModulesPerDependencies[peerDependencyName] = versionRange;
}
}
return { uiModulesPerDependencies };
return { extensionModulesPerDependencies };
})();
const parsedPackageJson = await (async () => {
@ -117,7 +117,9 @@ export async function installUiModulesPeerDependencies(params: {
const parsedPackageJson_before = JSON.parse(JSON.stringify(parsedPackageJson));
for (const [moduleName, versionRange] of Object.entries(uiModulesPerDependencies)) {
for (const [moduleName, versionRange] of Object.entries(
extensionModulesPerDependencies
)) {
if (moduleName.startsWith("@types/")) {
(parsedPackageJson.devDependencies ??= {})[moduleName] = versionRange;
continue;
@ -149,7 +151,7 @@ export async function installUiModulesPeerDependencies(params: {
await fsPr.writeFile(buildContext.packageJsonFilePath, packageJsonContentStr);
npmInstall({
await npmInstall({
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
});

View File

@ -7,7 +7,7 @@ import {
} from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import type { UiModuleMeta } from "./uiModuleMeta";
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
import { existsAsync } from "../tools/fs.existsAsync";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
@ -17,17 +17,17 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>();
const DELIMITER_START = `# === Ejected files start ===`;
const DELIMITER_END = `# === Ejected files end =====`;
const DELIMITER_START = `# === Owned files start ===`;
const DELIMITER_END = `# === Owned files end =====`;
export async function writeManagedGitignoreFile(params: {
buildContext: BuildContextLike;
uiModuleMetas: UiModuleMeta[];
ejectedFilesRelativePaths: string[];
extensionModuleMetas: ExtensionModuleMeta[];
ownedFilesRelativePaths: string[];
}): Promise<void> {
const { buildContext, uiModuleMetas, ejectedFilesRelativePaths } = params;
const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
if (uiModuleMetas.length === 0) {
if (extensionModuleMetas.length === 0) {
return;
}
@ -38,19 +38,19 @@ export async function writeManagedGitignoreFile(params: {
`# This file is managed by Keycloakify, do not edit it manually.`,
``,
DELIMITER_START,
...ejectedFilesRelativePaths
...ownedFilesRelativePaths
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
.map(line => `# ${line}`),
DELIMITER_END,
``,
...uiModuleMetas
.map(uiModuleMeta => [
`# === ${uiModuleMeta.moduleName} v${uiModuleMeta.version} ===`,
...uiModuleMeta.files
...extensionModuleMetas
.map(extensionModuleMeta => [
`# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
...extensionModuleMeta.files
.map(({ fileRelativePath }) => fileRelativePath)
.filter(
fileRelativePath =>
!ejectedFilesRelativePaths.includes(fileRelativePath)
!ownedFilesRelativePaths.includes(fileRelativePath)
)
.map(
fileRelativePath =>
@ -92,14 +92,14 @@ export async function writeManagedGitignoreFile(params: {
export async function readManagedGitignoreFile(params: {
buildContext: BuildContextLike;
}): Promise<{
ejectedFilesRelativePaths: string[];
ownedFilesRelativePaths: string[];
}> {
const { buildContext } = params;
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
if (!(await existsAsync(filePath))) {
return { ejectedFilesRelativePaths: [] };
return { ownedFilesRelativePaths: [] };
}
const contentStr = (await fsPr.readFile(filePath)).toString("utf8");
@ -116,10 +116,10 @@ export async function readManagedGitignoreFile(params: {
})();
if (payload === undefined) {
return { ejectedFilesRelativePaths: [] };
return { ownedFilesRelativePaths: [] };
}
const ejectedFilesRelativePaths = payload
const ownedFilesRelativePaths = payload
.split("\n")
.map(line => line.trim())
.map(line => line.replace(/^# /, ""))
@ -132,5 +132,5 @@ export async function readManagedGitignoreFile(params: {
)
.map(filePath => pathRelative(buildContext.themeSrcDirPath, filePath));
return { ejectedFilesRelativePaths };
return { ownedFilesRelativePaths };
}

View File

@ -1,6 +1,6 @@
import type { BuildContext } from "../shared/buildContext";
import { getUiModuleMetas, computeHash } from "./uiModuleMeta";
import { installUiModulesPeerDependencies } from "./installUiModulesPeerDependencies";
import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
import {
readManagedGitignoreFile,
writeManagedGitignoreFile
@ -9,36 +9,36 @@ 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 { getIsKnownByGit } from "../tools/isKnownByGit";
import { untrackFromGit } from "../tools/untrackFromGit";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
const uiModuleMetas = await getUiModuleMetas({ buildContext });
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
await installUiModulesPeerDependencies({
await installExtensionModulesPeerDependencies({
buildContext,
uiModuleMetas
extensionModuleMetas
});
const { ejectedFilesRelativePaths } = await readManagedGitignoreFile({
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
buildContext
});
await writeManagedGitignoreFile({
buildContext,
ejectedFilesRelativePaths,
uiModuleMetas
ownedFilesRelativePaths,
extensionModuleMetas
});
await Promise.all(
uiModuleMetas
.map(uiModuleMeta =>
extensionModuleMetas
.map(extensionModuleMeta =>
Promise.all(
uiModuleMeta.files.map(
extensionModuleMeta.files.map(
async ({ fileRelativePath, copyableFilePath, hash }) => {
if (ejectedFilesRelativePaths.includes(fileRelativePath)) {
if (ownedFilesRelativePaths.includes(fileRelativePath)) {
return;
}
@ -65,19 +65,7 @@ export async function command(params: { buildContext: BuildContext }) {
return;
}
git_untrack: {
if (!doesFileExist) {
break git_untrack;
}
const isTracked = await getIsTrackedByGit({
filePath: destFilePath
});
if (!isTracked) {
break git_untrack;
}
if (await getIsKnownByGit({ filePath: destFilePath })) {
await untrackFromGit({
filePath: destFilePath
});

View File

@ -1,12 +0,0 @@
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
[Key in keyof T]: undefined extends T[Key] ? Key : never;
}[keyof T];
/**
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
* is
* { p1?: string | undefined; p2: string }
*/
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };

View File

@ -0,0 +1,99 @@
import { z } from "zod";
import { same } from "evt/tools/inDepth/same";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
export type Stringifyable =
| StringifyableAtomic
| StringifyableObject
| StringifyableArray;
export type StringifyableAtomic = string | number | boolean | null;
// NOTE: Use Record<string, Stringifyable>
interface StringifyableObject {
[key: string]: Stringifyable;
}
// NOTE: Use Stringifyable[]
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface StringifyableArray extends Array<Stringifyable> {}
export const zStringifyableAtomic = (() => {
type TargetType = StringifyableAtomic;
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
export const zStringifyable: z.ZodType<Stringifyable> = z
.any()
.superRefine((val, ctx) => {
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
if (!isStringifyable) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Not stringifyable"
});
}
});
export function getIsAtomic(
stringifyable: Stringifyable
): stringifyable is StringifyableAtomic {
return (
["string", "number", "boolean"].includes(typeof stringifyable) ||
stringifyable === null
);
}
export const { getValueAtPath } = (() => {
function getValueAtPath_rec(
stringifyable: Stringifyable,
path: (string | number)[]
): Stringifyable | undefined {
if (path.length === 0) {
return stringifyable;
}
if (getIsAtomic(stringifyable)) {
return undefined;
}
const [first, ...rest] = path;
let dereferenced: Stringifyable | undefined;
if (stringifyable instanceof Array) {
if (typeof first !== "number") {
return undefined;
}
dereferenced = stringifyable[first];
} else {
if (typeof first !== "string") {
return undefined;
}
dereferenced = stringifyable[first];
}
if (dereferenced === undefined) {
return undefined;
}
return getValueAtPath_rec(dereferenced, rest);
}
function getValueAtPath(
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
path: (string | number)[]
): Stringifyable | undefined {
return getValueAtPath_rec(stringifyableObjectOrArray, path);
}
return { getValueAtPath };
})();

View File

@ -0,0 +1,164 @@
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
export function canonicalStringify(params: {
data: Record<string, Stringifyable> | Stringifyable[];
referenceData: Record<string, Stringifyable> | Stringifyable[];
}): string {
const { data, referenceData } = params;
return JSON.stringify(
makeDeterministicCopy({
path: [],
data,
getCanonicalKeys: path => {
const referenceValue = (() => {
const path_patched: (string | number)[] = [];
for (let i = 0; i < path.length; i++) {
let value_i = getValueAtPath(referenceData, [
...path_patched,
path[i]
]);
if (value_i !== undefined) {
path_patched.push(path[i]);
continue;
}
if (typeof path[i] !== "number") {
return undefined;
}
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
if (value_i !== undefined) {
path_patched.push(0);
continue;
}
return undefined;
}
return getValueAtPath(referenceData, path_patched);
})();
if (referenceValue === undefined) {
return undefined;
}
if (getIsAtomic(referenceValue)) {
return undefined;
}
if (referenceValue instanceof Array) {
return undefined;
}
return Object.keys(referenceValue);
}
}),
null,
2
);
}
function makeDeterministicCopy(params: {
path: (string | number)[];
data: Stringifyable;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable {
const { path, data, getCanonicalKeys } = params;
if (getIsAtomic(data)) {
return data;
}
if (data instanceof Array) {
return makeDeterministicCopy_array({
path,
data,
getCanonicalKeys
});
}
return makeDeterministicCopy_record({
path,
data,
getCanonicalKeys
});
}
function makeDeterministicCopy_record(params: {
path: (string | number)[];
data: Record<string, Stringifyable>;
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Record<string, Stringifyable> {
const { path, data, getCanonicalKeys } = params;
const keysOfAtomicValues: string[] = [];
const keysOfNonAtomicValues: string[] = [];
for (const [key, value] of Object.entries(data)) {
if (getIsAtomic(value)) {
keysOfAtomicValues.push(key);
} else {
keysOfNonAtomicValues.push(key);
}
}
keysOfAtomicValues.sort();
keysOfNonAtomicValues.sort();
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
reorder_according_to_canonical: {
const canonicalKeys = getCanonicalKeys(path);
if (canonicalKeys === undefined) {
break reorder_according_to_canonical;
}
const keys_toPrepend: string[] = [];
for (const key of canonicalKeys) {
const indexOfKey = keys.indexOf(key);
if (indexOfKey === -1) {
continue;
}
keys.splice(indexOfKey, 1);
keys_toPrepend.push(key);
}
keys.unshift(...keys_toPrepend);
}
const result: Record<string, Stringifyable> = {};
for (const key of keys) {
result[key] = makeDeterministicCopy({
path: [...path, key],
data: data[key],
getCanonicalKeys
});
}
return result;
}
function makeDeterministicCopy_array(params: {
path: (string | number)[];
data: Stringifyable[];
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
}): Stringifyable[] {
const { path, data, getCanonicalKeys } = params;
return [...data].map((entry, i) =>
makeDeterministicCopy({
path: [...path, i],
data: entry,
getCanonicalKeys
})
);
}

View File

@ -1,73 +0,0 @@
import { Readable } from "stream";
const crc32tab = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
];
/**
*
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
* @returns a promise for a checksum (uint32)
*/
export function crc32(input: Readable | String | Buffer): Promise<number> {
if (typeof input === "string") {
let crc = ~0;
for (let i = 0; i < input.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Buffer) {
let crc = ~0;
for (let i = 0; i < input.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Readable) {
return new Promise<number>((resolve, reject) => {
let crc = ~0;
input.setMaxListeners(Infinity);
input.on("end", () => resolve((crc ^ -1) >>> 0));
input.on("error", e => reject(e));
input.on("data", (chunk: Buffer) => {
for (let i = 0; i < chunk.length; i++)
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
});
});
} else {
throw new Error("Unsupported input " + typeof input);
}
}

View File

@ -0,0 +1,90 @@
const keyIsTrapped = "isTrapped_zSskDe9d";
export class AccessError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, new.target.prototype);
}
}
export function createObjectThatThrowsIfAccessed<T extends object>(params?: {
debugMessage?: string;
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
}): T {
const { debugMessage = "", isPropertyWhitelisted = () => false } = params ?? {};
const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
const [, prop] = args;
if (isPropertyWhitelisted(prop)) {
return Reflect.get(...args);
}
if (prop === keyIsTrapped) {
return true;
}
throw new AccessError(`Cannot access ${String(prop)} yet ${debugMessage}`);
};
const trappedObject = new Proxy<T>({} as any, {
get,
set: get
});
return trappedObject;
}
export function createObjectThatThrowsIfAccessedFactory(params: {
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
}) {
const { isPropertyWhitelisted } = params;
return {
createObjectThatThrowsIfAccessed: <T extends object>(params?: {
debugMessage?: string;
}) => {
const { debugMessage } = params ?? {};
return createObjectThatThrowsIfAccessed<T>({
debugMessage,
isPropertyWhitelisted
});
}
};
}
export function isObjectThatThrowIfAccessed(obj: object) {
return (obj as any)[keyIsTrapped] === true;
}
export const THROW_IF_ACCESSED = {
__brand: "THROW_IF_ACCESSED"
};
export function createObjectWithSomePropertiesThatThrowIfAccessed<
T extends Record<string, unknown>
>(obj: { [K in keyof T]: T[K] | typeof THROW_IF_ACCESSED }, debugMessage?: string): T {
return Object.defineProperties(
obj,
Object.fromEntries(
Object.entries(obj)
.filter(([, value]) => value === THROW_IF_ACCESSED)
.map(([key]) => {
const getAndSet = () => {
throw new AccessError(
`Cannot access ${key} yet ${debugMessage ?? ""}`
);
};
const pd = {
get: getAndSet,
set: getAndSet,
enumerable: true
};
return [key, pd];
})
)
) as any;
}

View File

@ -1,61 +0,0 @@
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
import { pipeline } from "stream/promises";
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
import { promisify } from "util";
import { crc32 } from "./crc32";
import tee from "./tee";
const deflateRaw = promisify(deflateRawCb);
/**
* A stream transformer that records the number of bytes
* passed in its `size` property.
*/
class ByteCounter extends PassThrough {
size: number = 0;
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
if ("length" in chunk) this.size += chunk.length;
super._transform(chunk, encoding, callback);
}
}
/**
* @param data buffer containing the data to be compressed
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
* of the source data
*/
export async function deflateBuffer(data: Buffer) {
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
return { deflated, crc32: checksum };
}
/**
* @param input a byte stream, containing data to be compressed
* @param sink a method that will accept chunks of compressed data; We don't pass
* a writable here, since we don't want the writablestream to be closed after
* a single file
* @returns a promise, which will resolve with the crc32 checksum and the
* compressed size
*/
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
const deflateWriter = new Writable({
write(chunk, _, callback) {
sink(chunk);
callback();
}
});
// tee the input stream, so we can compress and calc crc32 in parallel
const [rs1, rs2] = tee(input);
const byteCounter = new ByteCounter();
const [_, crc] = await Promise.all([
// pipe input into zip compressor, count the bytes
// returned and pass compressed data to the sink
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
// calc checksum
crc32(rs2)
]);
return { crc32: crc, compressedSize: byteCounter.size };
}

View File

@ -2,40 +2,42 @@ import { join as pathJoin } from "path";
import { existsAsync } from "./fs.existsAsync";
import * as child_process from "child_process";
import { assert } from "tsafe/assert";
import { getIsRootPath } from "../tools/isRootPath";
export async function getInstalledModuleDirPath(params: {
moduleName: string;
packageJsonDirPath: string;
projectDirPath: string;
}) {
const { moduleName, packageJsonDirPath, projectDirPath } = params;
const { moduleName, packageJsonDirPath } = params;
common_case: {
const dirPath = pathJoin(
...[packageJsonDirPath, "node_modules", ...moduleName.split("/")]
);
{
let dirPath = packageJsonDirPath;
if (!(await existsAsync(dirPath))) {
break common_case;
while (true) {
const dirPath_candidate = pathJoin(
dirPath,
"node_modules",
...moduleName.split("/")
);
let doesExist: boolean;
try {
doesExist = await existsAsync(dirPath_candidate);
} catch {
doesExist = false;
}
if (doesExist) {
return dirPath_candidate;
}
if (getIsRootPath(dirPath)) {
break;
}
dirPath = pathJoin(dirPath, "..");
}
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

View File

@ -0,0 +1,45 @@
import * as child_process from "child_process";
import {
dirname as pathDirname,
basename as pathBasename,
join as pathJoin,
sep as pathSep
} from "path";
import { Deferred } from "evt/tools/Deferred";
import * as fs from "fs";
export function getIsKnownByGit(params: { filePath: string }): Promise<boolean> {
const { filePath } = params;
const dIsKnownByGit = new Deferred<boolean>();
let relativePath = pathBasename(filePath);
let dirPath = pathDirname(filePath);
while (!fs.existsSync(dirPath)) {
relativePath = pathJoin(pathBasename(dirPath), relativePath);
dirPath = pathDirname(dirPath);
}
child_process.exec(
`git ls-files --error-unmatch '${relativePath.split(pathSep).join("/")}'`,
{ cwd: dirPath },
error => {
if (error === null) {
dIsKnownByGit.resolve(true);
return;
}
if (error.code === 1) {
dIsKnownByGit.resolve(false);
return;
}
dIsKnownByGit.reject(error);
}
);
return dIsKnownByGit.pr;
}

View File

@ -0,0 +1,22 @@
import { normalize as pathNormalize } from "path";
export function getIsRootPath(filePath: string): boolean {
const path_normalized = pathNormalize(filePath);
// Unix-like root ("/")
if (path_normalized === "/") {
return true;
}
// Check for Windows drive root (e.g., "C:\\")
if (/^[a-zA-Z]:\\$/.test(path_normalized)) {
return true;
}
// Check for UNC root (e.g., "\\server\share")
if (/^\\\\[^\\]+\\[^\\]+\\?$/.test(path_normalized)) {
return true;
}
return false;
}

View File

@ -1,29 +0,0 @@
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

@ -8,7 +8,6 @@ import { exclude } from "tsafe/exclude";
export async function listInstalledModules(params: {
packageJsonFilePath: string;
projectDirPath: string;
filter: (params: { moduleName: string }) => boolean;
}): Promise<
{
@ -18,13 +17,13 @@ export async function listInstalledModules(params: {
peerDependencies: Record<string, string>;
}[]
> {
const { packageJsonFilePath, projectDirPath, filter } = params;
const { packageJsonFilePath, filter } = params;
const parsedPackageJson = await readPackageJsonDependencies({
packageJsonFilePath
});
const uiModuleNames = (
const extensionModuleNames = (
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
)
.filter(exclude(undefined))
@ -33,11 +32,10 @@ export async function listInstalledModules(params: {
.filter(moduleName => filter({ moduleName }));
const result = await Promise.all(
uiModuleNames.map(async moduleName => {
extensionModuleNames.map(async moduleName => {
const dirPath = await getInstalledModuleDirPath({
moduleName,
packageJsonDirPath: pathDirname(packageJsonFilePath),
projectDirPath
packageJsonDirPath: pathDirname(packageJsonFilePath)
});
const { version, peerDependencies } =

View File

@ -9,8 +9,9 @@ import { objectKeys } from "tsafe/objectKeys";
import { getAbsoluteAndInOsFormatPath } from "./getAbsoluteAndInOsFormatPath";
import { exclude } from "tsafe/exclude";
import { rmSync } from "./fs.rmSync";
import { Deferred } from "evt/tools/Deferred";
export function npmInstall(params: { packageJsonDirPath: string }) {
export async function npmInstall(params: { packageJsonDirPath: string }) {
const { packageJsonDirPath } = params;
const packageManagerBinName = (() => {
@ -68,7 +69,7 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
console.log(chalk.green("Installing in a way that won't break the links..."));
installWithoutBreakingLinks({
await installWithoutBreakingLinks({
packageJsonDirPath,
garronejLinkInfos
});
@ -77,9 +78,9 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
}
try {
child_process.execSync(`${packageManagerBinName} install`, {
cwd: packageJsonDirPath,
stdio: "inherit"
await runPackageManagerInstall({
packageManagerBinName,
cwd: packageJsonDirPath
});
} catch {
console.log(
@ -90,6 +91,42 @@ export function npmInstall(params: { packageJsonDirPath: string }) {
}
}
async function runPackageManagerInstall(params: {
packageManagerBinName: string;
cwd: string;
}) {
const { packageManagerBinName, cwd } = params;
const dCompleted = new Deferred<void>();
const child = child_process.spawn(packageManagerBinName, ["install"], {
cwd,
env: process.env,
shell: true
});
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => {
if (data.toString("utf8").includes("peer dependency")) {
return;
}
process.stderr.write(data);
});
child.on("exit", code => {
if (code !== 0) {
dCompleted.reject(new Error(`Failed with code ${code}`));
return;
}
dCompleted.resolve();
});
await dCompleted.pr;
}
function getGarronejLinkInfos(params: {
packageJsonDirPath: string;
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
@ -180,7 +217,7 @@ function getGarronejLinkInfos(params: {
return { linkedModuleNames, yarnHomeDirPath };
}
function installWithoutBreakingLinks(params: {
async function installWithoutBreakingLinks(params: {
packageJsonDirPath: string;
garronejLinkInfos: Exclude<ReturnType<typeof getGarronejLinkInfos>, undefined>;
}) {
@ -261,9 +298,9 @@ function installWithoutBreakingLinks(params: {
pathJoin(tmpProjectDirPath, YARN_LOCK)
);
child_process.execSync(`yarn install`, {
cwd: tmpProjectDirPath,
stdio: "inherit"
await runPackageManagerInstall({
packageManagerBinName: "yarn",
cwd: tmpProjectDirPath
});
// NOTE: Moving the modules from the tmp project to the actual project

View File

@ -1,47 +0,0 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
import { SemVer } from "../SemVer";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
async function getLatestsSemVersionedTag(params: {
owner: string;
repo: string;
count: number;
doIgnoreReleaseCandidates: boolean;
}): Promise<
{
tag: string;
version: SemVer;
}[]
> {
const { owner, repo, count, doIgnoreReleaseCandidates } = params;
const semVersionedTags: { tag: string; version: SemVer }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
let version: SemVer;
try {
version = SemVer.parse(tag.replace(/^[vV]?/, ""));
} catch {
continue;
}
if (doIgnoreReleaseCandidates && version.rc !== undefined) {
continue;
}
semVersionedTags.push({ tag, version });
}
return semVersionedTags
.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX))
.slice(0, count);
}
return { getLatestsSemVersionedTag };
}

View File

@ -1,60 +0,0 @@
import type { Octokit } from "@octokit/rest";
const per_page = 99;
export function listTagsFactory(params: { octokit: Octokit }) {
const { octokit } = params;
const octokit_repo_listTags = async (params: {
owner: string;
repo: string;
per_page: number;
page: number;
}) => {
return octokit.repos.listTags(params);
};
async function* listTags(params: {
owner: string;
repo: string;
}): AsyncGenerator<string> {
const { owner, repo } = params;
let page = 1;
while (true) {
const resp = await octokit_repo_listTags({
owner,
repo,
per_page,
page: page++
});
for (const branch of resp.data.map(({ name }) => name)) {
yield branch;
}
if (resp.data.length < 99) {
break;
}
}
}
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
async function getLatestTag(params: {
owner: string;
repo: string;
}): Promise<string | undefined> {
const { owner, repo } = params;
const itRes = await listTags({ owner, repo }).next();
if (itRes.done) {
return undefined;
}
return itRes.value;
}
return { listTags, getLatestTag };
}

View File

@ -101,7 +101,7 @@ export async function runPrettier(params: {
resolveConfig: true
});
if (ignored) {
if (ignored || inferredParser === null) {
return sourceCode;
}
@ -110,7 +110,7 @@ export async function runPrettier(params: {
formattedSourceCode = await prettier.format(sourceCode, {
...config,
filePath,
parser: inferredParser ?? undefined
parser: inferredParser
});
} catch (error) {
console.log(

View File

@ -1,39 +0,0 @@
import { PassThrough, Readable } from "stream";
export default function tee(input: Readable) {
const a = new PassThrough();
const b = new PassThrough();
let aFull = false;
let bFull = false;
a.setMaxListeners(Infinity);
a.on("drain", () => {
aFull = false;
if (!aFull && !bFull) input.resume();
});
b.on("drain", () => {
bFull = false;
if (!aFull && !bFull) input.resume();
});
input.on("error", e => {
a.emit("error", e);
b.emit("error", e);
});
input.on("data", chunk => {
aFull = !a.write(chunk);
bFull = !b.write(chunk);
if (aFull || bFull) input.pause();
});
input.on("end", () => {
a.end();
b.end();
});
return [a, b] as const;
}

View File

@ -1,49 +0,0 @@
/**
* Concatenate the string fragments and interpolated values
* to get a single string.
*/
function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
const chunks: string[] = [];
for (let i = 0; i < strings.length; i++) {
let lastStringLineLength = 0;
if (strings[i]) {
chunks.push(strings[i]);
// remember last indent of the string portion
lastStringLineLength = strings[i].split("\n").slice(-1)[0]?.length ?? 0;
}
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values
// using the last known string indent
const chunk = String(args[i]).replace(
/([\r?\n])/g,
"$1" + " ".repeat(lastStringLineLength)
);
chunks.push(chunk);
}
}
return chunks.join("");
}
/**
* Shift all lines left by the *smallest* indentation level,
* and remove initial newline and all trailing spaces.
*/
export default function trimIndent(strings: TemplateStringsArray, ...args: any[]) {
// Remove initial and final newlines
let string = populateTemplate(strings, ...args)
.replace(/^[\r\n]/, "")
.replace(/\r?\n *$/, "");
const dents =
string
.match(/^([ \t])+/gm)
?.filter(s => /^\s+$/.test(s))
?.map(s => s.length) ?? [];
// No dents? no change required
if (!dents || dents.length == 0) return string;
const minDent = Math.min(...dents);
// The min indentation is 0, no change needed
if (!minDent) return string;
const re = new RegExp(`^${" ".repeat(minDent)}`, "gm");
const dedented = string.replace(re, "");
return dedented;
}

View File

@ -1,15 +1,31 @@
import * as child_process from "child_process";
import { dirname as pathDirname, basename as pathBasename } from "path";
import {
dirname as pathDirname,
basename as pathBasename,
join as pathJoin,
sep as pathSep
} from "path";
import { Deferred } from "evt/tools/Deferred";
import { existsAsync } from "./fs.existsAsync";
export async function untrackFromGit(params: { filePath: string }): Promise<void> {
const { filePath } = params;
const dDone = new Deferred<void>();
let relativePath = pathBasename(filePath);
let dirPath = pathDirname(filePath);
while (!(await existsAsync(dirPath))) {
relativePath = pathJoin(pathBasename(dirPath), relativePath);
dirPath = pathDirname(dirPath);
}
child_process.exec(
`git rm --cached ${pathBasename(filePath)}`,
{ cwd: pathDirname(filePath) },
`git rm --cached '${relativePath.split(pathSep).join("/")}'`,
{ cwd: dirPath },
error => {
if (error !== null) {
dDone.reject(error);

View File

@ -10,5 +10,5 @@
"rootDir": "."
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["initialize-account-theme/src"]
"exclude": ["initialize-account-theme/multi-page-boilerplate"]
}

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { lazy, Suspense } from "react";
import { assert, type Equals } from "tsafe/assert";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useReducer, Fragment } from "react";
import { assert } from "keycloakify/tools/assert";
import type { KcClsx } from "keycloakify/login/lib/kcClsx";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import type { Attribute } from "keycloakify/login/KcContext";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import type { GenericI18n_noJsx } from "../noJsx/GenericI18n_noJsx";
import { assert, type Equals } from "tsafe/assert";

View File

@ -46,7 +46,7 @@ export type I18nBuilder<
}>
) => I18nBuilder<
ThemeName,
MessageKey_themeDefined,
string extends MessageKey_themeDefined ? never : MessageKey_themeDefined,
LanguageTag_notInDefaultSet,
ExcludedMethod | "withCustomTranslations"
>;

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { createGetI18n, type KcContextLike } from "../noJsx/getI18n";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import * as reactlessApi from "./getUserProfileApi/index";
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
import { useEffect, useState, useMemo, Fragment } from "react";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx } from "keycloakify/login/lib/kcClsx";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState, useEffect, useReducer } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { assert } from "keycloakify/tools/assert";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState, useEffect, useReducer } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { clsx } from "keycloakify/tools/clsx";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useReducer } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { assert } from "keycloakify/tools/assert";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx } from "keycloakify/login/lib/kcClsx";

View File

@ -1,3 +1,4 @@
import type { JSX } from "keycloakify/tools/JSX";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";

Some files were not shown because too many files have changed in this diff Show More