Keycloak config persistance implemented (to test)

This commit is contained in:
Joseph Garrone 2024-12-14 14:36:11 +01:00
parent 8d59fe7b67
commit 9185740d35
17 changed files with 465 additions and 228 deletions

View File

@ -0,0 +1,89 @@
import fetch from "make-fetch-happen";
import type { BuildContext } from "../shared/buildContext";
import { assert } from "tsafe/assert";
import { z } from "zod";
import { SemVer } from "../tools/SemVer";
import { exclude } from "tsafe/exclude";
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
export type BuildContextLike = {
fetchOptions: BuildContext["fetchOptions"];
};
assert<BuildContext extends BuildContextLike ? true : false>;
let cache: string[] | undefined = undefined;
export async function getKeycloakDockerImageLatestSemVerTagsForEveryMajors(params: {
buildContext: BuildContextLike;
}) {
if (cache !== undefined) {
return cache;
}
const { buildContext } = params;
const { tags } = await fetch(
"https://quay.io/v2/keycloak/keycloak/tags/list",
buildContext.fetchOptions
)
.then(r => r.json())
.then(j =>
z
.object({
tags: z.array(z.string())
})
.parse(j)
);
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();
cache = Object.values(versionByMajor)
.map(version => {
assert(version !== undefined);
if (!supportedKeycloakMajorVersions.includes(version.major)) {
return undefined;
}
return SemVer.stringify(version);
})
.filter(exclude(undefined));
return cache;
}

View File

