Compare commits

...

19 Commits

Author SHA1 Message Date
7d8ae040fd Bundle JAR WPI 2024-08-24 23:13:16 +02:00
c6cf564842 Release candidate 2024-08-23 19:01:56 +02:00
380b739017 Don't pin the patch version in the docker tag 2024-08-23 19:01:37 +02:00
c3f3c55303 Release candidate 2024-08-23 18:45:56 +02:00
2c01018529 #618 2024-08-23 18:36:40 +02:00
dd2edf3013 Merge pull request #616 from keycloakify/all-contributors/add-oliviergoulet5
docs: add oliviergoulet5 as a contributor for code
2024-08-22 00:56:15 +02:00
7f3cdf9fac Release candidate 2024-08-22 00:55:39 +02:00
f75a91fbc1 docs: update .all-contributorsrc [skip ci] 2024-08-21 22:55:00 +00:00
f151086bb1 docs: update README.md [skip ci] 2024-08-21 22:54:59 +00:00
7c833e6f10 Merge pull request #615 from oliviergoulet5/fix-array-operations
Fix array comparison and improve type check
2024-08-22 00:53:48 +02:00
885e8314e8 Fix array comparison and type check 2024-08-21 17:13:06 -04:00
3bdd955ab6 Release candidate 2024-08-19 02:11:31 +02:00
9499587bad Fix formating bug of Docker command being run 2024-08-19 02:10:59 +02:00
0879ddba7c Release candidate 2024-08-19 00:25:54 +02:00
106a1dd4c7 Support parsing of the KC_HTTP_RELATIVE_PATH option 2024-08-19 00:25:41 +02:00
5580248bcd Release candidate 2024-08-19 00:00:22 +02:00
c9c10b8fba Fix issue with the port in the start-keycloak command 2024-08-19 00:00:08 +02:00
ed254922e9 Relase candidate 2024-08-18 23:46:12 +02:00
4b7d1e2cec Fix bug in docker command 2024-08-18 23:45:58 +02:00
12 changed files with 280 additions and 80 deletions

View File

