Compare commits

..

1 Commits

Author SHA1 Message Date
daf95b3dbb wip 2024-10-05 05:57:14 +02:00
153 changed files with 2658 additions and 9519 deletions
.all-contributorsrc.prettierignoreREADME.mdpackage.json
scripts
src
account
bin
lib
login
tools
vite-plugin
stories
test/login
yarn.lock

@ -290,43 +290,6 @@
"code", "code",
"test" "test"
] ]
},
{
"login": "nima70",
"name": "Nima Shokouhfar",
"avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
"profile": "https://github.com/nima70",
"contributions": [
"code",
"test"
]
},
{
"login": "marvinruder",
"name": "Marvin A. Ruder",
"avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
"profile": "https://mruder.dev",
"contributions": [
"bug"
]
},
{
"login": "zvn2060",
"name": "HI_OuO",
"avatar_url": "https://avatars.githubusercontent.com/u/45450852?v=4",
"profile": "https://github.com/zvn2060",
"contributions": [
"code"
]
},
{
"login": "tripheo0412",
"name": "Tri Hoang",
"avatar_url": "https://avatars.githubusercontent.com/u/25382052?v=4",
"profile": "https://github.com/tripheo0412",
"contributions": [
"doc"
]
} }
], ],
"contributorsPerLine": 7, "contributorsPerLine": 7,

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

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

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "11.5.1", "version": "11.2.10",
"description": "Framework to create custom Keycloak UIs", "description": "Framework to create custom Keycloak UIs",
"repository": { "repository": {
"type": "git", "type": "git",
@ -38,14 +38,12 @@
"dist/", "dist/",
"!dist/tsconfig.tsbuildinfo", "!dist/tsconfig.tsbuildinfo",
"!dist/bin/", "!dist/bin/",
"dist/bin/**/*.d.ts",
"dist/bin/main.js", "dist/bin/main.js",
"dist/bin/*.index.js", "dist/bin/*.index.js",
"dist/bin/*.node", "dist/bin/*.node",
"dist/bin/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/constants.js.map", "dist/bin/shared/*.d.ts",
"dist/bin/shared/customHandler.js", "dist/bin/shared/*.js.map",
"dist/bin/shared/customHandler.js.map",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"dist/vite-plugin/index.js", "dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts", "dist/vite-plugin/index.d.ts",
@ -64,7 +62,7 @@
], ],
"homepage": "https://www.keycloakify.dev", "homepage": "https://www.keycloakify.dev",
"dependencies": { "dependencies": {
"tsafe": "^1.8.5" "tsafe": "^1.7.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.24.5", "@babel/core": "^7.24.5",
@ -73,6 +71,7 @@
"@babel/preset-env": "7.24.8", "@babel/preset-env": "7.24.8",
"@babel/types": "^7.24.5", "@babel/types": "^7.24.5",
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@keycloakify/angular": "^0.0.1-rc.19",
"@octokit/rest": "^20.1.1", "@octokit/rest": "^20.1.1",
"@storybook/addon-a11y": "^6.5.16", "@storybook/addon-a11y": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.13", "@storybook/builder-webpack5": "^6.5.13",
@ -95,14 +94,14 @@
"cli-select": "^1.1.2", "cli-select": "^1.1.2",
"dompurify": "^3.1.6", "dompurify": "^3.1.6",
"eslint-plugin-storybook": "^0.6.7", "eslint-plugin-storybook": "^0.6.7",
"evt": "^2.5.8", "evt": "^2.5.7",
"html-entities": "^2.5.2", "html-entities": "^2.5.2",
"husky": "^4.3.8", "husky": "^4.3.8",
"isomorphic-dompurify": "^2.15.0", "isomorphic-dompurify": "^2.15.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"magic-string": "^0.30.7", "magic-string": "^0.30.7",
"make-fetch-happen": "^11.0.3", "make-fetch-happen": "^11.0.3",
"powerhooks": "^1.0.19", "powerhooks": "^1.0.10",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"properties-parser": "^0.3.1", "properties-parser": "^0.3.1",
"react": "^18.2.0", "react": "^18.2.0",

@ -1,39 +0,0 @@
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);
}

@ -7,7 +7,6 @@ import { createAccountV1Dir } from "./createAccountV1Dir";
import chalk from "chalk"; import chalk from "chalk";
import { run } from "../shared/run"; import { run } from "../shared/run";
import { vendorFrontendDependencies } from "./vendorFrontendDependencies"; import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
(async () => { (async () => {
console.log(chalk.cyan("Building Keycloakify...")); console.log(chalk.cyan("Building Keycloakify..."));
@ -41,9 +40,7 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
); );
} }
run( run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
);
transformCodebase({ transformCodebase({
srcDirPath: join("dist", "ncc_out"), srcDirPath: join("dist", "ncc_out"),
@ -116,7 +113,7 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
} }
run( run(
`npx ncc build ${join("dist", "vite-plugin", "index.js")} --external prettier -o ${join( `npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join(
"dist", "dist",
"ncc_out" "ncc_out"
)}` )}`
@ -149,6 +146,9 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
fs.cpSync(dirBasename, destDirPath, { recursive: true }); fs.cpSync(dirBasename, destDirPath, { recursive: true });
} }
await createPublicKeycloakifyDevResourcesDir();
await createAccountV1Dir();
transformCodebase({ transformCodebase({
srcDirPath: join("stories"), srcDirPath: join("stories"),
destDirPath: join("dist", "stories"), destDirPath: join("dist", "stories"),
@ -161,12 +161,6 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
} }
}); });
await createPublicKeycloakifyDevResourcesDir();
await createAccountV1Dir();
await downloadKeycloakifyLogging({
distDirPath: join(process.cwd(), "dist")
});
console.log( console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`) chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
); );

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

@ -1,15 +1,66 @@
import { CONTAINER_NAME } from "../src/bin/shared/constants"; import { CONTAINER_NAME } from "../src/bin/shared/constants";
import child_process from "child_process"; import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer"; import { SemVer } from "../src/bin/tools/SemVer";
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig"; import { join as pathJoin, relative as pathRelative } from "path";
import { cacheDirPath } from "./shared/cacheDirPath";
import { runPrettier } from "../src/bin/tools/runPrettier";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { join as pathJoin } from "path";
import * as fs from "fs";
import chalk from "chalk"; import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { run } from "./shared/run";
(async () => { (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( const keycloakMajorVersionNumber = SemVer.parse(
child_process child_process
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`) .execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
@ -18,32 +69,19 @@ import chalk from "chalk";
.split(":")[1] .split(":")[1]
).major; ).major;
const parsedRealmJson = await dumpContainerConfig({ const targetFilePath = pathRelative(
buildContext: { process.cwd(),
cacheDirPath pathJoin(
}, __dirname,
keycloakMajorVersionNumber, "..",
realmName: "myrealm" "src",
}); "bin",
"start-keycloak",
let sourceCode = JSON.stringify(parsedRealmJson, null, 2); `myrealm-realm-${keycloakMajorVersionNumber}.json`
)
const filePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
"realmConfig",
"defaultConfig",
`realm-kc-${keycloakMajorVersionNumber}.json`
); );
sourceCode = await runPrettier({ run(`docker cp ${CONTAINER_NAME}:/tmp/myrealm-realm.json ${targetFilePath}`);
sourceCode,
filePath
});
fs.writeFileSync(filePath, Buffer.from(sourceCode, "utf8")); console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
console.log(chalk.green(`Realm config dumped to ${filePath}`));
})(); })();

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

@ -55,6 +55,7 @@ const commonThirdPartyDeps = [
Buffer.from(modifiedPackageJsonContent, "utf8") Buffer.from(modifiedPackageJsonContent, "utf8")
); );
} }
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home"); const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true }); fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
@ -63,21 +64,6 @@ fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => { const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params; const { targetModuleName, cwd } = params;
if (targetModuleName === undefined) {
const packageJsonFilePath = pathJoin(cwd, "package.json");
const packageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
delete packageJson["packageManager"];
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(packageJson, null, 2))
);
}
const cmd = [ const cmd = [
"yarn", "yarn",
"link", "link",
@ -91,10 +77,7 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
env: { env: {
...process.env, ...process.env,
...(os.platform() === "win32" ...(os.platform() === "win32"
? { ? { USERPROFILE: yarnGlobalDirPath }
USERPROFILE: yarnGlobalDirPath,
LOCALAPPDATA: yarnGlobalDirPath
}
: { HOME: yarnGlobalDirPath }) : { HOME: yarnGlobalDirPath })
} }
}); });
@ -125,54 +108,7 @@ if (testAppPaths.length === 0) {
process.exit(-1); process.exit(-1);
} }
testAppPaths.forEach(testAppPath => { testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
const packageJsonFilePath = pathJoin(testAppPath, "package.json");
const packageJsonContent = fs.readFileSync(packageJsonFilePath);
const parsedPackageJson = JSON.parse(packageJsonContent.toString("utf8")) as {
scripts?: Record<string, string>;
};
let hasPostInstallOrPrepareScript = false;
if (parsedPackageJson.scripts !== undefined) {
for (const scriptName of ["postinstall", "prepare"]) {
if (parsedPackageJson.scripts[scriptName] === undefined) {
continue;
}
hasPostInstallOrPrepareScript = true;
delete parsedPackageJson.scripts[scriptName];
}
}
if (hasPostInstallOrPrepareScript) {
fs.writeFileSync(
packageJsonFilePath,
Buffer.from(JSON.stringify(parsedPackageJson, null, 2), "utf8")
);
}
const restorePackageJson = () => {
if (!hasPostInstallOrPrepareScript) {
return;
}
fs.writeFileSync(packageJsonFilePath, packageJsonContent);
};
try {
execSync("yarn install", { cwd: testAppPath });
} catch (error) {
restorePackageJson();
throw error;
}
restorePackageJson();
});
console.log("=== Linking common dependencies ==="); console.log("=== Linking common dependencies ===");
@ -219,20 +155,4 @@ testAppPaths.forEach(testAppPath =>
}) })
); );
testAppPaths.forEach(testAppPath => {
const { scripts = {} } = JSON.parse(
fs.readFileSync(pathJoin(testAppPath, "package.json")).toString("utf8")
) as {
scripts?: Record<string, string>;
};
for (const scriptName of ["postinstall", "prepare"]) {
if (scripts[scriptName] === undefined) {
continue;
}
execSync(`yarn run ${scriptName}`, { cwd: testAppPath });
}
});
export {}; export {};

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

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

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