@ -3,11 +3,14 @@ import { assert, type Equals } from "tsafe/assert";
import { is } from "tsafe/is"; import { is } from "tsafe/is";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
export type ParsedRealmJson = { export type ParsedRealmJson = {
name: string; name: string;
loginTheme?: string;
accountTheme?: string;
adminTheme?: string;
emailTheme?: string;
eventsListeners: string[];
users: { users: {
id: string; id: string;
email: string; email: string;
@ -52,6 +55,11 @@ export function readRealmJsonFile(params: {
const zTargetType = z.object({ const zTargetType = z.object({
name: z.string(), name: 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( users: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),
@ -105,19 +113,3 @@ export function readRealmJsonFile(params: {
return parsedRealmJson; return parsedRealmJson;
} }
export function getDefaultConfig(params: {
keycloakMajorVersionNumber: number;
}): ParsedRealmJson {
const { keycloakMajorVersionNumber } = params;
const realmJsonFilePath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
);
return readRealmJsonFile({ realmJsonFilePath });
}

View File

@ -0,0 +1,74 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import { getThisCodebaseRootDirPath } from "../../../tools/getThisCodebaseRootDirPath";
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import { 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));
return cache;
}
return { getSupportedKeycloakMajorVersions };
})();
export function getDefaultConfig(params: {
keycloakMajorVersionNumber: number;
}): ParsedRealmJson {
const { keycloakMajorVersionNumber } = params;
assert(
getSupportedKeycloakMajorVersions().includes(keycloakMajorVersionNumber),
`We do not have a default config for Keycloak ${keycloakMajorVersionNumber}`
);
return readRealmJsonFile({
realmJsonFilePath: getDefaultRealmJsonFilePath({
keycloakMajorVersionNumber
})
});
}

View File

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

View File

@ -1,4 +1,3 @@
import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier";
import { CONTAINER_NAME } from "../../shared/constants"; import { CONTAINER_NAME } from "../../shared/constants";
import child_process from "child_process"; import child_process from "child_process";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
@ -6,7 +5,7 @@ import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert, is } from "tsafe/assert"; import { assert, is } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import * as fs from "fs/promises"; import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
@ -17,15 +16,9 @@ assert<BuildContext extends BuildContextLike ? true : false>();
export async function dumpContainerConfig(params: { export async function dumpContainerConfig(params: {
realmName: string; realmName: string;
keycloakMajorVersionNumber: number; keycloakMajorVersionNumber: number;
targetRealmConfigJsonFilePath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}) { }): Promise<ParsedRealmJson> {
const { const { realmName, keycloakMajorVersionNumber, buildContext } = params;
realmName,
keycloakMajorVersionNumber,
targetRealmConfigJsonFilePath,
buildContext
} = params;
{ {
// https://github.com/keycloak/keycloak/issues/33800 // https://github.com/keycloak/keycloak/issues/33800
@ -148,20 +141,7 @@ export async function dumpContainerConfig(params: {
await dCompleted.pr; await dCompleted.pr;
} }
let sourceCode = (await fs.readFile(targetRealmConfigJsonFilePath_tmp)).toString( return readRealmJsonFile({
"utf8" realmJsonFilePath: targetRealmConfigJsonFilePath_tmp
);
run_prettier: {
if (!(await getIsPrettierAvailable())) {
break run_prettier;
}
sourceCode = await runPrettier({
filePath: targetRealmConfigJsonFilePath,
sourceCode: sourceCode
}); });
}
await fs.writeFile(targetRealmConfigJsonFilePath, Buffer.from(sourceCode, "utf8"));
} }

View File

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

View File

@ -1,15 +1,26 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { getDefaultConfig, type ParsedRealmJson } from "./ParsedRealmJson"; import type { ParsedRealmJson } from "./ParsedRealmJson";
import { getDefaultConfig } from "./defaultConfig";
import type { BuildContext } from "../../shared/buildContext";
import { objectKeys } from "tsafe/objectKeys";
export type BuildContextLike = {
themeNames: BuildContext["themeNames"];
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>;
export function prepareRealmConfig(params: { export function prepareRealmConfig(params: {
parsedRealmJson: ParsedRealmJson; parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number; keycloakMajorVersionNumber: number;
buildContext: BuildContextLike;
}): { }): {
realmName: string; realmName: string;
clientName: string; clientName: string;
username: string; username: string;
} { } {
const { parsedRealmJson, keycloakMajorVersionNumber } = params; const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params;
const { username } = addOrEditTestUser({ const { username } = addOrEditTestUser({
parsedRealmJson, parsedRealmJson,
@ -23,6 +34,22 @@ export function prepareRealmConfig(params: {
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson }); 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);
}
return { return {
realmName: parsedRealmJson.name, realmName: parsedRealmJson.name,
clientName: clientId, clientName: clientId,
@ -30,6 +57,21 @@ export function prepareRealmConfig(params: {
}; };
} }
function enableCustomThemes(params: {
parsedRealmJson: ParsedRealmJson;
themeName: string;
implementedThemeTypes: BuildContextLike["implementedThemeTypes"];
}) {
const { parsedRealmJson, themeName, implementedThemeTypes } = params;
for (const themeType of objectKeys(implementedThemeTypes)) {
parsedRealmJson[`${themeType}Theme` as const] = implementedThemeTypes[themeType]
.isImplemented
? themeName
: "";
}
}
function addOrEditTestUser(params: { function addOrEditTestUser(params: {
parsedRealmJson: ParsedRealmJson; parsedRealmJson: ParsedRealmJson;
keycloakMajorVersionNumber: number; keycloakMajorVersionNumber: number;

View File

@ -0,0 +1,108 @@
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 } from "path";
import { existsAsync } from "../../tools/fs.existsAsync";
import { readRealmJsonFile, type ParsedRealmJson } from "./ParsedRealmJson";
import {
dumpContainerConfig,
type BuildContextLike as BuildContextLike_dumpContainerConfig
} from "./dumpContainerConfig";
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,
`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 });
async function onRealmConfigChange() {
const parsedRealmJson = await dumpContainerConfig({
buildContext,
realmName,
keycloakMajorVersionNumber
});
await writeRealmJsonFile({ parsedRealmJson });
}
return {
realmJsonFilePath,
clientName,
realmName,
username,
onRealmConfigChange
};
}

View File

@ -1,6 +1,5 @@
import type { BuildContext } from "../shared/buildContext"; import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { import {
CONTAINER_NAME, CONTAINER_NAME,
KEYCLOAKIFY_SPA_DEV_SERVER_PORT, KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
@ -13,8 +12,7 @@ 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";
@ -32,6 +30,9 @@ 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 { startViteDevServer } from "./startViteDevServer";
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
import { getKeycloakDockerImageLatestSemVerTagsForEveryMajors } from "./getQuayIoKeycloakDockerImageTags";
import { getRealmConfig } from "./realmConfig";
export async function command(params: { export async function command(params: {
buildContext: BuildContext; buildContext: BuildContext;
@ -95,9 +96,32 @@ export async function command(params: {
const { cliCommandOptions, buildContext } = params; const { cliCommandOptions, buildContext } = params;
const availableTags = await getKeycloakDockerImageLatestSemVerTagsForEveryMajors({
buildContext
});
const { dockerImageTag } = await (async () => { const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) { if (cliCommandOptions.keycloakVersion !== undefined) {
return { dockerImageTag: cliCommandOptions.keycloakVersion }; const cliCommandOptions_keycloakVersion = 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) {
@ -112,50 +136,81 @@ 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 25.0.2` (or any other version)" "You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)"
) )
].join("\n") ].join("\n")
); );
const { keycloakVersion } = await promptKeycloakVersion({ const { value: tag } = await cliSelect<string>({
startingFromMajor: 18, values: availableTags
excludeMajorVersions: [22], }).catch(() => {
doOmitPatch: true, process.exit(-1);
buildContext
}); });
console.log(`${keycloakVersion}`); console.log(`${tag}`);
return { dockerImageTag: keycloakVersion }; return { dockerImageTag: tag };
})(); })();
const keycloakMajorVersionNumber = (() => { const keycloakMajorVersionNumber = (() => {
if (buildContext.startKeycloakOptions.dockerImage === undefined) { const [wrap] = getSupportedKeycloakMajorVersions()
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: tag.indexOf(`${majorVersionNumber}`) index: dockerImageTag.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) {
console.warn( try {
const version = SemVer.parse(dockerImageTag);
console.error(
chalk.yellow( chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25` `Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
) )
); );
return 25;
process.exit(1);
} catch {
console.warn(
chalk.yellow(
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming 26`
)
);
return 26;
}
} }
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
@ -193,7 +248,15 @@ export async function command(params: {
assert(jarFilePath !== undefined); assert(jarFilePath !== undefined);
const extensionJarFilePaths = await Promise.all( const extensionJarFilePaths = [
pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak",
KEYCLOAKIFY_LOGIN_JAR_BASENAME
),
...(await Promise.all(
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => { buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
switch (extensionJar.type) { switch (extensionJar.type) {
case "path": { case "path": {
@ -216,133 +279,8 @@ export async function command(params: {
} }
assert<Equals<typeof extensionJar, never>>(false); assert<Equals<typeof extensionJar, never>>(false);
}) })
); ))
];
const thisDirPath = pathJoin(
getThisCodebaseRootDirPath(),
"src",
"bin",
"start-keycloak"
);
extensionJarFilePaths.unshift(pathJoin(thisDirPath, KEYCLOAKIFY_LOGIN_JAR_BASENAME));
const getRealmJsonFilePath_defaultForKeycloakMajor = (
keycloakMajorVersionNumber: number
) => pathJoin(thisDirPath, `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({
@ -382,9 +320,7 @@ export async function command(params: {
}); });
} catch {} } catch {}
const DEFAULT_PORT = 8080; const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
const port =
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
const doStartDevServer = (() => { const doStartDevServer = (() => {
const hasSpaUi = const hasSpaUi =
@ -457,7 +393,7 @@ export async function command(params: {
...(realmJsonFilePath === undefined ...(realmJsonFilePath === undefined
? [] ? []
: [ : [
`-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json` `-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json`
]), ]),
`-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`, `-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map( ...extensionJarFilePaths.map(
@ -532,7 +468,14 @@ export async function command(params: {
{ shell: true } { shell: true }
); );
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", async data => {
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
await onRealmConfigChange();
return;
}
process.stdout.write(data);
});
child.stderr.on("data", data => process.stderr.write(data)); child.stderr.on("data", data => process.stderr.write(data));
@ -581,7 +524,7 @@ export async function command(params: {
(() => { (() => {
const url = new URL("https://my-theme.keycloakify.dev"); const url = new URL("https://my-theme.keycloakify.dev");
if (port !== DEFAULT_PORT) { if (port !== 8080) {
url.searchParams.set("port", `${port}`); url.searchParams.set("port", `${port}`);
} }
if (kcHttpRelativePath !== undefined) { if (kcHttpRelativePath !== undefined) {
@ -590,13 +533,20 @@ 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("testuser")}`, `- username: ${chalk.cyan.bold(username)}`,
`- password: ${chalk.cyan.bold("password123")}`, `- password: ${chalk.cyan.bold("password123")}`,
"", "",
`Watching for changes in ${chalk.bold( `Watching for changes in ${chalk.bold(