@ -240,6 +240,15 @@
"contributions": [
"code"
]
},
{
"login": "oliviergoulet5",
"name": "Olivier Goulet",
"avatar_url": "https://avatars.githubusercontent.com/u/17685861?v=4",
"profile": "https://github.com/oliviergoulet5",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@ -132,6 +132,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madmadson"><img src="https://avatars.githubusercontent.com/u/798831?v=4?s=100" width="100px;" alt="Tobias Matt"/><br /><sub><b>Tobias Matt</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=madmadson" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oliviergoulet5"><img src="https://avatars.githubusercontent.com/u/17685861?v=4?s=100" width="100px;" alt="Olivier Goulet"/><br /><sub><b>Olivier Goulet</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=oliviergoulet5" title="Code">💻</a></td>
</tr>
</tbody>
</table>

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "10.0.0-rc.140",
"version": "10.0.0-rc.147",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",

View File

@ -17,15 +17,20 @@ import { isInside } from "../../tools/isInside";
import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import {
bundleExtensionsIntoJar,
type BuildContextLike as BuildContextLike_bundleExtensionsIntoJar
} from "./bundleExtensionsIntoJar";
export type BuildContextLike = BuildContextLike_generatePom & {
keycloakifyBuildDirPath: string;
themeNames: string[];
artifactId: string;
themeVersion: string;
cacheDirPath: string;
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
export type BuildContextLike = BuildContextLike_generatePom &
BuildContextLike_bundleExtensionsIntoJar & {
keycloakifyBuildDirPath: string;
themeNames: string[];
artifactId: string;
themeVersion: string;
cacheDirPath: string;
implementedThemeTypes: BuildContext["implementedThemeTypes"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
@ -234,12 +239,19 @@ export async function buildJar(params: {
)
);
const jarFilePath_generatedByMaven = pathJoin(
keycloakifyBuildCacheDirPath,
"target",
`${buildContext.artifactId}-${buildContext.themeVersion}.jar`
);
await bundleExtensionsIntoJar({
buildContext,
jarFilePath: jarFilePath_generatedByMaven
});
await fs.rename(
pathJoin(
keycloakifyBuildCacheDirPath,
"target",
`${buildContext.artifactId}-${buildContext.themeVersion}.jar`
),
jarFilePath_generatedByMaven,
pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename)
);
}

View File

@ -0,0 +1,137 @@
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../../shared/buildContext";
import { transformCodebase } from "../../tools/transformCodebase";
import { join as pathJoin, basename as pathBasename, sep as pathSep } from "path";
import { rm } from "../../tools/fs.rm";
import { extractArchive } from "../../tools/extractArchive";
import * as crypto from "crypto";
export type BuildContextLike = {
cacheDirPath: string;
fetchOptions: BuildContext["fetchOptions"];
extensionJars: BuildContext["extensionJars"];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function bundleExtensionsIntoJar(params: {
jarFilePath: string;
buildContext: BuildContextLike;
}): Promise<void> {
const { jarFilePath, buildContext } = params;
if (buildContext.extensionJars.length === 0) {
return;
}
const mergeDirPath = pathJoin(
buildContext.cacheDirPath,
`merge_${pathBasename(jarFilePath).replace(/\.jar$/, "")}_${crypto
.createHash("sha256")
.update(jarFilePath)
.digest("hex")
.substring(0, 5)}`
);
await extractArchive({
archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) =>
writeFile({
filePath: pathJoin(mergeDirPath, relativeFilePathInArchive)
})
});
for (const extensionJar of buildContext.extensionJars) {
const transformSourceCode = (params: {
fileRelativePath: string;
sourceCode: Buffer;
}): { modifiedSourceCode: Buffer } | undefined => {
const { fileRelativePath } = params;
if (!fileRelativePath.startsWith(`META-INF${pathSep}`)) {
for (const ext of [".DSA", ".SF", ".RSA"]) {
if (fileRelativePath.endsWith(ext)) {
return undefined;
}
}
}
return undefined;
};
switch (extensionJar.type) {
case "path":
await extractArchive({
archiveFilePath: extensionJar.path,
onArchiveFile: async ({
relativeFilePathInArchive,
writeFile,
readFile
}) => {
const transformResult = transformSourceCode({
fileRelativePath: relativeFilePathInArchive,
sourceCode: await readFile()
});
if (transformResult === undefined) {
return;
}
await writeFile({
filePath: pathJoin(mergeDirPath, relativeFilePathInArchive),
modifiedData: transformResult.modifiedSourceCode
});
}
});
break;
case "url": {
const { extractedDirPath } = await downloadAndExtractArchive({
url: extensionJar.url,
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "noOp",
onArchiveFile: async ({ fileRelativePath, writeFile }) =>
writeFile({ fileRelativePath })
});
transformCodebase({
srcDirPath: extractedDirPath,
destDirPath: mergeDirPath,
transformSourceCode
});
break;
}
}
/*
transformCodebase({
srcDirPath: extractedDirPath,
destDirPath: mergeDirPath,
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (fileRelativePath === pathJoin("META-INF", "MANIFEST.MF")) {
const sourceCodeStr = sourceCode.toString("utf8");
const lines = sourceCodeStr.split(/\r?\n/);
console.log(lines);
return {
modifiedSourceCode: Buffer.concat([
sourceCode,
Buffer.from(
`Class-Path: ${pathBasename(userProvidedJarFilePathOrUrl)}\n`
)
])
};
}
}
});
*/
}
// TODO: Acctually build new jar
await rm(mergeDirPath, { recursive: true, force: true });
}

View File

@ -314,7 +314,7 @@ export async function generateResourcesForMainTheme(params: {
}
const { extractedDirPath } = await downloadAndExtractArchive({
urlOrPath: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",

View File

@ -26,6 +26,8 @@ import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { same } from "evt/tools/inDepth/same";
export type BuildContext = {
themeVersion: string;
@ -61,6 +63,7 @@ export type BuildContext = {
keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string;
}[];
extensionJars: ({ type: "path"; path: string } | { type: "url"; url: string })[];
startKeycloakOptions: {
dockerImage:
| {
@ -88,6 +91,7 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string;
extensionJars?: string[];
startKeycloakOptions?: {
dockerImage?: string;
dockerExtraArgs?: string[];
@ -360,6 +364,7 @@ export function getBuildContext(params: {
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(),
kcContextExclusionsFtl: z.string().optional(),
extensionJars: z.array(z.string()).optional(),
startKeycloakOptions: zStartKeycloakOptions.optional()
}),
zAccountThemeImplAndKeycloakVersionTargets
@ -520,6 +525,36 @@ export function getBuildContext(params: {
return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
})();
const buildForKeycloakMajorVersionNumber = (() => {
const envValue = process.env[BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME];
if (envValue === undefined) {
return undefined;
}
const major = parseInt(envValue);
assert(!isNaN(major));
return major;
})();
function urlOrPathToDiscriminatingWrapper(
urlOrPath: string
): { type: "url"; url: string } | { type: "path"; path: string } {
if (/^https?:\/\//.test(urlOrPath)) {
return { type: "url", url: urlOrPath };
}
return {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
cwd: projectDirPath
})
};
}
return {
bundler,
packageJsonFilePath,
@ -717,21 +752,6 @@ export function getBuildContext(params: {
`keycloak-theme-for-kc-${range}.jar`;
build_for_specific_keycloak_major_version: {
const buildForKeycloakMajorVersionNumber = (() => {
const envValue =
process.env[BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME];
if (envValue === undefined) {
return undefined;
}
const major = parseInt(envValue);
assert(!isNaN(major));
return major;
})();
if (buildForKeycloakMajorVersionNumber === undefined) {
break build_for_specific_keycloak_major_version;
}
@ -931,6 +951,10 @@ export function getBuildContext(params: {
return jarTargets;
})(),
extensionJars: (buildForKeycloakMajorVersionNumber !== undefined
? []
: buildOptions.extensionJars ?? []
).map(urlOrPath => urlOrPathToDiscriminatingWrapper(urlOrPath)),
startKeycloakOptions: {
dockerImage: (() => {
if (buildOptions.startKeycloakOptions?.dockerImage === undefined) {
@ -949,21 +973,14 @@ export function getBuildContext(params: {
})(),
dockerExtraArgs: buildOptions.startKeycloakOptions?.dockerExtraArgs ?? [],
keycloakExtraArgs: buildOptions.startKeycloakOptions?.keycloakExtraArgs ?? [],
extensionJars: (buildOptions.startKeycloakOptions?.extensionJars ?? []).map(
urlOrPath => {
if (/^https?:\/\//.test(urlOrPath)) {
return { type: "url", url: urlOrPath };
}
return {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
cwd: projectDirPath
})
};
}
),
extensionJars: [
...(buildForKeycloakMajorVersionNumber !== undefined
? buildOptions.extensionJars ?? []
: []),
...(buildOptions.startKeycloakOptions?.extensionJars ?? [])
]
.map(urlOrPath => urlOrPathToDiscriminatingWrapper(urlOrPath))
.reduce(...removeDuplicates<BuildContext["extensionJars"][number]>(same)),
realmJsonFilePath:
buildOptions.startKeycloakOptions?.realmJsonFilePath === undefined
? undefined

View File

@ -21,7 +21,7 @@ export async function downloadKeycloakDefaultTheme(params: {
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
const { extractedDirPath } = await downloadAndExtractArchive({
urlOrPath: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",

View File

@ -55,7 +55,7 @@ export async function promptKeycloakVersion(params: {
});
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
({ tag }) => tag
({ version }) => `${version.major}.${version.minor}`
);
const { value } = await cliSelect<string>({

View File

@ -200,7 +200,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const { archiveFilePath } = await downloadAndExtractArchive({
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
urlOrPath: extensionJar.url,
url: extensionJar.url,
uniqueIdOfOnArchiveFile: "no extraction",
onArchiveFile: async () => {}
});
@ -334,19 +334,30 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
});
} catch {}
const DEFAULT_PORT = 8080;
const port =
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
const dockerRunArgs: string[] = [
`-p${SPACE_PLACEHOLDER}${cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080}:8080`,
`-p${SPACE_PLACEHOLDER}${port}:8080`,
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
? []
: [
buildContext.startKeycloakOptions.dockerExtraArgs.join(
SPACE_PLACEHOLDER
)
]),
...(realmJsonFilePath === undefined
? []
: [
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
]),
`-v${SPACE_PLACEHOLDER}"./${pathRelative(process.cwd(), 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(
jarFilePath =>
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
@ -388,14 +399,19 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
({ name, envValue }) =>
`--env${SPACE_PLACEHOLDER}${name}='${envValue.replace(/'/g, "'\\''")}'`
),
...buildContext.startKeycloakOptions.dockerExtraArgs.join(SPACE_PLACEHOLDER),
`${buildContext.startKeycloakOptions.dockerImage?.reference ?? "quay.io/keycloak/keycloak"}:${dockerImageTag}`,
"start-dev",
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
? ["--features=declarative-user-profile"]
: []),
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]),
...buildContext.startKeycloakOptions.keycloakExtraArgs.join(SPACE_PLACEHOLDER)
...(buildContext.startKeycloakOptions.keycloakExtraArgs.length === 0
? []
: [
buildContext.startKeycloakOptions.keycloakExtraArgs.join(
SPACE_PLACEHOLDER
)
])
];
console.log(
@ -427,6 +443,18 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const srcDirPath = pathJoin(buildContext.projectDirPath, "src");
{
const kcHttpRelativePath = (() => {
const match = buildContext.startKeycloakOptions.dockerExtraArgs
.join(" ")
.match(/KC_HTTP_RELATIVE_PATH=([^ ]+)/);
if (match === null) {
return undefined;
}
return match[1];
})();
const handler = async (data: Buffer) => {
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
return;
@ -444,7 +472,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
)} are mounted in the Keycloak container.`,
"",
`Keycloak Admin console: ${chalk.cyan.bold(
`http://localhost:${cliCommandOptions.port}`
`http://localhost:${port}${kcHttpRelativePath ?? ""}`
)}`,
`- user: ${chalk.cyan.bold("admin")}`,
`- password: ${chalk.cyan.bold("admin")}`,
@ -452,7 +480,21 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
"",
`${chalk.green("Your theme is accessible at:")}`,
`${chalk.green("➜")} ${chalk.cyan.bold(
`https://my-theme.keycloakify.dev${cliCommandOptions.port === 8080 ? "" : `?port=${cliCommandOptions.port}`}`
(() => {
const url = new URL("https://my-theme.keycloakify.dev");
if (port !== DEFAULT_PORT) {
url.searchParams.set("port", `${port}`);
}
if (kcHttpRelativePath !== undefined) {
url.searchParams.set(
"kcHttpRelativePath",
kcHttpRelativePath
);
}
return url.href;
})()
)}`,
"",
"You can login with the following credentials:",

View File

@ -1,15 +1,14 @@
import fetch, { type FetchOptions } from "make-fetch-happen";
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { extractArchive } from "./extractArchive";
import { existsAsync } from "./fs.existsAsync";
import * as crypto from "crypto";
import { rm } from "./fs.rm";
import * as fsPr from "fs/promises";
export async function downloadAndExtractArchive(params: {
urlOrPath: string;
url: string;
uniqueIdOfOnArchiveFile: string;
onArchiveFile: (params: {
fileRelativePath: string;
@ -21,34 +20,17 @@ export async function downloadAndExtractArchive(params: {
}) => Promise<void>;
cacheDirPath: string;
fetchOptions: FetchOptions | undefined;
}): Promise<{ extractedDirPath: string; archiveFilePath: string; }> {
const {
urlOrPath,
uniqueIdOfOnArchiveFile,
onArchiveFile,
cacheDirPath,
fetchOptions
} = params;
}): Promise<{ extractedDirPath: string; archiveFilePath: string }> {
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
params;
const isUrl = /^https?:\/\//.test(urlOrPath);
const archiveFileBasename = isUrl
? urlOrPath.split("?")[0].split("/").reverse()[0]
: pathBasename(urlOrPath);
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
download: {
await mkdir(pathDirname(archiveFilePath), { recursive: true });
if (!isUrl) {
await fsPr.copyFile(urlOrPath, archiveFilePath);
break download;
}
const url = urlOrPath;
if (await existsAsync(archiveFilePath)) {
const isDownloaded = await SuccessTracker.getIsDownloaded({
cacheDirPath,

View File

@ -153,7 +153,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
function getPubKeyCredParams(signatureAlgorithmsList) {
let pubKeyCredParams = [];
if (signatureAlgorithmsList === []) {
if (signatureAlgorithmsList.length === 0) {
pubKeyCredParams.push({type: "public-key", alg: -7});
return pubKeyCredParams;
}
@ -184,7 +184,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
}
function getTransportsAsString(transportsList) {
if (transportsList === '' || transportsList.constructor !== Array) return "";
if (transportsList === '' || Array.isArray(transportsList)) return "";
let transportsString = "";