@ -5,30 +5,25 @@ import {
ACCOUNT_THEME_PAGE_IDS, ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId, type LoginThemePageId,
type AccountThemePageId, type AccountThemePageId,
THEME_TYPES THEME_TYPES,
type ThemeType
} from "./shared/constants"; } from "./shared/constants";
import { capitalize } from "tsafe/capitalize"; import { capitalize } from "tsafe/capitalize";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const buildContext = getBuildContext({
commandName: "add-story", cliCommandOptions
buildContext
}); });
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:")); console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => { const themeType = await (async () => {
@ -38,8 +33,6 @@ export async function command(params: { buildContext: BuildContext }) {
return buildContext.implementedThemeTypes.account.isImplemented; return buildContext.implementedThemeTypes.account.isImplemented;
case "login": case "login":
return buildContext.implementedThemeTypes.login.isImplemented; return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
} }
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>(false);
}); });
@ -50,7 +43,7 @@ export async function command(params: { buildContext: BuildContext }) {
return values[0]; return values[0];
} }
const { value } = await cliSelect({ const { value } = await cliSelect<ThemeType>({
values values
}).catch(() => { }).catch(() => {
process.exit(-1); process.exit(-1);
@ -69,16 +62,6 @@ export async function command(params: { buildContext: BuildContext }) {
); );
process.exit(0); process.exit(0);
return;
}
if (themeType === "admin") {
console.log(
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
);
process.exit(0);
return;
} }
console.log(`${themeType}`); console.log(`${themeType}`);
@ -119,7 +102,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1); process.exit(-1);
} }
let sourceCode = fs const componentCode = fs
.readFileSync( .readFileSync(
pathJoin( pathJoin(
getThisCodebaseRootDirPath(), getThisCodebaseRootDirPath(),
@ -133,17 +116,6 @@ export async function command(params: { buildContext: BuildContext }) {
.replace('import React from "react";\n', "") .replace('import React from "react";\n', "")
.replace(/from "[./]+dist\//, 'from "keycloakify/'); .replace(/from "[./]+dist\//, 'from "keycloakify/');
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetFilePath,
sourceCode: sourceCode
});
}
{ {
const targetDirPath = pathDirname(targetFilePath); const targetDirPath = pathDirname(targetFilePath);
@ -152,7 +124,7 @@ export async function command(params: { buildContext: BuildContext }) {
} }
} }
fs.writeFileSync(targetFilePath, Buffer.from(sourceCode, "utf8")); fs.writeFileSync(targetFilePath, Buffer.from(componentCode, "utf8"));
console.log( console.log(
[ [

@ -1,96 +1,13 @@
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate"; import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import { join as pathJoin, dirname as pathDirname } from "path"; import { getBuildContext } from "./shared/buildContext";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants"; import type { CliCommandOptions } from "./main";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
import type { BuildContext } from "./shared/buildContext";
import { transformCodebase } from "./tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const buildContext = getBuildContext({ cliCommandOptions });
commandName: "copy-keycloak-resources-to-public",
copyKeycloakResourcesToPublic({
buildContext buildContext
}); });
if (hasBeenHandled) {
return;
}
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
} }

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

@ -7,7 +7,8 @@ import {
ACCOUNT_THEME_PAGE_IDS, ACCOUNT_THEME_PAGE_IDS,
type LoginThemePageId, type LoginThemePageId,
type AccountThemePageId, type AccountThemePageId,
THEME_TYPES THEME_TYPES,
type ThemeType
} from "./shared/constants"; } from "./shared/constants";
import { capitalize } from "tsafe/capitalize"; import { capitalize } from "tsafe/capitalize";
import * as fs from "fs"; import * as fs from "fs";
@ -19,23 +20,17 @@ import {
} from "path"; } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const buildContext = getBuildContext({
commandName: "eject-page", cliCommandOptions
buildContext
}); });
if (hasBeenHandled) {
return;
}
console.log(chalk.cyan("Theme type:")); console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => { const themeType = await (async () => {
@ -45,8 +40,6 @@ export async function command(params: { buildContext: BuildContext }) {
return buildContext.implementedThemeTypes.account.isImplemented; return buildContext.implementedThemeTypes.account.isImplemented;
case "login": case "login":
return buildContext.implementedThemeTypes.login.isImplemented; return buildContext.implementedThemeTypes.login.isImplemented;
case "admin":
return buildContext.implementedThemeTypes.admin.isImplemented;
} }
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>(false);
}); });
@ -57,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) {
return values[0]; return values[0];
} }
const { value } = await cliSelect({ const { value } = await cliSelect<ThemeType>({
values values
}).catch(() => { }).catch(() => {
process.exit(-1); process.exit(-1);
@ -66,14 +59,6 @@ export async function command(params: { buildContext: BuildContext }) {
return value; return value;
})(); })();
if (themeType === "admin") {
console.log(
"Use `npx keycloakify eject-file` command instead, see documentation"
);
process.exit(-1);
}
if ( if (
themeType === "account" && themeType === "account" &&
(assert(buildContext.implementedThemeTypes.account.isImplemented), (assert(buildContext.implementedThemeTypes.account.isImplemented),
@ -83,13 +68,13 @@ export async function command(params: { buildContext: BuildContext }) {
pathDirname(buildContext.packageJsonFilePath), pathDirname(buildContext.packageJsonFilePath),
"node_modules", "node_modules",
"@keycloakify", "@keycloakify",
`keycloak-account-ui`, "keycloak-account-ui",
"src" "src"
); );
console.log( console.log(
[ [
`There isn't an interactive CLI to eject components of the Account SPA UI.`, `There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
`You can however copy paste into your codebase the any file or directory from the following source directory:`, `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)))}`, `${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
@ -98,44 +83,41 @@ export async function command(params: { buildContext: BuildContext }) {
); );
eject_entrypoint: { eject_entrypoint: {
const kcUiTsxFileRelativePath = `KcAccountUi.tsx` as const; const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
const themeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(
buildContext.themeSrcDirPath,
"account"
);
const targetFilePath = pathJoin(themeSrcDirPath, kcUiTsxFileRelativePath); const targetFilePath = pathJoin(
accountThemeSrcDirPath,
kcAccountUiTsxFileRelativePath
);
if (fs.existsSync(targetFilePath)) { if (fs.existsSync(targetFilePath)) {
break eject_entrypoint; break eject_entrypoint;
} }
fs.cpSync(pathJoin(srcDirPath, kcUiTsxFileRelativePath), targetFilePath); fs.cpSync(
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
targetFilePath
);
{ {
const kcPageTsxFilePath = pathJoin(themeSrcDirPath, "KcPage.tsx"); const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8"); const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
const componentName = pathBasename(kcUiTsxFileRelativePath).replace( const componentName = pathBasename(
/.tsx$/, kcAccountUiTsxFileRelativePath
"" ).replace(/.tsx$/, "");
);
let modifiedKcPageTsxCode = kcPageTsxCode.replace( const modifiedKcPageTsxCode = kcPageTsxCode.replace(
`@keycloakify/keycloak-account-ui/${componentName}`, `@keycloakify/keycloak-account-ui/${componentName}`,
`./${componentName}` `./${componentName}`
); );
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
modifiedKcPageTsxCode = await runPrettier({
filePath: kcPageTsxFilePath,
sourceCode: modifiedKcPageTsxCode
});
}
fs.writeFileSync( fs.writeFileSync(
kcPageTsxFilePath, kcPageTsxFilePath,
Buffer.from(modifiedKcPageTsxCode, "utf8") Buffer.from(modifiedKcPageTsxCode, "utf8")
@ -151,14 +133,13 @@ export async function command(params: { buildContext: BuildContext }) {
[ [
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`, `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)}`, `The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), themeSrcDirPath)}\``, `with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
`then update the import of routes in ${kcUiTsxFileRelativePath}.` `then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
].join("\n") ].join("\n")
); );
} }
process.exit(0); process.exit(0);
return;
} }
console.log(`${themeType}`); console.log(`${themeType}`);
@ -235,7 +216,7 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1); process.exit(-1);
} }
let componentCode = fs const componentCode = fs
.readFileSync( .readFileSync(
pathJoin( pathJoin(
getThisCodebaseRootDirPath(), getThisCodebaseRootDirPath(),
@ -247,17 +228,6 @@ export async function command(params: { buildContext: BuildContext }) {
) )
.toString("utf8"); .toString("utf8");
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
componentCode = await runPrettier({
filePath: targetFilePath,
sourceCode: componentCode
});
}
{ {
const targetDirPath = pathDirname(targetFilePath); const targetDirPath = pathDirname(targetFilePath);
@ -274,12 +244,12 @@ export async function command(params: { buildContext: BuildContext }) {
)} copy pasted from the Keycloakify source code into your project` )} copy pasted from the Keycloakify source code into your project`
); );
edit_KcPage: { edit_KcApp: {
if ( if (
pageIdOrComponent !== templateValue && pageIdOrComponent !== templateValue &&
pageIdOrComponent !== userProfileFormFieldsValue pageIdOrComponent !== userProfileFormFieldsValue
) { ) {
break edit_KcPage; break edit_KcApp;
} }
const kcAppTsxPath = pathJoin( const kcAppTsxPath = pathJoin(

@ -1,24 +1,17 @@
import type { BuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import type { CliCommandOptions } from "../main";
import cliSelect from "cli-select"; import cliSelect from "cli-select";
import child_process from "child_process";
import chalk from "chalk"; import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs"; import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig"; import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { command as updateKcGenCommand } from "../update-kc-gen"; import { generateKcGenTs } from "../shared/generateKcGenTs";
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const buildContext = getBuildContext({ cliCommandOptions });
commandName: "initialize-account-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account"); const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
@ -38,9 +31,37 @@ export async function command(params: { buildContext: BuildContext }) {
process.exit(-1); process.exit(-1);
} }
exitIfUncommittedChanges({ exit_if_uncommitted_changes: {
projectDirPath: buildContext.projectDirPath let hasUncommittedChanges: boolean | undefined = undefined;
});
try {
hasUncommittedChanges =
child_process
.execSync(`git status --porcelain`, {
cwd: buildContext.projectDirPath
})
.toString()
.trim() !== "";
} catch {
// Probably not a git repository
break exit_if_uncommitted_changes;
}
if (!hasUncommittedChanges) {
break exit_if_uncommitted_changes;
}
console.warn(
[
chalk.red(
"Please commit or stash your changes before running this command.\n"
),
"This command will modify your project's files so it's better to have a clean working directory",
"so that you can easily see what has been changed and revert if needed."
].join(" ")
);
process.exit(-1);
}
const { value: accountThemeType } = await cliSelect({ const { value: accountThemeType } = await cliSelect({
values: ["Single-Page" as const, "Multi-Page" as const] values: ["Single-Page" as const, "Multi-Page" as const]
@ -76,7 +97,7 @@ export async function command(params: { buildContext: BuildContext }) {
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType }); updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
await updateKcGenCommand({ await generateKcGenTs({
buildContext: { buildContext: {
...buildContext, ...buildContext,
implementedThemeTypes: { implementedThemeTypes: {

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

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

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

@ -1,25 +1,15 @@
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase"; import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import type { BuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import * as fs from "fs"; import * as fs from "fs";
import type { CliCommandOptions } from "./main";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive"; import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import fetch from "make-fetch-happen";
import { SemVer } from "./tools/SemVer";
import { assert } from "tsafe/assert";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({ const buildContext = getBuildContext({ cliCommandOptions });
commandName: "initialize-email-theme",
buildContext
});
if (hasBeenHandled) {
return;
}
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
@ -39,7 +29,7 @@ export async function command(params: { buildContext: BuildContext }) {
console.log("Initialize with the base email theme from which version of Keycloak?"); console.log("Initialize with the base email theme from which version of Keycloak?");
let { keycloakVersion } = await promptKeycloakVersion({ const { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary // NOTE: This is arbitrary
startingFromMajor: 17, startingFromMajor: 17,
excludeMajorVersions: [], excludeMajorVersions: [],
@ -47,32 +37,8 @@ export async function command(params: { buildContext: BuildContext }) {
buildContext 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({ const { extractedDirPath } = await downloadAndExtractArchive({
url: getUrl(keycloakVersion), url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions, fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme", uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",

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

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

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

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

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

@ -2,16 +2,19 @@ import { generateResources } from "./generateResources";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import type { BuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants"; import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os"; import * as os from "os";
import { rmSync } from "../tools/fs.rmSync"; import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
const buildContext = getBuildContext({ cliCommandOptions });
exit_if_maven_not_installed: { exit_if_maven_not_installed: {
let commandOutput: Buffer | undefined = undefined; let commandOutput: Buffer | undefined = undefined;

@ -4,12 +4,8 @@ import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx"; 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 = { export type CliCommandOptions = {
projectDirPath: string | undefined; projectDirPath: string | undefined;
}; };
@ -73,17 +69,17 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./keycloakify"); const { command } = await import("./keycloakify");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
program program
.command<{ .command<{
port: number | undefined; port: number | undefined;
keycloakVersion: string | number | undefined; keycloakVersion: string | undefined;
realmJsonFilePath: string | undefined; realmJsonFilePath: string | undefined;
}>({ }>({
name: "start-keycloak", name: "start-keycloak",
@ -134,54 +130,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => { handler: async cliCommandOptions => {
const { command } = await import("./start-keycloak"); const { command } = await import("./start-keycloak");
validate_keycloak_version: { await command({ cliCommandOptions });
if (keycloakVersion === undefined) {
break validate_keycloak_version;
}
const isValidVersion = (() => {
if (typeof keycloakVersion === "number") {
return false;
}
try {
SemVer.parse(keycloakVersion);
} catch {
return false;
}
return;
})();
if (isValidVersion) {
break validate_keycloak_version;
}
console.log(
chalk.red(
[
`Invalid Keycloak version: ${keycloakVersion}`,
"It should be a valid semver version example: 26.0.4"
].join(" ")
)
);
process.exit(1);
}
assert(is<string | undefined>(keycloakVersion));
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: {
keycloakVersion,
port,
realmJsonFilePath
}
});
} }
}); });
@ -192,10 +144,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./eject-page"); const { command } = await import("./eject-page");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
@ -206,10 +158,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./add-story"); const { command } = await import("./add-story");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
@ -220,10 +172,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./initialize-email-theme"); const { command } = await import("./initialize-email-theme");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
@ -234,10 +186,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./initialize-account-theme"); const { command } = await import("./initialize-account-theme");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
@ -245,14 +197,14 @@ program
.command({ .command({
name: "copy-keycloak-resources-to-public", name: "copy-keycloak-resources-to-public",
description: description:
"(Internal) Copy Keycloak default theme resources to the public directory." "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./copy-keycloak-resources-to-public"); const { command } = await import("./copy-keycloak-resources-to-public");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
} }
}); });
@ -264,60 +216,10 @@ program
}) })
.task({ .task({
skip, skip,
handler: async ({ projectDirPath }) => { handler: async cliCommandOptions => {
const { command } = await import("./update-kc-gen"); const { command } = await import("./update-kc-gen");
await command({ buildContext: getBuildContext({ projectDirPath }) }); await command({ cliCommandOptions });
}
});
program
.command({
name: "postinstall",
description: "Initialize all the Keycloakify UI modules installed in the project."
})
.task({
skip,
handler: async ({ projectDirPath }) => {
const { command } = await import("./postinstall");
await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
program
.command<{
file: string;
}>({
name: "eject-file",
description: [
"WARNING: Not usable yet, will be used for future features",
"Take ownership over a given file"
].join(" ")
})
.option({
key: "file",
name: (() => {
const name = "file";
optionsKeys.push(name);
return name;
})(),
description: [
"Relative path of the file relative to the directory of your keycloak theme source",
"Example `--file src/login/page/Login.tsx`"
].join(" ")
})
.task({
skip,
handler: async ({ projectDirPath, file }) => {
const { command } = await import("./eject-file");
await command({
buildContext: getBuildContext({ projectDirPath }),
cliCommandOptions: { file }
});
} }
}); });

@ -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");
}

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

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

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

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

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

@ -7,9 +7,10 @@ import {
dirname as pathDirname dirname as pathDirname
} from "path"; } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath"; import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; import * as fs from "fs";
import { assert, type Equals, is } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { import {
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES, VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
@ -18,11 +19,13 @@ import {
import type { KeycloakVersionRange } from "./KeycloakVersionRange"; import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe"; import { exclude } from "tsafe";
import { crawl } from "../tools/crawl"; import { crawl } from "../tools/crawl";
import { THEME_TYPES, KEYCLOAK_THEME, type ThemeType } from "./constants"; import { THEME_TYPES } from "./constants";
import { objectEntries } from "tsafe/objectEntries"; import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import chalk from "chalk"; import chalk from "chalk";
import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions"; import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
import { is } from "tsafe/is";
export type BuildContext = { export type BuildContext = {
themeVersion: string; themeVersion: string;
@ -40,7 +43,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */ * In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined; urlPathname: string | undefined;
assetsDirPath: string; assetsDirPath: string;
fetchOptions: FetchOptionsLike; fetchOptions: ProxyFetchOptions;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string; themeSrcDirPath: string;
@ -50,7 +53,6 @@ export type BuildContext = {
account: account:
| { isImplemented: false } | { isImplemented: false }
| { isImplemented: true; type: "Single-Page" | "Multi-Page" }; | { isImplemented: true; type: "Single-Page" | "Multi-Page" };
admin: { isImplemented: boolean };
}; };
packageJsonFilePath: string; packageJsonFilePath: string;
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
@ -127,12 +129,14 @@ export type ResolvedViteConfig = {
}; };
export function getBuildContext(params: { export function getBuildContext(params: {
projectDirPath: string | undefined; cliCommandOptions: CliCommandOptions;
}): BuildContext { }): BuildContext {
const { cliCommandOptions } = params;
const projectDirPath = const projectDirPath =
params.projectDirPath !== undefined cliCommandOptions.projectDirPath !== undefined
? getAbsoluteAndInOsFormatPath({ ? getAbsoluteAndInOsFormatPath({
pathIsh: params.projectDirPath, pathIsh: cliCommandOptions.projectDirPath,
cwd: process.cwd() cwd: process.cwd()
}) })
: process.cwd(); : process.cwd();
@ -145,10 +149,7 @@ export function getBuildContext(params: {
returnedPathsType: "relative to dirPath" returnedPathsType: "relative to dirPath"
}) })
.map(fileRelativePath => { .map(fileRelativePath => {
for (const themeSrcDirBasename of [ for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
KEYCLOAK_THEME,
KEYCLOAK_THEME.replace(/-/g, "_")
]) {
const split = fileRelativePath.split(themeSrcDirBasename); const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) { if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename); return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
@ -174,7 +175,7 @@ export function getBuildContext(params: {
[ [
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`, `Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`, `Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
`If you are collocating your Keycloak theme with your app you must have a directory named '${KEYCLOAK_THEME}' or '${KEYCLOAK_THEME.replace(/-/g, "_")}' in your 'src' directory` `If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
].join("\n") ].join("\n")
) )
); );
@ -450,10 +451,7 @@ export function getBuildContext(params: {
isImplemented: true, isImplemented: true,
type: buildOptions.accountThemeImplementation type: buildOptions.accountThemeImplementation
}; };
})(), })()
admin: {
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "admin"))
}
}; };
if ( if (
@ -513,15 +511,6 @@ export function getBuildContext(params: {
return themeNames; return themeNames;
})(); })();
const relativePathsCwd = (() => {
switch (bundler) {
case "vite":
return projectDirPath;
case "webpack":
return pathDirname(packageJsonFilePath);
}
})();
const projectBuildDirPath = (() => { const projectBuildDirPath = (() => {
webpack: { webpack: {
if (bundler !== "webpack") { if (bundler !== "webpack") {
@ -533,7 +522,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) { if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath, pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
cwd: relativePathsCwd cwd: projectDirPath
}); });
} }
@ -577,7 +566,7 @@ export function getBuildContext(params: {
if (buildOptions.keycloakifyBuildDirPath !== undefined) { if (buildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.keycloakifyBuildDirPath, pathIsh: buildOptions.keycloakifyBuildDirPath,
cwd: relativePathsCwd cwd: projectDirPath
}); });
} }
@ -606,7 +595,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) { if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.publicDirPath, pathIsh: parsedPackageJson.keycloakify.publicDirPath,
cwd: relativePathsCwd cwd: projectDirPath
}); });
} }
@ -678,7 +667,7 @@ export function getBuildContext(params: {
pathIsh: pathIsh:
parsedPackageJson.keycloakify parsedPackageJson.keycloakify
.staticDirPathInProjectBuildDirPath, .staticDirPathInProjectBuildDirPath,
cwd: relativePathsCwd cwd: projectBuildDirPath
}); });
} }
@ -1006,7 +995,7 @@ export function getBuildContext(params: {
type: "path", type: "path",
path: getAbsoluteAndInOsFormatPath({ path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath, pathIsh: urlOrPath,
cwd: relativePathsCwd cwd: projectDirPath
}) })
}; };
} }
@ -1016,7 +1005,7 @@ export function getBuildContext(params: {
? undefined ? undefined
: getAbsoluteAndInOsFormatPath({ : getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath, pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
cwd: relativePathsCwd cwd: projectDirPath
}), }),
port: buildOptions.startKeycloakOptions?.port port: buildOptions.startKeycloakOptions?.port
} }

@ -4,14 +4,13 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
DIST: "dist" DIST: "dist"
} as const; } as const;
export const THEME_TYPES = ["login", "account", "admin"] as const; export const THEME_TYPES = ["login", "account"] as const;
export type ThemeType = (typeof THEME_TYPES)[number]; export type ThemeType = (typeof THEME_TYPES)[number];
export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = { export const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT", RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG", RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
READ_KC_CONTEXT_FROM_URL: "KEYCLOAKIFY_READ_KC_CONTEXT_FROM_URL"
} as const; } as const;
export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME = export const BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME =
@ -72,18 +71,3 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
export const CONTAINER_NAME = "keycloak-keycloakify"; export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en"; export const FALLBACK_LANGUAGE_TAG = "en";
export const CUSTOM_HANDLER_ENV_NAMES = {
COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
};
export const KEYCLOAK_THEME = "keycloak-theme";
export const KEYCLOAKIFY_SPA_DEV_SERVER_PORT = "KEYCLOAKIFY_SPA_DEV_SERVER_PORT";
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";

@ -0,0 +1,95 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { assert } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "../tools/fs.rmSync";
import type { BuildContext } from "./buildContext";
import { transformCodebase } from "../tools/transformCodebase";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
export type BuildContextLike = {
publicDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function copyKeycloakResourcesToPublic(params: {
buildContext: BuildContextLike;
}) {
const { buildContext } = params;
const destDirPath = pathJoin(
buildContext.publicDirPath,
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion: readThisNpmPackageVersion()
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs
.readFileSync(keycloakifyBuildinfoFilePath)
.toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { force: true, recursive: true });
// NOTE: To remove in a while, remove the legacy keycloak-resources directory
rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
force: true,
recursive: true
});
rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
force: true,
recursive: true
});
fs.mkdirSync(destDirPath, { recursive: true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
transformCodebase({
srcDirPath: pathJoin(
getThisCodebaseRootDirPath(),
"res",
"public",
WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
),
destDirPath
});
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This directory is only used in dev mode by Keycloakify",
"It won't be included in your final build.",
"Do not modify anything in this directory.",
].join("\n")
)
);
fs.writeFileSync(
keycloakifyBuildinfoFilePath,
Buffer.from(keycloakifyBuildinfoRaw, "utf8")
);
}

@ -1,42 +0,0 @@
import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
export const BIN_NAME = "_keycloakify-custom-handler";
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
export type CommandName =
| "update-kc-gen"
| "eject-page"
| "add-story"
| "initialize-account-theme"
| "initialize-admin-theme"
| "initialize-email-theme"
| "copy-keycloak-resources-to-public";
export type ApiVersion = "v1";
export function readParams(params: { apiVersion: ApiVersion }) {
const { apiVersion } = params;
assert(apiVersion === "v1");
const commandName = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
assert(envValue !== undefined);
return envValue as CommandName;
})();
const buildContext = (() => {
const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
assert(envValue !== undefined);
return JSON.parse(envValue) as BuildContext;
})();
return { commandName, buildContext };
}

@ -1,48 +0,0 @@
import { assert, type Equals } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
import {
NOT_IMPLEMENTED_EXIT_CODE,
type CommandName,
BIN_NAME,
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
import { getNodeModulesBinDirPath } from "../tools/nodeModulesBinDirPath";
import * as fs from "fs";
assert<Equals<ApiVersion, "v1">>();
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
return { hasBeenHandled: false };
}
try {
child_process.execSync(`npx ${BIN_NAME}`, {
stdio: "inherit",
env: {
...process.env,
[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
}
});
} catch (error: any) {
const status = error.status;
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
return { hasBeenHandled: false };
}
process.exit(status);
}
return { hasBeenHandled: true };
}

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

@ -0,0 +1,175 @@
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { BuildContext } from "./buildContext";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
import { z } from "zod";
export type BuildContextLike = {
projectDirPath: string;
themeNames: string[];
environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
implementedThemeTypes: Pick<
BuildContext["implementedThemeTypes"],
"login" | "account"
>;
packageJsonFilePath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateKcGenTs(params: {
buildContext: BuildContextLike;
}): Promise<void> {
const { buildContext } = params;
const isReactProject: boolean = await (async () => {
const parsedPackageJson = await (async () => {
type ParsedPackageJson = {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
};
const zParsedPackageJson = (() => {
type TargetType = ParsedPackageJson;
const zTargetType = z.object({
dependencies: z.record(z.string()).optional(),
devDependencies: z.record(z.string()).optional()
});
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
return id<z.ZodType<TargetType>>(zTargetType);
})();
return zParsedPackageJson.parse(
JSON.parse(
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
)
);
})();
return (
{
...parsedPackageJson.dependencies,
...parsedPackageJson.devDependencies
}.react !== undefined
);
})();
const filePath = pathJoin(
buildContext.themeSrcDirPath,
`kc.gen.ts${isReactProject ? "x" : ""}`
);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
: undefined;
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const newContent = Buffer.from(
[
`/* prettier-ignore-start */`,
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`// This file is auto-generated by Keycloakify`,
``,
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
...(!isReactProject
? []
: [
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`
]),
``,
`/* prettier-ignore-end */`,
``
]
.filter(item => typeof item === "string")
.join("\n"),
"utf8"
);
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
if (!isReactProject) {
break delete_legacy_file;
}
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
}

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

@ -1,230 +0,0 @@
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";
export type BuildContextLike = {
fetchOptions: BuildContext["fetchOptions"];
cacheDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>;
export async function getSupportedDockerImageTags(params: {
buildContext: BuildContextLike;
}) {
const { buildContext } = params;
{
const result = await getCachedValue({ cacheDirPath: buildContext.cacheDirPath });
if (result !== undefined) {
return result;
}
}
const tags: string[] = [];
await (async function callee(url: string) {
const r = await fetch(url, buildContext.fetchOptions);
await Promise.all([
(async () => {
tags.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 arr = tags
.map(tag => ({
tag,
version: (() => {
if (tag.includes("-")) {
return undefined;
}
let version: SemVer;
try {
version = SemVer.parse(tag);
} catch {
return undefined;
}
return version;
})()
}))
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
.filter(exclude(undefined));
const versionByMajor: Record<number, SemVer | undefined> = {};
for (const { version } of arr) {
const version_current = versionByMajor[version.major];
if (
version_current === undefined ||
SemVer.compare(version_current, version) === -1
) {
versionByMajor[version.major] = version;
}
}
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
const result = Object.entries(versionByMajor)
.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));
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
return result;
}
const { getCachedValue, setCachedValue } = (() => {
type Cache = {
keycloakifyVersion: string;
time: number;
result: string[];
};
const zCache = (() => {
type TargetType = Cache;
const zTargetType = z.object({
keycloakifyVersion: z.string(),
time: z.number(),
result: z.array(z.string())
});
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
};
})();

@ -73,7 +73,7 @@
"composites": { "composites": {
"realm": ["offline_access", "uma_authorization"], "realm": ["offline_access", "uma_authorization"],
"client": { "client": {
"account": ["view-profile", "manage-account", "delete-account"] "account": ["delete-account", "view-profile", "manage-account"]
} }
}, },
"clientRole": false, "clientRole": false,
@ -398,26 +398,6 @@
"otpPolicyLookAheadWindow": 1, "otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30, "otpPolicyPeriod": 30,
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"], "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": [ "users": [
{ {
"id": "00a62e75-bcc1-419a-a292-63ee5d161ed3", "id": "00a62e75-bcc1-419a-a292-63ee5d161ed3",
@ -442,43 +422,30 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "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": [ "scopeMappings": [
{ {
"clientScope": "offline_access", "clientScope": "offline_access",
@ -538,12 +505,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -555,7 +518,6 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256" "pkce.code.challenge.method": "S256"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
@ -674,7 +636,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "login_theme": "keycloakify-starter",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false" "backchannel.logout.revoke.offline.tokens": "false"
@ -732,12 +694,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -749,7 +707,6 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"post.logout.redirect.uris": "+",
"pkce.code.challenge.method": "S256" "pkce.code.challenge.method": "S256"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
@ -800,8 +757,7 @@
"consentRequired": false, "consentRequired": false,
"config": { "config": {
"id.token.claim": "true", "id.token.claim": "true",
"access.token.claim": "true", "access.token.claim": "true"
"userinfo.token.claim": "true"
} }
} }
] ]
@ -1249,7 +1205,6 @@
"consentRequired": false, "consentRequired": false,
"config": { "config": {
"multivalued": "true", "multivalued": "true",
"userinfo.token.claim": "true",
"user.attribute": "foo", "user.attribute": "foo",
"id.token.claim": "true", "id.token.claim": "true",
"access.token.claim": "true", "access.token.claim": "true",
@ -1316,11 +1271,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1336,14 +1291,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"saml-user-property-mapper", "saml-user-property-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper" "saml-role-list-mapper",
"oidc-usermodel-property-mapper"
] ]
} }
}, },
@ -1392,14 +1347,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"saml-user-attribute-mapper" "oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-role-list-mapper"
] ]
} }
}, },
@ -1439,12 +1394,6 @@
"providerId": "rsa-generated", "providerId": "rsa-generated",
"subComponents": {}, "subComponents": {},
"config": { "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"] "priority": ["100"]
} }
}, },
@ -1454,12 +1403,6 @@
"providerId": "rsa-enc-generated", "providerId": "rsa-enc-generated",
"subComponents": {}, "subComponents": {},
"config": { "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"], "priority": ["100"],
"algorithm": ["RSA-OAEP"] "algorithm": ["RSA-OAEP"]
} }
@ -1470,8 +1413,6 @@
"providerId": "aes-generated", "providerId": "aes-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["132fb843-59e9-4f36-ad55-5ce2d3a13fb3"],
"secret": ["ETyyqapnrkUsNXLQ-tBVKw"],
"priority": ["100"] "priority": ["100"]
} }
}, },
@ -1481,10 +1422,6 @@
"providerId": "hmac-generated", "providerId": "hmac-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["5110d380-c930-49d9-b91b-87f338f6170b"],
"secret": [
"uCpQrJvP5OBuTxXfDb4JRL0bCKpXUgfGn5vb8UvL-Sfs_sZ9rtvBmd6vuFWARqyezjJQtpoNlMv7sXgxkN-yxQ"
],
"priority": ["100"], "priority": ["100"],
"algorithm": ["HS256"] "algorithm": ["HS256"]
} }
@ -1517,7 +1454,7 @@
"defaultLocale": "en", "defaultLocale": "en",
"authenticationFlows": [ "authenticationFlows": [
{ {
"id": "223ce532-2038-4f24-a606-2a5c73f7bd65", "id": "f7f2b89b-43cb-491d-8e7c-f1814024a6da",
"alias": "Account verification options", "alias": "Account verification options",
"description": "Method with which to verity the existing account", "description": "Method with which to verity the existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1543,7 +1480,7 @@
] ]
}, },
{ {
"id": "57e47732-79cc-4d60-bee7-4f0b8fd44540", "id": "17cdac6f-d2a3-4907-8d44-a42827610b63",
"alias": "Authentication Options", "alias": "Authentication Options",
"description": "Authentication options.", "description": "Authentication options.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1577,7 +1514,7 @@
] ]
}, },
{ {
"id": "c2735d89-60c0-45a4-9b3c-ae5df17df395", "id": "53a3e43f-9468-401f-8051-40f982d12f85",
"alias": "Browser - Conditional OTP", "alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1603,7 +1540,7 @@
] ]
}, },
{ {
"id": "11a5a507-2b9a-443f-961b-dffd66f4318d", "id": "26286808-3b7b-43df-b32e-af55a37af2e9",
"alias": "Direct Grant - Conditional OTP", "alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1629,7 +1566,7 @@
] ]
}, },
{ {
"id": "963bd753-6ea7-4d93-ab56-30f9ab59d597", "id": "8a6a752a-9a9a-4d38-b1f8-edf0a9433490",
"alias": "First broker login - Conditional OTP", "alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1655,7 +1592,7 @@
] ]
}, },
{ {
"id": "1db6a489-a3b4-44c4-b480-1d1e8c123d20", "id": "a6f6804c-4160-4a84-8a1f-c2747a2d3f27",
"alias": "Handle Existing Account", "alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1681,7 +1618,7 @@
] ]
}, },
{ {
"id": "7a38f32d-4f34-450f-8f03-64802d7cb8f1", "id": "740baa9e-8328-4035-9e1a-8fc1616d1f0f",
"alias": "Reset - Conditional OTP", "alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1707,7 +1644,7 @@
] ]
}, },
{ {
"id": "0df88739-3739-4d70-8893-47c546f19003", "id": "e60187a8-3e16-4a0c-9daa-f3a4a1fcfdba",
"alias": "User creation or linking", "alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives", "description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1734,7 +1671,7 @@
] ]
}, },
{ {
"id": "35025424-e291-4c54-8a29-70aadba549ce", "id": "d959d0c2-4004-4633-b280-f80d6423f574",
"alias": "Verify Existing Account by Re-authentication", "alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account", "description": "Reauthentication of existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1760,7 +1697,7 @@
] ]
}, },
{ {
"id": "1813b7f2-c3c2-4b92-8ffc-9ff2d12186c6", "id": "ba02689d-b9e8-4a4b-8fdd-0d1386b198fc",
"alias": "browser", "alias": "browser",
"description": "browser based authentication", "description": "browser based authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1802,7 +1739,7 @@
] ]
}, },
{ {
"id": "954283ac-f1c2-40b6-a39f-bf23ff9f3ce8", "id": "f09ac92a-e091-4e84-9cd1-cb905ca57b89",
"alias": "clients", "alias": "clients",
"description": "Base authentication for clients", "description": "Base authentication for clients",
"providerId": "client-flow", "providerId": "client-flow",
@ -1844,7 +1781,7 @@
] ]
}, },
{ {
"id": "52a789ce-2cad-4f0f-93b2-295b7fd519f0", "id": "aaf72b22-cec4-4714-93d6-f54d5a986ab8",
"alias": "direct grant", "alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant", "description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1878,7 +1815,7 @@
] ]
}, },
{ {
"id": "5a6a71e1-9105-45b6-b5f0-52538461357b", "id": "c4a54bb3-f009-4231-a82b-376c2515e07e",
"alias": "docker auth", "alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP", "description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1896,7 +1833,7 @@
] ]
}, },
{ {
"id": "8392b6e7-bdbf-4d7f-97b6-885761c200db", "id": "f55ded54-683a-4f5a-a101-9cfbd7b96781",
"alias": "first broker login", "alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1923,7 +1860,7 @@
] ]
}, },
{ {
"id": "52136d70-8d08-42ea-b04b-cf40ea2807aa", "id": "931d5a82-378f-4533-8c69-2239a4acd047",
"alias": "forms", "alias": "forms",
"description": "Username, password, otp and other auth forms.", "description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1949,7 +1886,7 @@
] ]
}, },
{ {
"id": "26bbc7e6-ef01-4cdb-9dba-520e2f3f8993", "id": "22b05374-f480-4ca8-aca8-9db8b6dd1729",
"alias": "http challenge", "alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1975,7 +1912,7 @@
] ]
}, },
{ {
"id": "f0887979-04eb-4033-8f19-0ffd8c8b7f6a", "id": "c0371832-e4b7-485e-bf23-6babe4c6ac83",
"alias": "registration", "alias": "registration",
"description": "registration flow", "description": "registration flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1994,7 +1931,7 @@
] ]
}, },
{ {
"id": "a3b7b94b-bfbf-4760-a8c9-7d9cd98d262e", "id": "4d0445da-073e-465e-b25b-af522915c73f",
"alias": "registration form", "alias": "registration form",
"description": "registration form", "description": "registration form",
"providerId": "form-flow", "providerId": "form-flow",
@ -2036,7 +1973,7 @@
] ]
}, },
{ {
"id": "dc68a665-2e51-4a22-aaad-bd693ddc77cc", "id": "740d467f-4203-425b-8203-9bfd3eed25ae",
"alias": "reset credentials", "alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something", "description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2078,7 +2015,7 @@
] ]
}, },
{ {
"id": "ae6b73aa-1318-4ae8-a3d9-d01b5e7d957e", "id": "cf1a9af9-dadd-4cb9-a26e-fbbba216f8e1",
"alias": "saml ecp", "alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow", "description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2098,14 +2035,14 @@
], ],
"authenticatorConfig": [ "authenticatorConfig": [
{ {
"id": "0c18de7f-0714-41f4-9a3f-ed4edd53ae9c", "id": "4e65eb4b-9f0a-4ab8-98b2-6daf50cd1bf8",
"alias": "create unique user config", "alias": "create unique user config",
"config": { "config": {
"require.password.update.after.registration": "false" "require.password.update.after.registration": "false"
} }
}, },
{ {
"id": "65b3c8bb-34a4-4d19-b578-245dc8ff53ea", "id": "5e8dc1c5-1489-4d39-bb75-9c499583b91b",
"alias": "review profile config", "alias": "review profile config",
"config": { "config": {
"update.profile.on.first.login": "missing" "update.profile.on.first.login": "missing"
@ -2195,8 +2132,8 @@
"attributes": { "attributes": {
"cibaBackchannelTokenDeliveryMode": "poll", "cibaBackchannelTokenDeliveryMode": "poll",
"cibaAuthRequestedUserHint": "login_hint", "cibaAuthRequestedUserHint": "login_hint",
"clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5", "oauth2DevicePollingInterval": "5",
"clientOfflineSessionMaxLifespan": "0",
"clientSessionIdleTimeout": "0", "clientSessionIdleTimeout": "0",
"userProfileEnabled": "true", "userProfileEnabled": "true",
"clientOfflineSessionIdleTimeout": "0", "clientOfflineSessionIdleTimeout": "0",

@ -73,7 +73,7 @@
"composites": { "composites": {
"realm": ["offline_access", "uma_authorization"], "realm": ["offline_access", "uma_authorization"],
"client": { "client": {
"account": ["view-profile", "manage-account", "delete-account"] "account": ["delete-account", "view-profile", "manage-account"]
} }
}, },
"clientRole": false, "clientRole": false,
@ -435,46 +435,13 @@
"type": "password", "type": "password",
"userLabel": "My password", "userLabel": "My password",
"createdDate": 1716214710762, "createdDate": 1716214710762,
"secretData": "{\"value\":\"QzJjOdXU0L9Pdxdx1V5xUs7BY9beGlmN8NpR2qiWxbkjrQ434Q1GwSiJKekZQ/zrLDtNZ7sAbVu+SS+XIe9Zaw==\",\"salt\":\"x8cABpa0Hk/nJ2BPKdFXTg==\",\"additionalParameters\":{}}", "secretData": "{\"value\":\"OaI4sKqQn+NZtS6N/bcqoZ8Q+ucpBby1n4XmzVmioKw=\",\"salt\":\"temixVCSbpA7Genml2KTAw==\",\"additionalParameters\":{}}",
"credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}"
} }
], ],
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -540,12 +507,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -680,6 +643,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
@ -740,12 +704,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1324,11 +1284,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1344,14 +1304,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-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" "oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"saml-role-list-mapper"
] ]
} }
}, },
@ -1400,14 +1360,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"saml-user-attribute-mapper" "saml-role-list-mapper",
"oidc-usermodel-property-mapper"
] ]
} }
}, },
@ -1525,7 +1485,7 @@
"defaultLocale": "en", "defaultLocale": "en",
"authenticationFlows": [ "authenticationFlows": [
{ {
"id": "1f4d4e13-1591-4751-8985-17886a8c98a9", "id": "e134634e-f219-4df4-867c-8110688d8e56",
"alias": "Account verification options", "alias": "Account verification options",
"description": "Method with which to verity the existing account", "description": "Method with which to verity the existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1551,7 +1511,7 @@
] ]
}, },
{ {
"id": "126f07c3-1bcb-4a02-bf16-bb44674bf55d", "id": "a611a8eb-9626-4aa4-8b54-ee565ea6e5dc",
"alias": "Authentication Options", "alias": "Authentication Options",
"description": "Authentication options.", "description": "Authentication options.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1585,7 +1545,7 @@
] ]
}, },
{ {
"id": "eb3a08c8-5f99-49b6-b02b-16b62571f273", "id": "d87cbb31-5c69-45c8-888d-f9649ebbbf97",
"alias": "Browser - Conditional OTP", "alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1611,7 +1571,7 @@
] ]
}, },
{ {
"id": "3dc19838-5025-4bbb-b569-b574bd5a8d90", "id": "752ba282-a369-4592-92e8-b4287192dbbf",
"alias": "Direct Grant - Conditional OTP", "alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1637,7 +1597,7 @@
] ]
}, },
{ {
"id": "70d6fd40-d740-4dae-b0e6-350f8e9d4a1c", "id": "2349282e-40ff-431a-984d-53911511e3d3",
"alias": "First broker login - Conditional OTP", "alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1663,7 +1623,7 @@
] ]
}, },
{ {
"id": "6e24dcb3-5818-483c-8e44-883858171901", "id": "4ff5463d-26d9-4219-ba85-41464401098f",
"alias": "Handle Existing Account", "alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1689,7 +1649,7 @@
] ]
}, },
{ {
"id": "ac6254cd-403b-457b-b308-22a2a0e4f99d", "id": "87bb6c6d-cca8-4832-b5ab-67ecb9454a42",
"alias": "Reset - Conditional OTP", "alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1715,7 +1675,7 @@
] ]
}, },
{ {
"id": "485e74e6-9b3e-4b2c-a9b9-927802dc4f06", "id": "1fc3d028-0e0a-43a4-aaf9-ba7f7d60b409",
"alias": "User creation or linking", "alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives", "description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1742,7 +1702,7 @@
] ]
}, },
{ {
"id": "ff9bb879-1d6a-4d1c-9836-1e4fab6f8997", "id": "036aae59-641f-4799-9124-c7e5034af6c1",
"alias": "Verify Existing Account by Re-authentication", "alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account", "description": "Reauthentication of existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1768,7 +1728,7 @@
] ]
}, },
{ {
"id": "af8b2470-d581-401c-9984-762b966ebcc2", "id": "2e8b9f28-93b8-4368-84b0-1a8326daafe0",
"alias": "browser", "alias": "browser",
"description": "browser based authentication", "description": "browser based authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1810,7 +1770,7 @@
] ]
}, },
{ {
"id": "414dbda4-eb3f-4baa-b23a-d3423af1eae6", "id": "0b826105-8493-45ce-87b3-7d917d190b39",
"alias": "clients", "alias": "clients",
"description": "Base authentication for clients", "description": "Base authentication for clients",
"providerId": "client-flow", "providerId": "client-flow",
@ -1852,7 +1812,7 @@
] ]
}, },
{ {
"id": "1cae0c4b-8dfb-4f5d-a781-e74d0a13c940", "id": "bf6d9edd-48d8-4392-bbc8-4b17a6866074",
"alias": "direct grant", "alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant", "description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1886,7 +1846,7 @@
] ]
}, },
{ {
"id": "e798b655-7d85-4b6b-aee7-1448a3e1e0ea", "id": "97e31722-dd11-42be-aa99-88788fa2dde6",
"alias": "docker auth", "alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP", "description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1904,7 +1864,7 @@
] ]
}, },
{ {
"id": "eb94b723-1041-426a-87bf-f7b4bd2f485d", "id": "3f45cf34-231f-4ea1-8e58-d636c451a76b",
"alias": "first broker login", "alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1931,7 +1891,7 @@
] ]
}, },
{ {
"id": "452d1d5f-7632-44d7-bc89-77ff2b209b3e", "id": "9bef2f7c-f989-4871-aaa7-18e2cfa73f22",
"alias": "forms", "alias": "forms",
"description": "Username, password, otp and other auth forms.", "description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1957,7 +1917,7 @@
] ]
}, },
{ {
"id": "7c1b9e8f-6b57-49d1-a9a7-494862f93c0f", "id": "0bfaa325-acde-4443-8bd8-1dc2ae759c5f",
"alias": "http challenge", "alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1983,7 +1943,7 @@
] ]
}, },
{ {
"id": "2b38f34a-1739-499e-bb24-1dff96f32009", "id": "37ddbe8c-abf3-4654-bd6d-ffabbeefbb98",
"alias": "registration", "alias": "registration",
"description": "registration flow", "description": "registration flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2002,7 +1962,7 @@
] ]
}, },
{ {
"id": "d26ae72b-a933-44dc-9927-1c82757004b2", "id": "5d7b4bc9-e93b-40da-aeb6-ba0c38392f1a",
"alias": "registration form", "alias": "registration form",
"description": "registration form", "description": "registration form",
"providerId": "form-flow", "providerId": "form-flow",
@ -2044,7 +2004,7 @@
] ]
}, },
{ {
"id": "222ee8d6-1892-4768-9ada-720274b6bf9a", "id": "ee7a56e4-c827-4f24-8b8b-8476050b0b64",
"alias": "reset credentials", "alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something", "description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2086,7 +2046,7 @@
] ]
}, },
{ {
"id": "e8b4d92c-27c1-4a9b-9b16-7ceb810fa230", "id": "360f0031-4c3b-4272-84ca-2172d430b4bc",
"alias": "saml ecp", "alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow", "description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2106,14 +2066,14 @@
], ],
"authenticatorConfig": [ "authenticatorConfig": [
{ {
"id": "e5847a0b-855d-4d93-85fd-94714be3ed92", "id": "53630acd-a33a-40e3-8786-cf85464c6f9e",
"alias": "create unique user config", "alias": "create unique user config",
"config": { "config": {
"require.password.update.after.registration": "false" "require.password.update.after.registration": "false"
} }
}, },
{ {
"id": "a2a18aa4-bd4c-4c2a-9286-e9d6c64f4812", "id": "c0d2b6a0-caad-4e90-b040-17cacdaf70bb",
"alias": "review profile config", "alias": "review profile config",
"config": { "config": {
"update.profile.on.first.login": "missing" "update.profile.on.first.login": "missing"

@ -73,7 +73,7 @@
"composites": { "composites": {
"realm": ["offline_access", "uma_authorization"], "realm": ["offline_access", "uma_authorization"],
"client": { "client": {
"account": ["view-profile", "manage-account", "delete-account"] "account": ["delete-account", "view-profile", "manage-account"]
} }
}, },
"clientRole": false, "clientRole": false,
@ -407,7 +407,7 @@
"otpPolicyLookAheadWindow": 1, "otpPolicyLookAheadWindow": 1,
"otpPolicyPeriod": 30, "otpPolicyPeriod": 30,
"otpPolicyCodeReusable": false, "otpPolicyCodeReusable": false,
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"], "otpSupportedApplications": ["totpAppGoogleName", "totpAppFreeOTPName"],
"webAuthnPolicyRpEntityName": "keycloak", "webAuthnPolicyRpEntityName": "keycloak",
"webAuthnPolicySignatureAlgorithms": ["ES256"], "webAuthnPolicySignatureAlgorithms": ["ES256"],
"webAuthnPolicyRpId": "", "webAuthnPolicyRpId": "",
@ -452,40 +452,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -551,12 +517,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -691,6 +653,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
@ -751,12 +714,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1335,11 +1294,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1355,14 +1314,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper", "saml-user-property-mapper",
"saml-user-attribute-mapper" "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper"
] ]
} }
}, },
@ -1411,14 +1370,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-property-mapper", "saml-user-property-mapper",
"oidc-usermodel-attribute-mapper" "oidc-usermodel-property-mapper"
] ]
} }
}, },
@ -1536,7 +1495,7 @@
"defaultLocale": "en", "defaultLocale": "en",
"authenticationFlows": [ "authenticationFlows": [
{ {
"id": "c40791b4-4d59-4df2-bebd-2b71e793704f", "id": "19317acb-fe8e-4c79-82bc-90e159273075",
"alias": "Account verification options", "alias": "Account verification options",
"description": "Method with which to verity the existing account", "description": "Method with which to verity the existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1562,7 +1521,7 @@
] ]
}, },
{ {
"id": "8813b6d1-8b88-4672-b29b-8420ce3f3975", "id": "122857d2-33da-4086-8acb-cb0e303aaf1b",
"alias": "Authentication Options", "alias": "Authentication Options",
"description": "Authentication options.", "description": "Authentication options.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1596,7 +1555,7 @@
] ]
}, },
{ {
"id": "a9937c40-a1ee-4c57-adf7-ede0a9983953", "id": "abf5dd35-4791-4268-a10c-5f4b6a06b84a",
"alias": "Browser - Conditional OTP", "alias": "Browser - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1622,7 +1581,7 @@
] ]
}, },
{ {
"id": "2d494b5a-eb73-40d0-94d3-a8d8024a7db4", "id": "a18daeec-a33c-4a43-b014-10c84ec69b81",
"alias": "Direct Grant - Conditional OTP", "alias": "Direct Grant - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1648,7 +1607,7 @@
] ]
}, },
{ {
"id": "2e977f5a-8110-412b-b704-3e15164dbb1b", "id": "e9f032a7-32f7-457c-becf-011a1a35cc6a",
"alias": "First broker login - Conditional OTP", "alias": "First broker login - Conditional OTP",
"description": "Flow to determine if the OTP is required for the authentication", "description": "Flow to determine if the OTP is required for the authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1674,7 +1633,7 @@
] ]
}, },
{ {
"id": "6f171b4b-8723-4e6d-bb1e-6b4293a7bb3f", "id": "9db65b7c-98ca-4003-beea-611038831ffe",
"alias": "Handle Existing Account", "alias": "Handle Existing Account",
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1700,7 +1659,7 @@
] ]
}, },
{ {
"id": "2dbb7f27-757d-4178-8217-4a24fdb0163c", "id": "7bd0854c-d7ae-43d7-a1ae-7b759a34cb1d",
"alias": "Reset - Conditional OTP", "alias": "Reset - Conditional OTP",
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1726,7 +1685,7 @@
] ]
}, },
{ {
"id": "7295aaf7-acf4-4b78-8186-d2415ea4ede0", "id": "2de1a450-fe98-443a-9c6c-d24d8a7ebcb3",
"alias": "User creation or linking", "alias": "User creation or linking",
"description": "Flow for the existing/non-existing user alternatives", "description": "Flow for the existing/non-existing user alternatives",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1753,7 +1712,7 @@
] ]
}, },
{ {
"id": "e0d34d7c-7bbb-4847-8864-fbd97a1f3e89", "id": "7b3efad5-4b7d-4385-a41c-fecc73afdcc4",
"alias": "Verify Existing Account by Re-authentication", "alias": "Verify Existing Account by Re-authentication",
"description": "Reauthentication of existing account", "description": "Reauthentication of existing account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1779,7 +1738,7 @@
] ]
}, },
{ {
"id": "5f3d0fb0-d95e-4841-89d3-a27d0cdbbcb4", "id": "de93418e-8f28-4099-b15e-ad36ec194796",
"alias": "browser", "alias": "browser",
"description": "browser based authentication", "description": "browser based authentication",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1821,7 +1780,7 @@
] ]
}, },
{ {
"id": "c246380d-af25-4151-ab19-1f1e5b553008", "id": "0dd3345c-6e82-4c3a-a39a-d49ae1f5c409",
"alias": "clients", "alias": "clients",
"description": "Base authentication for clients", "description": "Base authentication for clients",
"providerId": "client-flow", "providerId": "client-flow",
@ -1863,7 +1822,7 @@
] ]
}, },
{ {
"id": "abacf398-0f1f-4f28-a310-8d306d588048", "id": "87fb4dd0-5326-47a1-b670-982f4872ff89",
"alias": "direct grant", "alias": "direct grant",
"description": "OpenID Connect Resource Owner Grant", "description": "OpenID Connect Resource Owner Grant",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1897,7 +1856,7 @@
] ]
}, },
{ {
"id": "a0f87683-619a-44d4-8b4f-4b053bba2346", "id": "344723b3-4ab1-4999-abdd-32398e82327b",
"alias": "docker auth", "alias": "docker auth",
"description": "Used by Docker clients to authenticate against the IDP", "description": "Used by Docker clients to authenticate against the IDP",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1915,7 +1874,7 @@
] ]
}, },
{ {
"id": "e8820c7c-22a7-4618-beb7-3e09be72c00c", "id": "f3341938-caf9-4c8a-9cd5-eb34609809ab",
"alias": "first broker login", "alias": "first broker login",
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1942,7 +1901,7 @@
] ]
}, },
{ {
"id": "cac00c38-ee44-44c9-b95e-cc755bab36ef", "id": "ba7b7357-e324-4b71-9bda-f8512a760e02",
"alias": "forms", "alias": "forms",
"description": "Username, password, otp and other auth forms.", "description": "Username, password, otp and other auth forms.",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1968,7 +1927,7 @@
] ]
}, },
{ {
"id": "688cde36-507e-4a68-afdf-18ec4ad626a7", "id": "134971e6-bf63-432c-806e-74ca4fb09963",
"alias": "http challenge", "alias": "http challenge",
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes", "description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -1994,7 +1953,7 @@
] ]
}, },
{ {
"id": "e058697c-f450-4f14-ae64-04e9299fa24f", "id": "6ea9e2cf-5684-4c65-8c07-930d1cbb0b46",
"alias": "registration", "alias": "registration",
"description": "registration flow", "description": "registration flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2013,7 +1972,7 @@
] ]
}, },
{ {
"id": "ad768088-32c9-4979-90dd-61bf111fd72e", "id": "67e3c8c7-1b5e-4119-84a2-e90876293150",
"alias": "registration form", "alias": "registration form",
"description": "registration form", "description": "registration form",
"providerId": "form-flow", "providerId": "form-flow",
@ -2055,7 +2014,7 @@
] ]
}, },
{ {
"id": "47d4b090-f965-4588-b5bc-029ccb59876f", "id": "fc6d48ec-a1f1-41b1-9310-54f58861d5aa",
"alias": "reset credentials", "alias": "reset credentials",
"description": "Reset credentials for a user if they forgot their password or something", "description": "Reset credentials for a user if they forgot their password or something",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2097,7 +2056,7 @@
] ]
}, },
{ {
"id": "1f68feec-7f99-4c49-afe6-45d46684ca21", "id": "80b1d464-c2ec-4eb1-82e8-32cbede779a8",
"alias": "saml ecp", "alias": "saml ecp",
"description": "SAML ECP Profile Authentication Flow", "description": "SAML ECP Profile Authentication Flow",
"providerId": "basic-flow", "providerId": "basic-flow",
@ -2117,14 +2076,14 @@
], ],
"authenticatorConfig": [ "authenticatorConfig": [
{ {
"id": "bd7365c7-842b-4bc6-a4ca-498cf025c210", "id": "86b1d5fa-450c-40d8-899c-725861ac39fc",
"alias": "create unique user config", "alias": "create unique user config",
"config": { "config": {
"require.password.update.after.registration": "false" "require.password.update.after.registration": "false"
} }
}, },
{ {
"id": "b929192d-f650-4a09-9701-3d3216547552", "id": "ea724f02-029a-493d-b4d3-08972be21cfb",
"alias": "review profile config", "alias": "review profile config",
"config": { "config": {
"update.profile.on.first.login": "missing" "update.profile.on.first.login": "missing"

@ -73,7 +73,7 @@
"composites": { "composites": {
"realm": ["offline_access", "uma_authorization"], "realm": ["offline_access", "uma_authorization"],
"client": { "client": {
"account": ["view-profile", "manage-account", "delete-account"] "account": ["delete-account", "view-profile", "manage-account"]
} }
}, },
"clientRole": false, "clientRole": false,
@ -456,40 +456,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -555,12 +521,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -695,6 +657,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
@ -755,12 +718,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1339,11 +1298,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1359,13 +1318,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-attribute-mapper", "oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-usermodel-property-mapper", "saml-user-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper" "oidc-address-mapper"
] ]
} }
@ -1415,14 +1374,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper", "saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"saml-user-attribute-mapper" "saml-user-property-mapper"
] ]
} }
}, },

@ -55,7 +55,7 @@
"composites": { "composites": {
"realm": ["offline_access", "uma_authorization"], "realm": ["offline_access", "uma_authorization"],
"client": { "client": {
"account": ["view-profile", "delete-account", "manage-account"] "account": ["delete-account", "view-profile", "manage-account"]
} }
}, },
"clientRole": false, "clientRole": false,
@ -459,40 +459,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -539,6 +505,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
@ -565,12 +532,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -686,11 +649,7 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
"https://my-theme.keycloakify.dev/*",
"http://localhost*",
"http://127.0.0.1*"
],
"webOrigins": ["*"], "webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
@ -705,7 +664,8 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false" "backchannel.logout.revoke.offline.tokens": "false"
@ -765,12 +725,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1380,12 +1336,12 @@
"strictTransportSecurity": "max-age=31536000; includeSubDomains" "strictTransportSecurity": "max-age=31536000; includeSubDomains"
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1401,13 +1357,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
} }
@ -1477,13 +1433,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
} }

@ -468,40 +468,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -548,6 +514,7 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
@ -574,12 +541,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -695,11 +658,7 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
"https://my-theme.keycloakify.dev/*",
"http://localhost*",
"http://127.0.0.1*"
],
"webOrigins": ["*"], "webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
@ -714,7 +673,8 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false" "backchannel.logout.revoke.offline.tokens": "false"
@ -880,12 +840,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1495,12 +1451,12 @@
"strictTransportSecurity": "max-age=31536000; includeSubDomains" "strictTransportSecurity": "max-age=31536000; includeSubDomains"
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloak",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1548,11 +1504,11 @@
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper" "oidc-full-name-mapper",
"saml-user-property-mapper"
] ]
} }
}, },
@ -1584,14 +1540,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-full-name-mapper",
"oidc-usermodel-property-mapper",
"saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper", "oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-property-mapper" "saml-user-property-mapper",
"saml-role-list-mapper",
"saml-user-attribute-mapper"
] ]
} }
}, },

@ -538,10 +538,10 @@
"emailVerified": true, "emailVerified": true,
"attributes": { "attributes": {
"additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"], "additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
"favorite_pet": ["cats"],
"gender": ["prefer_not_to_say"], "gender": ["prefer_not_to_say"],
"bio": ["Hello I'm Test User and I do not exist."], "favorite_pet": ["cats"],
"favourite_pet": ["cat"], "favourite_pet": ["cat"],
"bio": ["Hello I'm Test User and I do not exist."],
"phone_number": ["1111111111"], "phone_number": ["1111111111"],
"locale": ["en"], "locale": ["en"],
"favorite_media": ["movies", "series"] "favorite_media": ["movies", "series"]
@ -562,40 +562,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -662,16 +628,14 @@
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3", "id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
"clientId": "account-console", "clientId": "account-console",
"name": "${client_account-console}", "name": "${client_account-console}",
"description": "",
"rootUrl": "${authBaseUrl}", "rootUrl": "${authBaseUrl}",
"adminUrl": "",
"baseUrl": "/realms/myrealm/account/", "baseUrl": "/realms/myrealm/account/",
"surrogateAuthRequired": false, "surrogateAuthRequired": false,
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": ["http://localhost*", "http://127.0.0.1*", "*"], "redirectUris": ["/realms/myrealm/account/*"],
"webOrigins": ["*"], "webOrigins": [],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -683,13 +647,8 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false", "pkce.code.challenge.method": "S256"
"display.on.consent.screen": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
@ -832,7 +791,8 @@
"attributes": { "attributes": {
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false" "backchannel.logout.revoke.offline.tokens": "false"
@ -925,12 +885,8 @@
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -1588,11 +1544,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1618,14 +1574,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-role-list-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"saml-user-property-mapper",
"saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper"
] ]
} }
}, },
@ -1655,14 +1611,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-address-mapper",
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-property-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper", "saml-user-property-mapper",
"oidc-usermodel-property-mapper" "saml-role-list-mapper"
] ]
} }
}, },
@ -1716,10 +1672,11 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"privateKey": [ "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/" "MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
], ],
"keyUse": ["SIG"],
"certificate": [ "certificate": [
"MIICnTCCAYUCBgGTy2TGBjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALKPPaqby4X/QyHAm9iQG8SLUS0iLz/zQmYTHQBMUx8q4TOQdu2UOjGe3SUG5zLu0Ce8ogpbA9V5wze1pY9O7GNgLN5WN7/ZIzgwXgA2JT7jdZu/31xR196n0U/j8FUmgITMpJqr1qfOlE4O7e6zY1QZ1zz1yLZkSyBfo2VDFkHbsDc56+iYeeGXSMBMuIb19W6TcNJz5eCl4jotnpb4Pq5OwYRcsBsEPx1AhpK3KtF0v2jTstQoZqg27avNe5mKVXrXgg3vMT9zD62f7Ni4hLeCKdXDaNHWPmuCcDs8C+NodxmkfvNIit6WTaPjK9CgTmPRl73VfzL3Dxy0O1bo1BkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAggzxmYvHqUaCPLxxSidLQMgpu1pTozg3rTq8dcxhcHINI//A/z7qQyDA/QQN5cuSpYvdt2MRWoNop+uRNKqSr3C8aRErbY0j4acl7yG/ghNfQUZ9KxDBxKrd0HLFUibdZobg10+Ih/qXo3Mi2VtkqyZQRl/iy0O3ITgqb7YJUEx5tuEWyGbn+SerFvqZNcmsLziOJefm1n4uqroHgIfmgY6Deh+wZK0DwO3WZ6ThjhMp5GFi1oNeZ9xoExNEXrYp07b2xTQFF57oypc7prf733lqGjPRLfoVJP6qcsjvAlOA7f8TG9sKwGuRsPfadYY9PxmdHxl2k7PHDJeDhA7VdQ==" "MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
], ],
"priority": ["100"] "priority": ["100"]
} }
@ -1731,10 +1688,11 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"privateKey": [ "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" "MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
], ],
"keyUse": ["ENC"],
"certificate": [ "certificate": [
"MIICnTCCAYUCBgGTy2TG/jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE3MzQ1OVoXDTM0MTIxNTE3MzYzOVowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMaBL57/mBwqlFgDRrpLuUGLHAGclBNFh0pXAM2dG0LoD6MOt6YkLLsT89HKTyLi2m+mnbLF1KihBYXk9R9AarZk/S/+4jigejEbiJzeGkc80x/atNkyj5HKUgMwB0xnLCQTDGsJW5yHTLufpnrzD8anlVcb65yhtu0vB91uY6iJEMPG6OuOh695eQ8av42O7JXEl113uGXXn3gr50qnmvn8uXPq/6+eOp1+BUxopJbuii0Gg3PUnxnHK2Q5pY2Ow19ZB65ryHS0oXLrpEx3NIYtqm4DHFLXv+/7L0iS1FjmTmysXrXeMtTgOPb9+sazKhocBg3cK1n9FbjATfHhzgkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAdUIlJ91E0UkFS45AByjFufRnQbAi1smnHkC3WSN39bhcFT7Hgip97qtABODR58zVHSTS0XcMiL4mMObH3Vyz9J3gmwWZnbokAuo9tYeyrhPh/gqXv3LGtGhTpWlUJ7JEJxH7RVI4UZZyG6Y6FR+3zwiZ0j1p3QsZclfcNmacoi/Ano+4TfloOnY4k8yP7G6LWUTJHpcRNWVVozM3RwekYgpJRAtXDoYfm9p2hRQ090e7NvbblSuVQ/FXhUn4g0wz91WdCWlwXZfvNaRjbynPCHejJpszqiyjPkx3aRKTWqer0ZocKNmY8+RO27XIsXmwOYcjdpX2TCFDv6O+VLfNdw==" "MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
], ],
"priority": ["100"], "priority": ["100"],
"algorithm": ["RSA-OAEP"] "algorithm": ["RSA-OAEP"]
@ -1746,8 +1704,8 @@
"providerId": "aes-generated", "providerId": "aes-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["95db7eb8-b57b-475e-90cd-58841a9388d3"], "kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
"secret": ["dp6bv53YrC2PZuJCxa3aNA"], "secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
"priority": ["100"] "priority": ["100"]
} }
}, },
@ -1757,9 +1715,9 @@
"providerId": "hmac-generated", "providerId": "hmac-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["d0254883-059e-4fdd-bf03-704c76650aab"], "kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
"secret": [ "secret": [
"bcW7E4rcbgSKZIQysWOSuhezRGYs5Kzmp3ZESthdTUMyFivK8RbBAdBE4PhFPk5B9TuByDO2RWvd8F7F5YhGJitf6cfYB1BfDuAk-2iBAtdZA98g7a2h4jpwzh-GIgtoRbGbH9qnquUn52f5qteo34g5WifKE2bWjOELza9FrTo" "j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
], ],
"priority": ["100"], "priority": ["100"],
"algorithm": ["HS512"] "algorithm": ["HS512"]
@ -2430,7 +2388,7 @@
"clientSessionMaxLifespan": "0", "clientSessionMaxLifespan": "0",
"organizationsEnabled": "false" "organizationsEnabled": "false"
}, },
"keycloakVersion": "25.0.6", "keycloakVersion": "25.0.0",
"userManagedAccessAllowed": false, "userManagedAccessAllowed": false,
"organizationsEnabled": false, "organizationsEnabled": false,
"clientProfiles": { "clientProfiles": {

@ -38,7 +38,6 @@
"bruteForceProtected": false, "bruteForceProtected": false,
"permanentLockout": false, "permanentLockout": false,
"maxTemporaryLockouts": 0, "maxTemporaryLockouts": 0,
"bruteForceStrategy": "MULTIPLE",
"maxFailureWaitSeconds": 900, "maxFailureWaitSeconds": 900,
"minimumQuickLoginWaitSeconds": 60, "minimumQuickLoginWaitSeconds": 60,
"waitIncrementSeconds": 60, "waitIncrementSeconds": 60,
@ -563,40 +562,6 @@
"disableableCredentialTypes": [], "disableableCredentialTypes": [],
"requiredActions": [], "requiredActions": [],
"realmRoles": ["default-roles-myrealm"], "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, "notBefore": 0,
"groups": [] "groups": []
} }
@ -639,7 +604,6 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "false",
"post.logout.redirect.uris": "+" "post.logout.redirect.uris": "+"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
@ -664,20 +628,14 @@
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3", "id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
"clientId": "account-console", "clientId": "account-console",
"name": "${client_account-console}", "name": "${client_account-console}",
"description": "",
"rootUrl": "${authBaseUrl}", "rootUrl": "${authBaseUrl}",
"adminUrl": "",
"baseUrl": "/realms/myrealm/account/", "baseUrl": "/realms/myrealm/account/",
"surrogateAuthRequired": false, "surrogateAuthRequired": false,
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/realms/myrealm/account/*"],
"http://localhost*", "webOrigins": [],
"http://127.0.0.1*",
"/realms/myrealm/account/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -689,14 +647,8 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false", "pkce.code.challenge.method": "S256"
"display.on.consent.screen": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
@ -747,12 +699,10 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "false",
"client.use.lightweight.access.token.enabled": "true",
"post.logout.redirect.uris": "+" "post.logout.redirect.uris": "+"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"defaultClientScopes": [ "defaultClientScopes": [
"web-origins", "web-origins",
@ -790,7 +740,6 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "true",
"post.logout.redirect.uris": "+" "post.logout.redirect.uris": "+"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
@ -840,10 +789,10 @@
"frontchannelLogout": true, "frontchannelLogout": true,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "false",
"oidc.ciba.grant.enabled": "false", "oidc.ciba.grant.enabled": "false",
"backchannel.logout.session.required": "true", "backchannel.logout.session.required": "true",
"post.logout.redirect.uris": "+", "login_theme": "keycloakify-starter",
"post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
"oauth2.device.authorization.grant.enabled": "false", "oauth2.device.authorization.grant.enabled": "false",
"display.on.consent.screen": "false", "display.on.consent.screen": "false",
"backchannel.logout.revoke.offline.tokens": "false" "backchannel.logout.revoke.offline.tokens": "false"
@ -906,7 +855,6 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "attributes": {
"realm_client": "true",
"post.logout.redirect.uris": "+" "post.logout.redirect.uris": "+"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
@ -931,20 +879,14 @@
"id": "fce8a109-6f32-4814-9a20-2ff2435d2da6", "id": "fce8a109-6f32-4814-9a20-2ff2435d2da6",
"clientId": "security-admin-console", "clientId": "security-admin-console",
"name": "${client_security-admin-console}", "name": "${client_security-admin-console}",
"description": "",
"rootUrl": "${authAdminUrl}", "rootUrl": "${authAdminUrl}",
"adminUrl": "",
"baseUrl": "/admin/myrealm/console/", "baseUrl": "/admin/myrealm/console/",
"surrogateAuthRequired": false, "surrogateAuthRequired": false,
"enabled": true, "enabled": true,
"alwaysDisplayInConsole": false, "alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret", "clientAuthenticatorType": "client-secret",
"redirectUris": [ "redirectUris": ["/admin/myrealm/console/*"],
"http://localhost*", "webOrigins": ["+"],
"http://127.0.0.1*",
"/admin/myrealm/console/*"
],
"webOrigins": ["*"],
"notBefore": 0, "notBefore": 0,
"bearerOnly": false, "bearerOnly": false,
"consentRequired": false, "consentRequired": false,
@ -956,18 +898,11 @@
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": { "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": "+", "post.logout.redirect.uris": "+",
"oauth2.device.authorization.grant.enabled": "false", "pkce.code.challenge.method": "S256"
"display.on.consent.screen": "false",
"pkce.code.challenge.method": "S256",
"backchannel.logout.revoke.offline.tokens": "false"
}, },
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
"protocolMappers": [ "protocolMappers": [
{ {
@ -985,24 +920,6 @@
"claim.name": "locale", "claim.name": "locale",
"jsonType.label": "String" "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": "false",
"access.token.claim": "true",
"claim.name": "allowed-origins",
"jsonType.label": "JSON",
"access.tokenResponse.claim": "false"
}
} }
], ],
"defaultClientScopes": [ "defaultClientScopes": [
@ -1627,11 +1544,11 @@
}, },
"smtpServer": {}, "smtpServer": {},
"loginTheme": "keycloakify-starter", "loginTheme": "keycloakify-starter",
"accountTheme": "", "accountTheme": "keycloakify-starter",
"adminTheme": "", "adminTheme": "",
"emailTheme": "", "emailTheme": "",
"eventsEnabled": false, "eventsEnabled": false,
"eventsListeners": ["keycloakify-logging", "jboss-logging"], "eventsListeners": ["jboss-logging"],
"enabledEventTypes": [], "enabledEventTypes": [],
"adminEventsEnabled": false, "adminEventsEnabled": false,
"adminEventsDetailsEnabled": false, "adminEventsDetailsEnabled": false,
@ -1657,14 +1574,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-usermodel-property-mapper", "oidc-full-name-mapper",
"saml-user-property-mapper", "oidc-usermodel-attribute-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-property-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper"
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper"
] ]
} }
}, },
@ -1694,14 +1611,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-attribute-mapper",
"oidc-full-name-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"saml-role-list-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-usermodel-attribute-mapper" "oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper"
] ]
} }
}, },
@ -1755,10 +1672,11 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"privateKey": [ "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==" "MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
], ],
"keyUse": ["SIG"],
"certificate": [ "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==" "MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
], ],
"priority": ["100"] "priority": ["100"]
} }
@ -1770,10 +1688,11 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"privateKey": [ "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=" "MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
], ],
"keyUse": ["ENC"],
"certificate": [ "certificate": [
"MIICnTCCAYUCBgGTulJDCDANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxMjEwMDExM1oXDTM0MTIxMjEwMDI1M1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALp4C+KLC8j/GxOjEio/GTCUxvFgYdytv8QeQSWAmHOz9QiDkWSPaP7ykMdUJz/gl5CCp50dpMyMwtK506pSDpGmX4Zkp+dIxubzQUvaj0/llO1Ht6isbliRcHGhVOUhCWsCR6pwzyvqZLZJMZLnXjB8+omij7PsCnG360Sevt2txakEHRxEIOCI1VPKQWZmoNXOyR2sojOejk+cuo5hOSb2OtPefYj62T10kzFN4TaIlcHciukMHSvv9MKzuvNVYSD/rtqXvdp0PobIDEkkCXoC8dVLBBMlPO7Mm5Rvqd8wCkkvpLdp74lf2tnGIojBNC3ZUN5ZyrEOLdWOsmQYktkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAPhPdLFcXdQT4k06oXB06ZSJ8AkZNXLvQFWCHXI34OmrS2yTse+dLqrqehnC3kPwxElVmawoUVc1sbsk7fUnspfM+Xw20PaABZu4MO2m5TB98f1hEkezP9fSqgPeuWJgTL8ZW5kkZyiD3IaZoqyxzYXaFxKHhU455g+k2+DO+N6FreVKcYz12Q5EMaxZ6U1neZAo3vicNxM3/TA5V8sPK8+oKvon7v5OyjpOH0goJo9v/klKeUk36h4u2h1S67IhVSU7tfzVFYrpns1JhrwGZ2xavVqEoqX8zFp3GKz3yVXkwHRHlrzYkZoGn21rm5boXIP3wEB7yXZbXWTiUko/IFw==" "MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
], ],
"priority": ["100"], "priority": ["100"],
"algorithm": ["RSA-OAEP"] "algorithm": ["RSA-OAEP"]
@ -1785,8 +1704,8 @@
"providerId": "aes-generated", "providerId": "aes-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["c36222c6-6a43-4d32-9d44-d5d355e5cabd"], "kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
"secret": ["rzL4qUQ7wTEkZDbgt595VA"], "secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
"priority": ["100"] "priority": ["100"]
} }
}, },
@ -1796,9 +1715,9 @@
"providerId": "hmac-generated", "providerId": "hmac-generated",
"subComponents": {}, "subComponents": {},
"config": { "config": {
"kid": ["06532a54-c310-41c1-829c-58776ce2ab4a"], "kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
"secret": [ "secret": [
"9v1ZjFhEFH6UpY6ncFkaCbqJYHMyI4tA0cvx4GuQ5KtMXYbimitSSVDqxIKwa-gBC_8bY2O4FQfpmp1Qn1-L4fFmPFfIF3ZKsO16263BwpADo_FNSBTte8Le4gJLylqFULdsn3ye17FHyq5Jjms_OTt3opzcDLNduCuK22GBBsU" "j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
], ],
"priority": ["100"], "priority": ["100"],
"algorithm": ["HS512"] "algorithm": ["HS512"]
@ -2469,7 +2388,7 @@
"clientSessionMaxLifespan": "0", "clientSessionMaxLifespan": "0",
"organizationsEnabled": "false" "organizationsEnabled": "false"
}, },
"keycloakVersion": "26.0.7", "keycloakVersion": "25.0.0",
"userManagedAccessAllowed": false, "userManagedAccessAllowed": false,
"organizationsEnabled": false, "organizationsEnabled": false,
"clientProfiles": { "clientProfiles": {

@ -1,136 +0,0 @@
import { z } from "zod";
import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is";
import { id } from "tsafe/id";
import * as fs from "fs";
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>;
}[];
}[];
};
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);
})();
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;
}

@ -1,75 +0,0 @@
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 { type ParsedRealmJson, readRealmJsonFile } from "../ParsedRealmJson";
export 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
})
});
}

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

@ -1,194 +0,0 @@
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
});
}

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

@ -1,365 +0,0 @@
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": "false",
"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);
}
}
}
}

@ -1,159 +0,0 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
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, 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 });
}
}
const writeRealmJsonFile = async (params: { parsedRealmJson: ParsedRealmJson }) => {
const { parsedRealmJson } = params;
let sourceCode = JSON.stringify(parsedRealmJson, null, 2);
if (await getIsPrettierAvailable()) {
sourceCode = await runPrettier({
sourceCode,
filePath: realmJsonFilePath
});
}
fs.writeFileSync(realmJsonFilePath, sourceCode);
};
await writeRealmJsonFile({ parsedRealmJson });
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({ parsedRealmJson });
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
};
}

@ -1,11 +1,8 @@
import type { BuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import { import type { CliCommandOptions as CliCommandOptions_common } from "../main";
CONTAINER_NAME, import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
KEYCLOAKIFY_SPA_DEV_SERVER_PORT, import { CONTAINER_NAME } from "../shared/constants";
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
TEST_APP_URL
} from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs"; import * as fs from "fs";
@ -13,7 +10,8 @@ import {
join as pathJoin, join as pathJoin,
relative as pathRelative, relative as pathRelative,
sep as pathSep, sep as pathSep,
basename as pathBasename basename as pathBasename,
dirname as pathDirname
} from "path"; } from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import chalk from "chalk"; import chalk from "chalk";
@ -30,19 +28,14 @@ import { isInside } from "../tools/isInside";
import { existsAsync } from "../tools/fs.existsAsync"; import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm"; import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; 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: { export type CliCommandOptions = CliCommandOptions_common & {
buildContext: BuildContext; port: number | undefined;
cliCommandOptions: { keycloakVersion: string | undefined;
port: number | undefined; realmJsonFilePath: string | undefined;
keycloakVersion: string | undefined; };
realmJsonFilePath: string | undefined;
}; export async function command(params: { cliCommandOptions: CliCommandOptions }) {
}) {
exit_if_docker_not_installed: { exit_if_docker_not_installed: {
let commandOutput: string | undefined = undefined; let commandOutput: string | undefined = undefined;
@ -95,34 +88,13 @@ export async function command(params: {
process.exit(1); process.exit(1);
} }
const { cliCommandOptions, buildContext } = params; const { cliCommandOptions } = params;
const availableTags = await getSupportedDockerImageTags({ const buildContext = getBuildContext({ cliCommandOptions });
buildContext
});
const { dockerImageTag } = await (async () => { const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) { if (cliCommandOptions.keycloakVersion !== undefined) {
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion; return { dockerImageTag: cliCommandOptions.keycloakVersion };
const tag = availableTags.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) { if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
@ -137,84 +109,50 @@ export async function command(params: {
"On which version of Keycloak do you want to test your theme?" "On which version of Keycloak do you want to test your theme?"
), ),
chalk.gray( chalk.gray(
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)" "You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)"
) )
].join("\n") ].join("\n")
); );
const { value: tag } = await cliSelect<string>({ const { keycloakVersion } = await promptKeycloakVersion({
values: availableTags startingFromMajor: 18,
}).catch(() => { excludeMajorVersions: [22],
process.exit(-1); doOmitPatch: true,
buildContext
}); });
console.log(`${tag}`); console.log(`${keycloakVersion}`);
return { dockerImageTag: tag }; return { dockerImageTag: keycloakVersion };
})(); })();
const keycloakMajorVersionNumber = (() => { const keycloakMajorVersionNumber = (() => {
const [wrap] = getSupportedKeycloakMajorVersions() 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]
.map(majorVersionNumber => ({ .map(majorVersionNumber => ({
majorVersionNumber, majorVersionNumber,
index: dockerImageTag.indexOf(`${majorVersionNumber}`) index: tag.indexOf(`${majorVersionNumber}`)
})) }))
.filter(({ index }) => index !== -1) .filter(({ index }) => index !== -1)
.sort((a, b) => a.index - b.index); .sort((a, b) => a.index - b.index);
if (wrap === undefined) { if (wrap === undefined) {
try { console.warn(
const version = SemVer.parse(dockerImageTag); chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
console.error( )
chalk.yellow( );
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}` return 25;
)
);
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; 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({ const { isAppBuildSuccess } = await appBuild({
buildContext buildContext
@ -252,48 +190,154 @@ export async function command(params: {
assert(jarFilePath !== undefined); assert(jarFilePath !== undefined);
const extensionJarFilePaths = [ const extensionJarFilePaths = await Promise.all(
...(keycloakMajorVersionNumber <= 20 buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
? (console.log( switch (extensionJar.type) {
chalk.yellow( case "path": {
"WARNING: With older version of keycloak your changes to the realm configuration are not persisted" assert(
) await existsAsync(extensionJar.path),
), `${extensionJar.path} does not exist`
[]) );
: [ return extensionJar.path;
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;
}
} }
assert<Equals<typeof extensionJar, never>>(false); 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");
}
async function extractThemeResourcesFromJar() { async function extractThemeResourcesFromJar() {
await extractArchive({ await extractArchive({
@ -333,76 +377,17 @@ export async function command(params: {
}); });
} catch {} } catch {}
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080; const DEFAULT_PORT = 8080;
const port =
const doStartDevServer = (() => { cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
const hasSpaUi =
buildContext.implementedThemeTypes.admin.isImplemented ||
(buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page");
if (!hasSpaUi) {
return false;
}
if (buildContext.bundler !== "vite") {
console.log(
chalk.yellow(
[
`WARNING: Since you are using ${buildContext.bundler} instead of Vite,`,
`you'll have to wait serval seconds for the changes you made on your account or admin theme to be reflected in the browser.\n`,
`For a better development experience, consider migrating to Vite.`
].join(" ")
)
);
return false;
}
if (keycloakMajorVersionNumber < 25) {
console.log(
chalk.yellow(
[
`WARNING: Your account or admin theme can't be tested with hot module replacement on Keycloak ${keycloakMajorVersionNumber}.`,
`This mean that you'll have to wait serval seconds for the changes to be reflected in the browser.`,
`For a better development experience, select a more recent version of Keycloak.`
].join("\n")
)
);
return false;
}
return true;
})();
let devServerPort: number | undefined = undefined;
if (doStartDevServer) {
const { port } = await startViteDevServer({ buildContext });
devServerPort = port;
}
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd"; const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
const dockerRunArgs: string[] = [ const dockerRunArgs: string[] = [
`-p${SPACE_PLACEHOLDER}${port}:8080`, `-p${SPACE_PLACEHOLDER}${port}:8080`,
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`, `--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
...(keycloakMajorVersionNumber >= 26 `-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
? [ `-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
`-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
? []
: [
`-e${SPACE_PLACEHOLDER}${KEYCLOAKIFY_SPA_DEV_SERVER_PORT}=${devServerPort}`
]),
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0 ...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
? [] ? []
: [ : [
@ -413,12 +398,12 @@ export async function command(params: {
...(realmJsonFilePath === undefined ...(realmJsonFilePath === undefined
? [] ? []
: [ : [
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json` `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
]), ]),
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`, `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map( ...extensionJarFilePaths.map(
jarFilePath => jarFilePath =>
`-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}` `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
), ),
...(keycloakMajorVersionNumber <= 20 ...(keycloakMajorVersionNumber <= 20
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`] ? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
@ -441,7 +426,7 @@ export async function command(params: {
})) }))
.map( .map(
({ localDirPath, containerDirPath }) => ({ localDirPath, containerDirPath }) =>
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw` `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
), ),
...buildContext.environmentVariables ...buildContext.environmentVariables
.map(({ name }) => ({ name, envValue: process.env[name] })) .map(({ name }) => ({ name, envValue: process.env[name] }))
@ -488,14 +473,7 @@ export async function command(params: {
{ shell: true } { shell: true }
); );
child.stdout.on("data", async data => { child.stdout.on("data", data => process.stdout.write(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)); child.stderr.on("data", data => process.stderr.write(data));
@ -542,9 +520,9 @@ export async function command(params: {
`${chalk.green("Your theme is accessible at:")}`, `${chalk.green("Your theme is accessible at:")}`,
`${chalk.green("➜")} ${chalk.cyan.bold( `${chalk.green("➜")} ${chalk.cyan.bold(
(() => { (() => {
const url = new URL(TEST_APP_URL); const url = new URL("https://my-theme.keycloakify.dev");
if (port !== 8080) { if (port !== DEFAULT_PORT) {
url.searchParams.set("port", `${port}`); url.searchParams.set("port", `${port}`);
} }
if (kcHttpRelativePath !== undefined) { if (kcHttpRelativePath !== undefined) {
@ -553,20 +531,13 @@ export async function command(params: {
kcHttpRelativePath kcHttpRelativePath
); );
} }
if (realmName !== "myrealm") {
url.searchParams.set("realm", realmName);
}
if (clientName !== "myclient") {
url.searchParams.set("client", clientName);
}
return url.href; return url.href;
})() })()
)}`, )}`,
"", "",
"You can login with the following credentials:", "You can login with the following credentials:",
`- username: ${chalk.cyan.bold(username)}`, `- username: ${chalk.cyan.bold("testuser")}`,
`- password: ${chalk.cyan.bold("password123")}`, `- password: ${chalk.cyan.bold("password123")}`,
"", "",
`Watching for changes in ${chalk.bold( `Watching for changes in ${chalk.bold(
@ -623,44 +594,6 @@ export async function command(params: {
} }
) )
.on("all", async (...[, filePath]) => { .on("all", async (...[, filePath]) => {
ignore_account_spa: {
const doImplementAccountSpa =
buildContext.implementedThemeTypes.account.isImplemented &&
buildContext.implementedThemeTypes.account.type === "Single-Page";
if (!doImplementAccountSpa) {
break ignore_account_spa;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "account"),
filePath
})
) {
break ignore_account_spa;
}
return;
}
ignore_admin: {
if (!buildContext.implementedThemeTypes.admin.isImplemented) {
break ignore_admin;
}
if (
!isInside({
dirPath: pathJoin(buildContext.themeSrcDirPath, "admin"),
filePath
})
) {
break ignore_admin;
}
return;
}
console.log(`Detected changes in ${filePath}`); console.log(`Detected changes in ${filePath}`);
await waitForDebounce(); await waitForDebounce();

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

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

@ -1,18 +1,16 @@
import { type FetchOptions } from "make-fetch-happen";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
export type FetchOptionsLike = { export type ProxyFetchOptions = Pick<
proxy: string | undefined; FetchOptions,
noProxy: string | string[]; "proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
strictSSL: boolean; >;
cert: string | string[] | undefined;
ca: string[] | undefined;
};
export function getProxyFetchOptions(params: { export function getProxyFetchOptions(params: {
npmConfigGetCwd: string; npmConfigGetCwd: string;
}): FetchOptionsLike { }): ProxyFetchOptions {
const { npmConfigGetCwd } = params; const { npmConfigGetCwd } = params;
const cfg = (() => { const cfg = (() => {

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

@ -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;
}

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

@ -1,38 +0,0 @@
import { sep as pathSep } from "path";
let cache: string | undefined = undefined;
export function getNodeModulesBinDirPath() {
if (cache !== undefined) {
return cache;
}
const binPath = process.argv[1];
const segments: string[] = [".bin"];
let foundNodeModules = false;
for (const segment of binPath.split(pathSep).reverse()) {
skip_segment: {
if (foundNodeModules) {
break skip_segment;
}
if (segment === "node_modules") {
foundNodeModules = true;
break skip_segment;
}
continue;
}
segments.unshift(segment);
}
const nodeModulesBinDirPath = segments.join(pathSep);
cache = nodeModulesBinDirPath;
return nodeModulesBinDirPath;
}

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

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

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

@ -1,24 +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 async function untrackFromGit(params: { filePath: string }): Promise<void> {
const { filePath } = params;
const dDone = new Deferred<void>();
child_process.exec(
`git rm --cached ${pathBasename(filePath)}`,
{ cwd: pathDirname(filePath) },
error => {
if (error !== null) {
dDone.reject(error);
return;
}
dDone.resolve();
}
);
await dDone.pr;
}

@ -1,161 +1,13 @@
import type { BuildContext } from "./shared/buildContext"; import type { CliCommandOptions } from "./main";
import * as fs from "fs/promises"; import { getBuildContext } from "./shared/buildContext";
import { join as pathJoin } from "path"; import { generateKcGenTs } from "./shared/generateKcGenTs";
import { existsAsync } from "./tools/fs.existsAsync";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
import * as crypto from "crypto";
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
export async function command(params: { buildContext: BuildContext }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { buildContext } = params; const { cliCommandOptions } = params;
run_copy_assets_to_public: { const buildContext = getBuildContext({
if (buildContext.bundler !== "webpack") { cliCommandOptions
break run_copy_assets_to_public;
}
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ buildContext });
}
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
}); });
if (hasBeenHandled) { await generateKcGenTs({ buildContext });
return;
}
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.tsx");
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
const hasAdminTheme = buildContext.implementedThemeTypes.admin.isImplemented;
let newContent = [
``,
`/* eslint-disable */`,
``,
`// @ts-nocheck`,
``,
`// noinspection JSUnusedGlobalSymbols`,
``,
`import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``,
`export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
Object.fromEntries(
buildContext.environmentVariables.map(
({ name, default: defaultValue }) => [name, defaultValue]
)
),
null,
2
)};`,
``,
`/**`,
` * NOTE: Do not import this type except maybe in your entrypoint. `,
` * If you need to import the KcContext import it either from src/login/KcContext.ts or src/account/KcContext.ts.`,
` * Depending on the theme type you are working on.`,
` */`,
`export type KcContext =`,
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
hasAdminTheme && ` | import("./admin/KcContext").KcContext`,
` ;`,
``,
`declare global {`,
` interface Window {`,
` kcContext?: KcContext;`,
` }`,
`}`,
``,
hasLoginTheme &&
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
hasAccountTheme &&
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
hasAdminTheme &&
`export const KcAdminPage = lazy(() => import("./admin/KcPage"));`,
``,
`export function KcPage(`,
` props: {`,
` kcContext: KcContext;`,
` fallback?: ReactNode;`,
` }`,
`) {`,
` const { kcContext, fallback } = props;`,
` return (`,
` <Suspense fallback={fallback}>`,
` {(() => {`,
` switch (kcContext.themeType) {`,
hasLoginTheme &&
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
hasAccountTheme &&
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
hasAdminTheme &&
` case "admin": return <KcAdminPage kcContext={kcContext} />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
`}`,
``
]
.filter(item => typeof item === "string")
.join("\n");
const hash = crypto.createHash("sha256").update(newContent).digest("hex");
skip_if_no_changes: {
if (!(await existsAsync(filePath))) {
break skip_if_no_changes;
}
const currentContent = (await fs.readFile(filePath)).toString("utf8");
if (!currentContent.includes(hash)) {
break skip_if_no_changes;
}
return;
}
newContent = [
`// This file is auto-generated by the \`update-kc-gen\` command. Do not edit it manually.`,
`// Hash: ${hash}`,
``,
newContent
].join("\n");
format: {
if (!(await getIsPrettierAvailable())) {
break format;
}
newContent = await runPrettier({
filePath,
sourceCode: newContent
});
}
await fs.writeFile(filePath, Buffer.from(newContent, "utf8"));
delete_legacy_file: {
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
break delete_legacy_file;
}
await fs.unlink(legacyFilePath);
}
} }

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

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

@ -33,7 +33,7 @@ export type KcContext =
| KcContext.LoginResetPassword | KcContext.LoginResetPassword
| KcContext.LoginVerifyEmail | KcContext.LoginVerifyEmail
| KcContext.Terms | KcContext.Terms
| KcContext.LoginOauth2DeviceVerifyUserCode | KcContext.LoginDeviceVerifyUserCode
| KcContext.LoginOauthGrant | KcContext.LoginOauthGrant
| KcContext.LoginOtp | KcContext.LoginOtp
| KcContext.LoginUsername | KcContext.LoginUsername
@ -94,7 +94,6 @@ export declare namespace KcContext {
languageTag: string; languageTag: string;
}[]; }[];
currentLanguageTag: string; currentLanguageTag: string;
rtl?: boolean;
}; };
auth?: { auth?: {
showUsername?: boolean; showUsername?: boolean;
@ -102,7 +101,7 @@ export declare namespace KcContext {
showTryAnotherWayLink?: boolean; showTryAnotherWayLink?: boolean;
attemptedUsername?: string; attemptedUsername?: string;
}; };
scripts?: string[]; scripts: string[];
message?: { message?: {
type: "success" | "warning" | "error" | "info"; type: "success" | "warning" | "error" | "info";
summary: string; summary: string;
@ -277,7 +276,7 @@ export declare namespace KcContext {
__localizationRealmOverridesTermsText?: string; __localizationRealmOverridesTermsText?: string;
}; };
export type LoginOauth2DeviceVerifyUserCode = Common & { export type LoginDeviceVerifyUserCode = Common & {
pageId: "login-oauth2-device-verify-user-code.ftl"; pageId: "login-oauth2-device-verify-user-code.ftl";
url: { url: {
oauth2DeviceVerificationAction: string; oauth2DeviceVerificationAction: string;

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

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

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

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

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

@ -1,11 +1,11 @@
import "keycloakify/tools/Object.fromEntries"; import "keycloakify/tools/Object.fromEntries";
import { assert, is } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { extractLastParenthesisContent } from "keycloakify/tools/extractLastParenthesisContent";
import messages_defaultSet_fallbackLanguage from "../messages_defaultSet/en"; import messages_defaultSet_fallbackLanguage from "../messages_defaultSet/en";
import { fetchMessages_defaultSet } from "../messages_defaultSet"; import { fetchMessages_defaultSet } from "../messages_defaultSet";
import type { KcContext } from "../../KcContext"; import type { KcContext } from "../../KcContext";
import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants"; import { FALLBACK_LANGUAGE_TAG } from "keycloakify/bin/shared/constants";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect"; import { Reflect } from "tsafe/Reflect";
import { import {
type LanguageTag as LanguageTag_defaultSet, type LanguageTag as LanguageTag_defaultSet,
@ -16,13 +16,9 @@ import type { GenericI18n_noJsx } from "./GenericI18n_noJsx";
export type KcContextLike = { export type KcContextLike = {
themeName: string; themeName: string;
realm: {
internationalizationEnabled: boolean;
};
locale?: { locale?: {
currentLanguageTag: string; currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[]; supported: { languageTag: string; url: string; label: string }[];
rtl?: boolean;
}; };
"x-keycloakify": { "x-keycloakify": {
messages: Record<string, string>; messages: Record<string, string>;
@ -94,56 +90,11 @@ export function createGetI18n<
return cachedResult; return cachedResult;
} }
const kcContextLocale = params.kcContext.realm.internationalizationEnabled ? params.kcContext.locale : undefined;
{ {
const currentLanguageTag = kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG; const currentLanguageTag = kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG;
const html = document.querySelector("html"); const html = document.querySelector("html");
assert(html !== null); assert(html !== null);
html.lang = currentLanguageTag; html.lang = currentLanguageTag;
const isRtl = (() => {
const { rtl } = kcContextLocale ?? {};
if (rtl !== undefined) {
return rtl;
}
return [
/* spell-checker: disable */
// Common RTL languages
"ar", // Arabic
"fa", // Persian (Farsi)
"he", // Hebrew
"ur", // Urdu
"ps", // Pashto
"syr", // Syriac
"dv", // Divehi (Maldivian)
"ku", // Kurdish (Sorani)
"ug", // Uighur
"az", // Azerbaijani (Arabic script)
"sd", // Sindhi
// Less common RTL languages
"yi", // Yiddish
"ha", // Hausa (when written in Arabic script)
"ks", // Kashmiri (written in the Perso-Arabic script)
"bal", // Balochi (when written in Arabic script)
"khw", // Khowar (Chitrali)
"brh", // Brahui (when written in Arabic script)
"tmh", // Tamashek (some dialects use Arabic script)
"bgn", // Western Balochi
"arc", // Aramaic
"sam", // Samaritan Aramaic
"prd", // Parsi-Dari (a dialect of Persian)
"huz", // Hazaragi (a dialect of Persian)
"gbz", // Zaza (written in Arabic script in some areas)
"urj" // Urdu in Romanized script (not always RTL, but to account for edge cases)
/* spell-checker: enable */
].includes(currentLanguageTag);
})();
html.dir = isRtl ? "rtl" : "ltr";
} }
const getLanguageLabel = (languageTag: LanguageTag) => { const getLanguageLabel = (languageTag: LanguageTag) => {
@ -159,20 +110,22 @@ export function createGetI18n<
} }
from_server: { from_server: {
if (kcContextLocale === undefined) { if (kcContext.locale === undefined) {
break from_server; break from_server;
} }
const supportedEntry = kcContextLocale.supported.find(entry => entry.languageTag === languageTag); const supportedEntry = kcContext.locale.supported.find(entry => entry.languageTag === languageTag);
if (supportedEntry === undefined) { if (supportedEntry === undefined) {
break from_server; break from_server;
} }
const lastParenthesisContent = extractLastParenthesisContent(supportedEntry.label); // cspell: disable-next-line
// from "Espagnol (Español)" we want to extract "Español"
const match = supportedEntry.label.match(/[^(]+\(([^)]+)\)/);
if (lastParenthesisContent !== undefined) { if (match !== null) {
return lastParenthesisContent; return match[1];
} }
return supportedEntry.label; return supportedEntry.label;
@ -183,7 +136,7 @@ export function createGetI18n<
}; };
const currentLanguage: I18n["currentLanguage"] = (() => { const currentLanguage: I18n["currentLanguage"] = (() => {
const languageTag = id<string>(kcContextLocale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag; const languageTag = id<string>(kcContext.locale?.currentLanguageTag ?? FALLBACK_LANGUAGE_TAG) as LanguageTag;
return { return {
languageTag, languageTag,
@ -194,8 +147,8 @@ export function createGetI18n<
const enabledLanguages: I18n["enabledLanguages"] = (() => { const enabledLanguages: I18n["enabledLanguages"] = (() => {
const enabledLanguages: I18n["enabledLanguages"] = []; const enabledLanguages: I18n["enabledLanguages"] = [];
if (kcContextLocale !== undefined) { if (kcContext.locale !== undefined) {
for (const entry of kcContextLocale.supported ?? []) { for (const entry of kcContext.locale.supported ?? []) {
const languageTag = id<string>(entry.languageTag) as LanguageTag; const languageTag = id<string>(entry.languageTag) as LanguageTag;
enabledLanguages.push({ enabledLanguages.push({

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

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

@ -1,4 +1,3 @@
import type { JSX } from "keycloakify/tools/JSX";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { kcSanitize } from "keycloakify/lib/kcSanitize"; import { kcSanitize } from "keycloakify/lib/kcSanitize";
import { createGetI18n, type KcContextLike } from "../noJsx/getI18n"; import { createGetI18n, type KcContextLike } from "../noJsx/getI18n";
@ -48,25 +47,11 @@ export function createUseI18n<
function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element { function renderHtmlString(params: { htmlString: string; msgKey: string }): JSX.Element {
const { htmlString, msgKey } = params; const { htmlString, msgKey } = params;
const htmlString_sanitized = kcSanitize(htmlString);
const Element = (() => {
if (htmlString_sanitized.includes("<") && htmlString_sanitized.includes(">")) {
for (const tagName of ["div", "section", "article", "ul", "ol"]) {
if (htmlString_sanitized.includes(`<${tagName}`)) {
return "div";
}
}
}
return "span";
})();
return ( return (
<Element <div
data-kc-msg={msgKey} data-kc-msg={msgKey}
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: htmlString_sanitized __html: kcSanitize(htmlString)
}} }}
/> />
); );
@ -98,7 +83,7 @@ export function createUseI18n<
})(); })();
add_style: { add_style: {
const attributeName = "data-kc-msg"; const attributeName = "data-kc-i18n";
// Check if already exists in head // Check if already exists in head
if (document.querySelector(`style[${attributeName}]`) !== null) { if (document.querySelector(`style[${attributeName}]`) !== null) {
@ -107,8 +92,7 @@ export function createUseI18n<
const styleElement = document.createElement("style"); const styleElement = document.createElement("style");
styleElement.attributes.setNamedItem(document.createAttribute(attributeName)); styleElement.attributes.setNamedItem(document.createAttribute(attributeName));
styleElement.textContent = `div[${attributeName}] { display: inline-block; }`; (styleElement.textContent = `[data-kc-msg] { display: inline-block; }`), document.head.prepend(styleElement);
document.head.prepend(styleElement);
} }
const { getI18n } = createGetI18n({ extraLanguageTranslations, messagesByLanguageTag_themeDefined }); const { getI18n } = createGetI18n({ extraLanguageTranslations, messagesByLanguageTag_themeDefined });

File diff suppressed because it is too large Load Diff

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

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