diff --git a/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts b/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts new file mode 100644 index 00000000..e36e7ae8 --- /dev/null +++ b/src/bin/start-keycloak/getQuayIoKeycloakDockerImageTags.ts @@ -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; + +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 = {}; + + 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; +} diff --git a/src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts b/src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts index ebb6354e..d92141c7 100644 --- a/src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts +++ b/src/bin/start-keycloak/realmConfig/ParsedRealmJson.ts @@ -3,11 +3,14 @@ import { assert, type Equals } from "tsafe/assert"; import { is } from "tsafe/is"; import { id } from "tsafe/id"; import * as fs from "fs"; -import { join as pathJoin } from "path"; -import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; export type ParsedRealmJson = { name: string; + loginTheme?: string; + accountTheme?: string; + adminTheme?: string; + emailTheme?: string; + eventsListeners: string[]; users: { id: string; email: string; @@ -52,6 +55,11 @@ export function readRealmJsonFile(params: { const zTargetType = z.object({ 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( z.object({ id: z.string(), @@ -105,19 +113,3 @@ export function readRealmJsonFile(params: { 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 }); -} diff --git a/src/bin/start-keycloak/realmConfig/defaultConfig/defaultConfig.ts b/src/bin/start-keycloak/realmConfig/defaultConfig/defaultConfig.ts new file mode 100644 index 00000000..220f4ac3 --- /dev/null +++ b/src/bin/start-keycloak/realmConfig/defaultConfig/defaultConfig.ts @@ -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 + }) + }); +} diff --git a/src/bin/start-keycloak/realmConfig/defaultConfig/index.ts b/src/bin/start-keycloak/realmConfig/defaultConfig/index.ts new file mode 100644 index 00000000..a067247f --- /dev/null +++ b/src/bin/start-keycloak/realmConfig/defaultConfig/index.ts @@ -0,0 +1 @@ +export * from "./defaultConfig"; diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-18.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-18.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-18.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-18.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-19.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-19.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-19.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-19.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-20.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-20.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-20.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-20.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-21.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-21.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-21.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-21.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-23.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-23.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-23.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-23.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-24.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-24.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-24.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-24.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-25.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-25.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-25.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-25.json diff --git a/src/bin/start-keycloak/realmConfig/realm-kc-26.json b/src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-26.json similarity index 100% rename from src/bin/start-keycloak/realmConfig/realm-kc-26.json rename to src/bin/start-keycloak/realmConfig/defaultConfig/realm-kc-26.json diff --git a/src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts b/src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts index b32d7879..81fb4443 100644 --- a/src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts +++ b/src/bin/start-keycloak/realmConfig/dumpContainerConfig.ts @@ -1,4 +1,3 @@ -import { runPrettier, getIsPrettierAvailable } from "../../tools/runPrettier"; import { CONTAINER_NAME } from "../../shared/constants"; import child_process from "child_process"; import { join as pathJoin } from "path"; @@ -6,7 +5,7 @@ import chalk from "chalk"; import { Deferred } from "evt/tools/Deferred"; import { assert, is } from "tsafe/assert"; import type { BuildContext } from "../../shared/buildContext"; -import * as fs from "fs/promises"; +import { type ParsedRealmJson, readRealmJsonFile } from "./ParsedRealmJson"; export type BuildContextLike = { cacheDirPath: string; @@ -17,15 +16,9 @@ assert(); export async function dumpContainerConfig(params: { realmName: string; keycloakMajorVersionNumber: number; - targetRealmConfigJsonFilePath: string; buildContext: BuildContextLike; -}) { - const { - realmName, - keycloakMajorVersionNumber, - targetRealmConfigJsonFilePath, - buildContext - } = params; +}): Promise { + const { realmName, keycloakMajorVersionNumber, buildContext } = params; { // https://github.com/keycloak/keycloak/issues/33800 @@ -148,20 +141,7 @@ export async function dumpContainerConfig(params: { await dCompleted.pr; } - let sourceCode = (await fs.readFile(targetRealmConfigJsonFilePath_tmp)).toString( - "utf8" - ); - - run_prettier: { - if (!(await getIsPrettierAvailable())) { - break run_prettier; - } - - sourceCode = await runPrettier({ - filePath: targetRealmConfigJsonFilePath, - sourceCode: sourceCode - }); - } - - await fs.writeFile(targetRealmConfigJsonFilePath, Buffer.from(sourceCode, "utf8")); + return readRealmJsonFile({ + realmJsonFilePath: targetRealmConfigJsonFilePath_tmp + }); } diff --git a/src/bin/start-keycloak/realmConfig/index.ts b/src/bin/start-keycloak/realmConfig/index.ts new file mode 100644 index 00000000..09a46a68 --- /dev/null +++ b/src/bin/start-keycloak/realmConfig/index.ts @@ -0,0 +1 @@ +export * from "./realmConfig"; diff --git a/src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts b/src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts index 37bda1ad..afe79572 100644 --- a/src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts +++ b/src/bin/start-keycloak/realmConfig/prepareRealmConfig.ts @@ -1,15 +1,26 @@ 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; export function prepareRealmConfig(params: { parsedRealmJson: ParsedRealmJson; keycloakMajorVersionNumber: number; + buildContext: BuildContextLike; }): { realmName: string; clientName: string; username: string; } { - const { parsedRealmJson, keycloakMajorVersionNumber } = params; + const { parsedRealmJson, keycloakMajorVersionNumber, buildContext } = params; const { username } = addOrEditTestUser({ parsedRealmJson, @@ -23,6 +34,22 @@ export function prepareRealmConfig(params: { 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 { realmName: parsedRealmJson.name, 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: { parsedRealmJson: ParsedRealmJson; keycloakMajorVersionNumber: number; diff --git a/src/bin/start-keycloak/realmConfig/realmConfig.ts b/src/bin/start-keycloak/realmConfig/realmConfig.ts new file mode 100644 index 00000000..b7793ec7 --- /dev/null +++ b/src/bin/start-keycloak/realmConfig/realmConfig.ts @@ -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; + +export async function getRealmConfig(params: { + keycloakMajorVersionNumber: number; + realmJsonFilePath_userProvided: string | undefined; + buildContext: BuildContextLike; +}): Promise<{ + realmJsonFilePath: string; + clientName: string; + realmName: string; + username: string; + onRealmConfigChange: () => Promise; +}> { + 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 + }; +} diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index a70ba002..7d2adcad 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -1,6 +1,5 @@ import type { BuildContext } from "../shared/buildContext"; import { exclude } from "tsafe/exclude"; -import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { CONTAINER_NAME, KEYCLOAKIFY_SPA_DEV_SERVER_PORT, @@ -13,8 +12,7 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep, - basename as pathBasename, - dirname as pathDirname + basename as pathBasename } from "path"; import * as child_process from "child_process"; import chalk from "chalk"; @@ -32,6 +30,9 @@ import { existsAsync } from "../tools/fs.existsAsync"; import { rm } from "../tools/fs.rm"; import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; import { startViteDevServer } from "./startViteDevServer"; +import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig"; +import { getKeycloakDockerImageLatestSemVerTagsForEveryMajors } from "./getQuayIoKeycloakDockerImageTags"; +import { getRealmConfig } from "./realmConfig"; export async function command(params: { buildContext: BuildContext; @@ -95,9 +96,32 @@ export async function command(params: { const { cliCommandOptions, buildContext } = params; + const availableTags = await getKeycloakDockerImageLatestSemVerTagsForEveryMajors({ + buildContext + }); + const { dockerImageTag } = await (async () => { 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) { @@ -112,50 +136,81 @@ export async function command(params: { "On which version of Keycloak do you want to test your theme?" ), chalk.gray( - "You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)" + "You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 26` (or any other version)" ) ].join("\n") ); - const { keycloakVersion } = await promptKeycloakVersion({ - startingFromMajor: 18, - excludeMajorVersions: [22], - doOmitPatch: true, - buildContext + const { value: tag } = await cliSelect({ + values: availableTags + }).catch(() => { + process.exit(-1); }); - console.log(`→ ${keycloakVersion}`); + console.log(`→ ${tag}`); - return { dockerImageTag: keycloakVersion }; + return { dockerImageTag: tag }; })(); const keycloakMajorVersionNumber = (() => { - if (buildContext.startKeycloakOptions.dockerImage === undefined) { - return SemVer.parse(dockerImageTag).major; - } - - const { tag } = buildContext.startKeycloakOptions.dockerImage; - - const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28] + const [wrap] = getSupportedKeycloakMajorVersions() .map(majorVersionNumber => ({ majorVersionNumber, - index: tag.indexOf(`${majorVersionNumber}`) + index: dockerImageTag.indexOf(`${majorVersionNumber}`) })) .filter(({ index }) => index !== -1) .sort((a, b) => a.index - b.index); if (wrap === undefined) { - console.warn( - chalk.yellow( - `Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25` - ) - ); - return 25; + try { + const version = SemVer.parse(dockerImageTag); + + console.error( + chalk.yellow( + `Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}` + ) + ); + + process.exit(1); + } catch { + 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; })(); + const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } = + await getRealmConfig({ + keycloakMajorVersionNumber, + realmJsonFilePath_userProvided: await (async () => { + if (cliCommandOptions.realmJsonFilePath !== undefined) { + return getAbsoluteAndInOsFormatPath({ + pathIsh: cliCommandOptions.realmJsonFilePath, + cwd: process.cwd() + }); + } + + if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) { + assert( + await existsAsync( + buildContext.startKeycloakOptions.realmJsonFilePath + ), + `${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist` + ); + return buildContext.startKeycloakOptions.realmJsonFilePath; + } + + return undefined; + })(), + buildContext + }); + { const { isAppBuildSuccess } = await appBuild({ buildContext @@ -193,156 +248,39 @@ export async function command(params: { assert(jarFilePath !== undefined); - const extensionJarFilePaths = await Promise.all( - buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => { - switch (extensionJar.type) { - case "path": { - assert( - await existsAsync(extensionJar.path), - `${extensionJar.path} does not exist` - ); - return extensionJar.path; + const extensionJarFilePaths = [ + pathJoin( + getThisCodebaseRootDirPath(), + "src", + "bin", + "start-keycloak", + KEYCLOAKIFY_LOGIN_JAR_BASENAME + ), + ...(await Promise.all( + buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => { + switch (extensionJar.type) { + case "path": { + assert( + await existsAsync(extensionJar.path), + `${extensionJar.path} does not exist` + ); + return extensionJar.path; + } + case "url": { + const { archiveFilePath } = await downloadAndExtractArchive({ + cacheDirPath: buildContext.cacheDirPath, + fetchOptions: buildContext.fetchOptions, + url: extensionJar.url, + uniqueIdOfOnArchiveFile: "no extraction", + onArchiveFile: async () => {} + }); + return archiveFilePath; + } } - case "url": { - const { archiveFilePath } = await downloadAndExtractArchive({ - cacheDirPath: buildContext.cacheDirPath, - fetchOptions: buildContext.fetchOptions, - url: extensionJar.url, - uniqueIdOfOnArchiveFile: "no extraction", - onArchiveFile: async () => {} - }); - return archiveFilePath; - } - } - assert>(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({ - 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 = JSON.parse( - fs.readFileSync(realmJsonFilePath).toString("utf8") - ); - - if (realm.users !== undefined) { - break add_test_user_if_missing; - } - - const realmJsonFilePath_internal = (() => { - const filePath = getRealmJsonFilePath_defaultForKeycloakMajor( - keycloakMajorVersionNumber - ); - - if (!fs.existsSync(filePath)) { - return getRealmJsonFilePath_defaultForKeycloakMajor(25); - } - - return filePath; - })(); - - const users = JSON.parse( - fs.readFileSync(realmJsonFilePath_internal).toString("utf8") - ).users; - - realm.users = users; - - fs.writeFileSync(realmJsonFilePath, JSON.stringify(realm, null, 2), "utf8"); - } + assert>(false); + }) + )) + ]; async function extractThemeResourcesFromJar() { await extractArchive({ @@ -382,9 +320,7 @@ export async function command(params: { }); } catch {} - const DEFAULT_PORT = 8080; - const port = - cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT; + const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080; const doStartDevServer = (() => { const hasSpaUi = @@ -457,7 +393,7 @@ export async function command(params: { ...(realmJsonFilePath === undefined ? [] : [ - `-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json` + `-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/${realmName}-realm.json` ]), `-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`, ...extensionJarFilePaths.map( @@ -532,7 +468,14 @@ export async function command(params: { { shell: true } ); - child.stdout.on("data", data => process.stdout.write(data)); + child.stdout.on("data", async data => { + if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) { + await onRealmConfigChange(); + return; + } + + process.stdout.write(data); + }); child.stderr.on("data", data => process.stderr.write(data)); @@ -581,7 +524,7 @@ export async function command(params: { (() => { const url = new URL("https://my-theme.keycloakify.dev"); - if (port !== DEFAULT_PORT) { + if (port !== 8080) { url.searchParams.set("port", `${port}`); } if (kcHttpRelativePath !== undefined) { @@ -590,13 +533,20 @@ export async function command(params: { kcHttpRelativePath ); } + if (realmName !== "myrealm") { + url.searchParams.set("realm", realmName); + } + + if (clientName !== "myclient") { + url.searchParams.set("client", clientName); + } return url.href; })() )}`, "", "You can login with the following credentials:", - `- username: ${chalk.cyan.bold("testuser")}`, + `- username: ${chalk.cyan.bold(username)}`, `- password: ${chalk.cyan.bold("password123")}`, "", `Watching for changes in ${chalk.bold(