Compare commits
1 Commits
main
...
angular_in
Author | SHA1 | Date | |
---|---|---|---|
|
daf95b3dbb |
@ -290,79 +290,6 @@
|
||||
"code",
|
||||
"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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "EternalSide",
|
||||
"name": "Lesha",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
|
||||
"profile": "http://t.me/AAT_L",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "bacongobbler",
|
||||
"name": "Matthew Fisher",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
|
||||
"profile": "https://blog.bacongobbler.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kodebach",
|
||||
"name": "Klemens Böswirth",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/23529132?v=4",
|
||||
"profile": "https://github.com/kodebach",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wnmzzzz",
|
||||
"name": "wnmzzzz",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/117174301?v=4",
|
||||
"profile": "https://github.com/wnmzzzz",
|
||||
"contributions": [
|
||||
"test"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
1
.github/FUNDING.yaml
vendored
1
.github/FUNDING.yaml
vendored
@ -1,3 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [garronej]
|
||||
custom: ['https://www.ringerhq.com/experts/garronej']
|
||||
|
@ -12,5 +12,4 @@ node_modules/
|
||||
/sample_react_project/
|
||||
/sample_custom_react_project/
|
||||
/keycloakify_starter_test/
|
||||
/.storybook/static/keycloak-resources/
|
||||
/src/bin/start-keycloak/*.json
|
||||
/.storybook/static/keycloak-resources/
|
47
README.md
47
README.md
@ -6,7 +6,7 @@
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/garronej/keycloakify/actions">
|
||||
<img src="https://github.com/keycloakify/keycloakify/actions/workflows/ci.yaml/badge.svg">
|
||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/keycloakify">
|
||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||
@ -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 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/features/compiler-options/keycloakversiontargets).
|
||||
|
||||
## Sponsors
|
||||
|
||||
Project backers, we trust and recommend their services.
|
||||
Friends for the project, we trust and recommend their services.
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||
|
||||
</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">
|
||||
|
||||

|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||

|
||||

|
||||
|
||||
</div>
|
||||
|
||||
@ -109,7 +82,7 @@ Project backers, we trust and recommend their services.
|
||||
</div>
|
||||
|
||||
<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/>
|
||||
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
||||
</p>
|
||||
@ -163,16 +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://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/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>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kodebach"><img src="https://avatars.githubusercontent.com/u/23529132?v=4?s=100" width="100px;" alt="Klemens Böswirth"/><br /><sub><b>Klemens Böswirth</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kodebach" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/wnmzzzz"><img src="https://avatars.githubusercontent.com/u/117174301?v=4?s=100" width="100px;" alt="wnmzzzz"/><br /><sub><b>wnmzzzz</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=wnmzzzz" title="Tests">⚠️</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
15
package.json
15
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "11.8.23",
|
||||
"version": "11.2.10",
|
||||
"description": "Framework to create custom Keycloak UIs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -38,14 +38,12 @@
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/",
|
||||
"dist/bin/**/*.d.ts",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/constants.js.map",
|
||||
"dist/bin/shared/customHandler.js",
|
||||
"dist/bin/shared/customHandler.js.map",
|
||||
"dist/bin/shared/*.d.ts",
|
||||
"dist/bin/shared/*.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
@ -64,7 +62,7 @@
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"dependencies": {
|
||||
"tsafe": "^1.8.5"
|
||||
"tsafe": "^1.7.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.5",
|
||||
@ -73,6 +71,7 @@
|
||||
"@babel/preset-env": "7.24.8",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@keycloakify/angular": "^0.0.1-rc.19",
|
||||
"@octokit/rest": "^20.1.1",
|
||||
"@storybook/addon-a11y": "^6.5.16",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
@ -95,14 +94,14 @@
|
||||
"cli-select": "^1.1.2",
|
||||
"dompurify": "^3.1.6",
|
||||
"eslint-plugin-storybook": "^0.6.7",
|
||||
"evt": "^2.5.8",
|
||||
"evt": "^2.5.7",
|
||||
"html-entities": "^2.5.2",
|
||||
"husky": "^4.3.8",
|
||||
"isomorphic-dompurify": "^2.15.0",
|
||||
"lint-staged": "^11.0.0",
|
||||
"magic-string": "^0.30.7",
|
||||
"make-fetch-happen": "^11.0.3",
|
||||
"powerhooks": "^1.0.19",
|
||||
"powerhooks": "^1.0.10",
|
||||
"prettier": "^3.2.5",
|
||||
"properties-parser": "^0.3.1",
|
||||
"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 { run } from "../shared/run";
|
||||
import { vendorFrontendDependencies } from "./vendorFrontendDependencies";
|
||||
import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||
|
||||
(async () => {
|
||||
console.log(chalk.cyan("Building Keycloakify..."));
|
||||
@ -41,9 +40,7 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||
);
|
||||
}
|
||||
|
||||
run(
|
||||
`npx ncc build ${join("dist", "bin", "main.js")} --external prettier -o ${join("dist", "ncc_out")}`
|
||||
);
|
||||
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
@ -116,7 +113,7 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||
}
|
||||
|
||||
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",
|
||||
"ncc_out"
|
||||
)}`
|
||||
@ -149,6 +146,9 @@ import { downloadKeycloakifyLogging } from "./downloadKeycloakifyLogging";
|
||||
fs.cpSync(dirBasename, destDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
await createPublicKeycloakifyDevResourcesDir();
|
||||
await createAccountV1Dir();
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("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(
|
||||
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)
|
||||
);
|
||||
|
@ -1,5 +1,10 @@
|
||||
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 { run } from "../shared/run";
|
||||
import { cacheDirPath as cacheDirPath_base } from "../shared/cacheDirPath";
|
||||
@ -36,12 +41,13 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
webpackConfigJsFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`const path = require('path');`,
|
||||
``,
|
||||
`module.exports = {`,
|
||||
` mode: 'production',`,
|
||||
` entry: Buffer.from("${Buffer.from(filePath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` entry: '${filePath}',`,
|
||||
` output: {`,
|
||||
` path: Buffer.from("${Buffer.from(webpackOutputDirPath, "utf8").toString("base64")}", "base64").toString("utf8"),`,
|
||||
` path: '${webpackOutputDirPath}',`,
|
||||
` filename: '${pathBasename(webpackOutputFilePath)}',`,
|
||||
` libraryTarget: 'module',`,
|
||||
` },`,
|
||||
@ -67,9 +73,7 @@ export function vendorFrontendDependencies(params: { distDirPath: string }) {
|
||||
)
|
||||
);
|
||||
|
||||
run(`npx webpack --config ${pathBasename(webpackConfigJsFilePath)}`, {
|
||||
cwd: pathDirname(webpackConfigJsFilePath)
|
||||
});
|
||||
run(`npx webpack --config ${webpackConfigJsFilePath}`);
|
||||
|
||||
fs.readdirSync(webpackOutputDirPath)
|
||||
.filter(fileBasename => !fileBasename.endsWith(".txt"))
|
||||
|
@ -1,14 +1,66 @@
|
||||
import { CONTAINER_NAME } from "../src/bin/shared/constants";
|
||||
import child_process from "child_process";
|
||||
import { SemVer } from "../src/bin/tools/SemVer";
|
||||
import { dumpContainerConfig } from "../src/bin/start-keycloak/realmConfig/dumpContainerConfig";
|
||||
import { cacheDirPath } from "./shared/cacheDirPath";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { writeRealmJsonFile } from "../src/bin/start-keycloak/realmConfig/ParsedRealmJson";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
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 () => {
|
||||
{
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn(
|
||||
"docker",
|
||||
[
|
||||
...["exec", CONTAINER_NAME],
|
||||
...["/opt/keycloak/bin/kc.sh", "export"],
|
||||
...["--dir", "/tmp"],
|
||||
...["--realm", "myrealm"],
|
||||
...["--users", "realm_file"]
|
||||
],
|
||||
{ shell: true }
|
||||
);
|
||||
|
||||
let output = "";
|
||||
|
||||
const onExit = (code: number | null) => {
|
||||
dCompleted.reject(new Error(`Exited with code ${code}`));
|
||||
};
|
||||
|
||||
child.on("exit", onExit);
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
const outputStr = data.toString("utf8");
|
||||
|
||||
if (outputStr.includes("Export finished successfully")) {
|
||||
child.removeListener("exit", onExit);
|
||||
|
||||
child.kill();
|
||||
|
||||
dCompleted.resolve();
|
||||
}
|
||||
|
||||
output += outputStr;
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => (output += chalk.red(data.toString("utf8"))));
|
||||
|
||||
try {
|
||||
await dCompleted.pr;
|
||||
} catch (error) {
|
||||
assert(is<Error>(error));
|
||||
|
||||
console.log(chalk.red(error.message));
|
||||
|
||||
console.log(output);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const keycloakMajorVersionNumber = SemVer.parse(
|
||||
child_process
|
||||
.execSync(`docker inspect --format '{{.Config.Image}}' ${CONTAINER_NAME}`)
|
||||
@ -17,29 +69,19 @@ import chalk from "chalk";
|
||||
.split(":")[1]
|
||||
).major;
|
||||
|
||||
const parsedRealmJson = await dumpContainerConfig({
|
||||
buildContext: {
|
||||
cacheDirPath
|
||||
},
|
||||
keycloakMajorVersionNumber,
|
||||
realmName: "myrealm"
|
||||
});
|
||||
|
||||
const realmJsonFilePath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
"realmConfig",
|
||||
"defaultConfig",
|
||||
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||
const targetFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(
|
||||
__dirname,
|
||||
"..",
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
`myrealm-realm-${keycloakMajorVersionNumber}.json`
|
||||
)
|
||||
);
|
||||
|
||||
await writeRealmJsonFile({
|
||||
parsedRealmJson,
|
||||
realmJsonFilePath,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
run(`docker cp ${CONTAINER_NAME}:/tmp/myrealm-realm.json ${targetFilePath}`);
|
||||
|
||||
console.log(chalk.green(`Realm config dumped to ${realmJsonFilePath}`));
|
||||
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
|
||||
})();
|
||||
|
@ -37,7 +37,7 @@ async function generateI18nMessages() {
|
||||
|
||||
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({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
|
@ -45,10 +45,7 @@ const commonThirdPartyDeps = [
|
||||
.replace(/"!\.\/dist\//g, '"!./');
|
||||
|
||||
modifiedPackageJsonContent = JSON.stringify(
|
||||
{
|
||||
...JSON.parse(modifiedPackageJsonContent),
|
||||
version: `0.0.0-rc.${~~(Math.random() * 1000000)}`
|
||||
},
|
||||
{ ...JSON.parse(modifiedPackageJsonContent), version: "0.0.0" },
|
||||
null,
|
||||
4
|
||||
);
|
||||
@ -58,6 +55,7 @@ const commonThirdPartyDeps = [
|
||||
Buffer.from(modifiedPackageJsonContent, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||
|
||||
fs.rmSync(yarnGlobalDirPath, { recursive: true, force: true });
|
||||
@ -66,21 +64,6 @@ fs.mkdirSync(yarnGlobalDirPath);
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
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 = [
|
||||
"yarn",
|
||||
"link",
|
||||
@ -94,10 +77,7 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
env: {
|
||||
...process.env,
|
||||
...(os.platform() === "win32"
|
||||
? {
|
||||
USERPROFILE: yarnGlobalDirPath,
|
||||
LOCALAPPDATA: yarnGlobalDirPath
|
||||
}
|
||||
? { USERPROFILE: yarnGlobalDirPath }
|
||||
: { HOME: yarnGlobalDirPath })
|
||||
}
|
||||
});
|
||||
@ -128,54 +108,7 @@ if (testAppPaths.length === 0) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
testAppPaths.forEach(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();
|
||||
});
|
||||
testAppPaths.forEach(testAppPath => execSync("yarn install", { cwd: testAppPath }));
|
||||
|
||||
console.log("=== Linking common dependencies ===");
|
||||
|
||||
@ -222,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 {};
|
||||
|
@ -1,88 +1,49 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, sep as pathSep } 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 { join } from "path";
|
||||
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 () => {
|
||||
const starterNames = fs
|
||||
.readdirSync(parentDirPath)
|
||||
.filter(
|
||||
basename =>
|
||||
basename.includes("starter") &&
|
||||
basename.includes("keycloakify") &&
|
||||
fs.statSync(pathJoin(parentDirPath, basename)).isDirectory()
|
||||
);
|
||||
try {
|
||||
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||
} catch {
|
||||
// NOTE: This is a workaround for windows
|
||||
// we can't remove locked executables.
|
||||
|
||||
if (starterNames.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`No starter found. Keycloakify Angular starter found in ${parentDirPath}`
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const starterName = await (async () => {
|
||||
if (starterNames.length === 1) {
|
||||
return starterNames[0];
|
||||
crawl({
|
||||
dirPath,
|
||||
returnedPathsType: "absolute"
|
||||
}).forEach(filePath => {
|
||||
try {
|
||||
fs.rmSync(filePath, { force: true });
|
||||
} catch (error) {
|
||||
if (filePath.endsWith(".exe")) {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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>({
|
||||
values: starterNames.map(starterName => `..${pathSep}${starterName}`)
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
|
||||
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({
|
||||
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`));
|
||||
})();
|
||||
startRebuildOnSrcChange();
|
||||
|
@ -280,24 +280,6 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
"fonts",
|
||||
"OpenSans-Semibold-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.woff"
|
||||
),
|
||||
pathJoin(
|
||||
"patternfly",
|
||||
"dist",
|
||||
"fonts",
|
||||
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||
),
|
||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||
|
@ -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 { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
|
||||
|
@ -5,30 +5,25 @@ import {
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
import { assert, Equals } from "tsafe/assert";
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "add-story",
|
||||
buildContext
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -38,8 +33,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
@ -50,7 +43,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect({
|
||||
const { value } = await cliSelect<ThemeType>({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
@ -69,16 +62,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log(
|
||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
@ -119,7 +102,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let sourceCode = fs
|
||||
const componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -133,17 +116,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
.replace('import React from "react";\n', "")
|
||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
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(
|
||||
[
|
||||
|
@ -1,96 +1,13 @@
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
|
||||
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";
|
||||
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "copy-keycloak-resources-to-public",
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
copyKeycloakResourcesToPublic({
|
||||
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")
|
||||
);
|
||||
}
|
||||
|
@ -7,30 +7,30 @@ import {
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
type LoginThemePageId,
|
||||
type AccountThemePageId,
|
||||
THEME_TYPES
|
||||
THEME_TYPES,
|
||||
type ThemeType
|
||||
} from "./shared/constants";
|
||||
import { capitalize } from "tsafe/capitalize";
|
||||
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,
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||
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 { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "eject-page",
|
||||
buildContext
|
||||
const buildContext = getBuildContext({
|
||||
cliCommandOptions
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.cyan("Theme type:"));
|
||||
|
||||
const themeType = await (async () => {
|
||||
@ -40,8 +40,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||
case "login":
|
||||
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||
case "admin":
|
||||
return buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
});
|
||||
@ -52,7 +50,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return values[0];
|
||||
}
|
||||
|
||||
const { value } = await cliSelect({
|
||||
const { value } = await cliSelect<ThemeType>({
|
||||
values
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
@ -61,28 +59,87 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return value;
|
||||
})();
|
||||
|
||||
if (themeType === "admin") {
|
||||
console.log("Use `npx keycloakify own` command instead, see documentation");
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||
) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
[
|
||||
"You are implementing a Single-Page Account theme.",
|
||||
"The eject-page command isn't applicable in this context"
|
||||
].join("\n")
|
||||
)
|
||||
const srcDirPath = pathJoin(
|
||||
pathDirname(buildContext.packageJsonFilePath),
|
||||
"node_modules",
|
||||
"@keycloakify",
|
||||
"keycloak-account-ui",
|
||||
"src"
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
return;
|
||||
console.log(
|
||||
[
|
||||
`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:`,
|
||||
``,
|
||||
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||
``
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
eject_entrypoint: {
|
||||
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"account"
|
||||
);
|
||||
|
||||
const targetFilePath = pathJoin(
|
||||
accountThemeSrcDirPath,
|
||||
kcAccountUiTsxFileRelativePath
|
||||
);
|
||||
|
||||
if (fs.existsSync(targetFilePath)) {
|
||||
break eject_entrypoint;
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
|
||||
targetFilePath
|
||||
);
|
||||
|
||||
{
|
||||
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
|
||||
|
||||
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
||||
|
||||
const componentName = pathBasename(
|
||||
kcAccountUiTsxFileRelativePath
|
||||
).replace(/.tsx$/, "");
|
||||
|
||||
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||
`@keycloakify/keycloak-account-ui/${componentName}`,
|
||||
`./${componentName}`
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
kcPageTsxFilePath,
|
||||
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
const routesTsxFilePath = pathRelative(
|
||||
process.cwd(),
|
||||
pathJoin(srcDirPath, "routes.tsx")
|
||||
);
|
||||
|
||||
console.log(
|
||||
[
|
||||
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
||||
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
||||
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
|
||||
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`→ ${themeType}`);
|
||||
@ -92,14 +149,12 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
const templateValue = "Template.tsx (Layout common to every page)";
|
||||
const userProfileFormFieldsValue =
|
||||
"UserProfileFormFields.tsx (Renders the form of the register.ftl, login-update-profile.ftl, update-email.ftl and idp-review-user-profile.ftl)";
|
||||
const otherPageValue = "The page you're looking for isn't listed here";
|
||||
|
||||
const { value: pageIdOrComponent } = await cliSelect<
|
||||
| LoginThemePageId
|
||||
| AccountThemePageId
|
||||
| typeof templateValue
|
||||
| typeof userProfileFormFieldsValue
|
||||
| typeof otherPageValue
|
||||
>({
|
||||
values: (() => {
|
||||
switch (themeType) {
|
||||
@ -107,11 +162,10 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
return [
|
||||
templateValue,
|
||||
userProfileFormFieldsValue,
|
||||
...LOGIN_THEME_PAGE_IDS,
|
||||
otherPageValue
|
||||
...LOGIN_THEME_PAGE_IDS
|
||||
];
|
||||
case "account":
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS, otherPageValue];
|
||||
return [templateValue, ...ACCOUNT_THEME_PAGE_IDS];
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()
|
||||
@ -119,17 +173,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
if (pageIdOrComponent === otherPageValue) {
|
||||
console.log(
|
||||
[
|
||||
"To style a page not included in the base Keycloak, such as one added by a third-party Keycloak extension,",
|
||||
"refer to the documentation: https://docs.keycloakify.dev/features/styling-a-custom-page-not-included-in-base-keycloak"
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`→ ${pageIdOrComponent}`);
|
||||
|
||||
const componentBasename = (() => {
|
||||
@ -173,7 +216,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let componentCode = fs
|
||||
const componentCode = fs
|
||||
.readFileSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
@ -185,17 +228,6 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
)
|
||||
.toString("utf8");
|
||||
|
||||
run_prettier: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break run_prettier;
|
||||
}
|
||||
|
||||
componentCode = await runPrettier({
|
||||
filePath: targetFilePath,
|
||||
sourceCode: componentCode
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
||||
@ -212,12 +244,12 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
)} copy pasted from the Keycloakify source code into your project`
|
||||
);
|
||||
|
||||
edit_KcPage: {
|
||||
edit_KcApp: {
|
||||
if (
|
||||
pageIdOrComponent !== templateValue &&
|
||||
pageIdOrComponent !== userProfileFormFieldsValue
|
||||
) {
|
||||
break edit_KcPage;
|
||||
break edit_KcApp;
|
||||
}
|
||||
|
||||
const kcAppTsxPath = pathJoin(
|
0
src/bin/eject-page/index.ts
Normal file
0
src/bin/eject-page/index.ts
Normal file
0
src/bin/eject-page/react.ts
Normal file
0
src/bin/eject-page/react.ts
Normal file
32
src/bin/initialize-account-theme/copyBoilerplate.ts
Normal file
32
src/bin/initialize-account-theme/copyBoilerplate.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
|
||||
export function copyBoilerplate(params: {
|
||||
accountThemeType: "Single-Page" | "Multi-Page";
|
||||
accountThemeSrcDirPath: string;
|
||||
}) {
|
||||
const { accountThemeType, accountThemeSrcDirPath } = params;
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"src",
|
||||
(() => {
|
||||
switch (accountThemeType) {
|
||||
case "Single-Page":
|
||||
return "single-page";
|
||||
case "Multi-Page":
|
||||
return "multi-page";
|
||||
}
|
||||
assert<Equals<typeof accountThemeType, never>>(false);
|
||||
})()
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
);
|
||||
}
|
@ -1,31 +1,67 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import cliSelect from "cli-select";
|
||||
import child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import * as fs from "fs";
|
||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||
import { command as updateKcGenCommand } from "../update-kc-gen";
|
||||
import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
|
||||
import { exitIfUncommittedChanges } from "../shared/exitIfUncommittedChanges";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
import { generateKcGenTs } from "../shared/generateKcGenTs";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-account-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
if (
|
||||
fs.existsSync(accountThemeSrcDirPath) &&
|
||||
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
accountThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
exit_if_uncommitted_changes: {
|
||||
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({
|
||||
values: ["Single-Page" as const, "Multi-Page" as const]
|
||||
@ -36,41 +72,23 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
switch (accountThemeType) {
|
||||
case "Multi-Page":
|
||||
{
|
||||
if (
|
||||
fs.existsSync(accountThemeSrcDirPath) &&
|
||||
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
accountThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
fs.cpSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"initialize-account-theme",
|
||||
"multi-page-boilerplate"
|
||||
),
|
||||
accountThemeSrcDirPath,
|
||||
{ recursive: true }
|
||||
const { initializeAccountTheme_multiPage } = await import(
|
||||
"./initializeAccountTheme_multiPage"
|
||||
);
|
||||
|
||||
await initializeAccountTheme_multiPage({
|
||||
accountThemeSrcDirPath
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "Single-Page":
|
||||
{
|
||||
const { initializeSpa } = await import("../shared/initializeSpa");
|
||||
const { initializeAccountTheme_singlePage } = await import(
|
||||
"./initializeAccountTheme_singlePage"
|
||||
);
|
||||
|
||||
await initializeSpa({
|
||||
themeType: "account",
|
||||
await initializeAccountTheme_singlePage({
|
||||
accountThemeSrcDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
@ -79,7 +97,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
|
||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||
|
||||
await updateKcGenCommand({
|
||||
await generateKcGenTs({
|
||||
buildContext: {
|
||||
...buildContext,
|
||||
implementedThemeTypes: {
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { relative as pathRelative } from "path";
|
||||
import chalk from "chalk";
|
||||
import { copyBoilerplate } from "./copyBoilerplate";
|
||||
|
||||
export async function initializeAccountTheme_multiPage(params: {
|
||||
accountThemeSrcDirPath: string;
|
||||
}) {
|
||||
const { accountThemeSrcDirPath } = params;
|
||||
|
||||
copyBoilerplate({
|
||||
accountThemeType: "Multi-Page",
|
||||
accountThemeSrcDirPath
|
||||
});
|
||||
|
||||
console.log(
|
||||
[
|
||||
chalk.green("The Multi-Page account theme has been initialized."),
|
||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -0,0 +1,155 @@
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import chalk from "chalk";
|
||||
import {
|
||||
getLatestsSemVersionedTag,
|
||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||
} from "../shared/getLatestsSemVersionedTag";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import fetch from "make-fetch-happen";
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { id } from "tsafe/id";
|
||||
import { npmInstall } from "../tools/npmInstall";
|
||||
import { copyBoilerplate } from "./copyBoilerplate";
|
||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function initializeAccountTheme_singlePage(params: {
|
||||
accountThemeSrcDirPath: string;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { accountThemeSrcDirPath, buildContext } = params;
|
||||
|
||||
const OWNER = "keycloakify";
|
||||
const REPO = "keycloak-account-ui";
|
||||
|
||||
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
||||
owner: OWNER,
|
||||
repo: REPO,
|
||||
count: 1,
|
||||
doIgnoreReleaseCandidates: false,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const dependencies = await fetch(
|
||||
`https://raw.githubusercontent.com/${OWNER}/${REPO}/${semVersionedTag.tag}/dependencies.gen.json`,
|
||||
buildContext.fetchOptions
|
||||
)
|
||||
.then(r => r.json())
|
||||
.then(
|
||||
(() => {
|
||||
type Dependencies = {
|
||||
dependencies: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zDependencies = (() => {
|
||||
type TargetType = Dependencies;
|
||||
|
||||
const zTargetType = z.object({
|
||||
dependencies: z.record(z.string()),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
return o => zDependencies.parse(o);
|
||||
})()
|
||||
);
|
||||
|
||||
dependencies.dependencies["@keycloakify/keycloak-account-ui"] = SemVer.stringify(
|
||||
semVersionedTag.version
|
||||
);
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
dependencies?: Record<string, string>;
|
||||
devDependencies?: Record<string, string>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
parsedPackageJson.dependencies = {
|
||||
...parsedPackageJson.dependencies,
|
||||
...dependencies.dependencies
|
||||
};
|
||||
|
||||
parsedPackageJson.devDependencies = {
|
||||
...parsedPackageJson.devDependencies,
|
||||
...dependencies.devDependencies
|
||||
};
|
||||
|
||||
if (Object.keys(parsedPackageJson.devDependencies).length === 0) {
|
||||
delete parsedPackageJson.devDependencies;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
buildContext.packageJsonFilePath,
|
||||
JSON.stringify(parsedPackageJson, undefined, 4)
|
||||
);
|
||||
|
||||
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({
|
||||
accountThemeType: "Single-Page",
|
||||
accountThemeSrcDirPath
|
||||
});
|
||||
|
||||
console.log(
|
||||
[
|
||||
chalk.green(
|
||||
"The Single-Page account theme has been successfully initialized."
|
||||
),
|
||||
`Using Account UI of Keycloak version: ${chalk.bold(semVersionedTag.tag.split("-")[0])}`,
|
||||
`Directory created: ${chalk.bold(pathRelative(process.cwd(), accountThemeSrcDirPath))}`,
|
||||
`Dependencies added to your project's package.json: `,
|
||||
chalk.bold(JSON.stringify(dependencies, null, 2))
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { i18nBuilder } from "keycloakify/account";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
/** @see: https://docs.keycloakify.dev/features/i18n */
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder.withThemeName<ThemeName>().build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n, type I18n };
|
12
src/bin/initialize-account-theme/src/multi-page/i18n.ts
Normal file
12
src/bin/initialize-account-theme/src/multi-page/i18n.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { i18nBuilder } from "keycloakify/account";
|
||||
import type { ThemeName } from "../kc.gen";
|
||||
|
||||
const { useI18n, ofTypeI18n } = i18nBuilder
|
||||
.withThemeName<ThemeName>()
|
||||
.withExtraLanguages({})
|
||||
.withCustomTranslations({})
|
||||
.build();
|
||||
|
||||
type I18n = typeof ofTypeI18n;
|
||||
|
||||
export { useI18n, type I18n };
|
@ -0,0 +1,7 @@
|
||||
import type { KcContextLike } from "@keycloakify/keycloak-account-ui";
|
||||
import type { KcEnvName } from "../kc.gen";
|
||||
|
||||
export type KcContext = KcContextLike & {
|
||||
themeType: "account";
|
||||
properties: Record<KcEnvName, string>;
|
||||
};
|
11
src/bin/initialize-account-theme/src/single-page/KcPage.tsx
Normal file
11
src/bin/initialize-account-theme/src/single-page/KcPage.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
import { lazy } from "react";
|
||||
import { KcAccountUiLoader } from "@keycloakify/keycloak-account-ui";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
const KcAccountUi = lazy(() => import("@keycloakify/keycloak-account-ui/KcAccountUi"));
|
||||
|
||||
export default function KcPage(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
return <KcAccountUiLoader kcContext={kcContext} KcAccountUi={KcAccountUi} />;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
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 * as fs from "fs";
|
||||
import chalk from "chalk";
|
||||
@ -8,14 +8,12 @@ import { id } from "tsafe/id";
|
||||
|
||||
export type BuildContextLike = {
|
||||
bundler: BuildContext["bundler"];
|
||||
projectDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function updateAccountThemeImplementationInConfig(params: {
|
||||
buildContext: BuildContextLike;
|
||||
buildContext: BuildContext;
|
||||
accountThemeType: "Single-Page" | "Multi-Page";
|
||||
}) {
|
||||
const { buildContext, accountThemeType } = params;
|
||||
@ -83,8 +81,6 @@ export function updateAccountThemeImplementationInConfig(params: {
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
|
@ -1,39 +0,0 @@
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { initializeSpa } from "./shared/initializeSpa";
|
||||
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||
import { command as updateKcGenCommand } from "./update-kc-gen";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-admin-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
|
||||
await initializeSpa({
|
||||
themeType: "admin",
|
||||
buildContext
|
||||
});
|
||||
|
||||
await updateKcGenCommand({
|
||||
buildContext: {
|
||||
...buildContext,
|
||||
implementedThemeTypes: {
|
||||
...buildContext.implementedThemeTypes,
|
||||
admin: {
|
||||
isImplemented: true
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -1,34 +1,15 @@
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import cliSelect from "cli-select";
|
||||
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
|
||||
import { exitIfUncommittedChanges } from "./shared/exitIfUncommittedChanges";
|
||||
|
||||
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import { addSyncExtensionsToPostinstallScript } from "./shared/addSyncExtensionsToPostinstallScript";
|
||||
import { getIsPrettierAvailable, runPrettier } from "./tools/runPrettier";
|
||||
import { npmInstall } from "./tools/npmInstall";
|
||||
import * as child_process from "child_process";
|
||||
import { z } from "zod";
|
||||
import chalk from "chalk";
|
||||
import type { CliCommandOptions } from "./main";
|
||||
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||
commandName: "initialize-email-theme",
|
||||
buildContext
|
||||
});
|
||||
|
||||
if (hasBeenHandled) {
|
||||
return;
|
||||
}
|
||||
|
||||
exitIfUncommittedChanges({
|
||||
projectDirPath: buildContext.projectDirPath
|
||||
});
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
@ -37,120 +18,69 @@ export async function command(params: { buildContext: BuildContext }) {
|
||||
fs.readdirSync(emailThemeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
`There is already a non empty ${pathRelative(
|
||||
process.cwd(),
|
||||
emailThemeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const { value: emailThemeType } = await cliSelect({
|
||||
values: [
|
||||
"native (FreeMarker)" as const,
|
||||
"Another email templating solution" as const
|
||||
]
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
console.log("Initialize with the base email theme from which version of Keycloak?");
|
||||
|
||||
if (emailThemeType === "Another email templating solution") {
|
||||
console.log(
|
||||
[
|
||||
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
||||
"https://docs.keycloakify.dev/theme-types/email-theme"
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
scripts?: Record<string, string | undefined>;
|
||||
dependencies?: Record<string, string | undefined>;
|
||||
devDependencies?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
addSyncExtensionsToPostinstallScript({
|
||||
parsedPackageJson,
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
// NOTE: This is arbitrary
|
||||
startingFromMajor: 17,
|
||||
excludeMajorVersions: [],
|
||||
doOmitPatch: false,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const moduleName = `@keycloakify/email-native`;
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
const fileRelativePath_target = pathRelative(
|
||||
pathJoin("theme", "base", "email"),
|
||||
fileRelativePath
|
||||
);
|
||||
|
||||
const [version] = ((): string[] => {
|
||||
const cmdOutput = child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
if (fileRelativePath_target.startsWith("..")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||
|
||||
// NOTE: Bug in some older npm versions
|
||||
if (typeof versions === "string") {
|
||||
return [versions];
|
||||
await writeFile({ fileRelativePath: fileRelativePath_target });
|
||||
}
|
||||
});
|
||||
|
||||
return versions;
|
||||
})()
|
||||
.reverse()
|
||||
.filter(version => !version.includes("-"));
|
||||
|
||||
assert(version !== undefined);
|
||||
|
||||
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||
|
||||
if (parsedPackageJson.devDependencies !== undefined) {
|
||||
delete parsedPackageJson.devDependencies[moduleName];
|
||||
}
|
||||
transformCodebase({
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
{
|
||||
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||
|
||||
if (await getIsPrettierAvailable()) {
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode,
|
||||
filePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
}
|
||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||
|
||||
fs.writeFileSync(
|
||||
buildContext.packageJsonFilePath,
|
||||
Buffer.from(sourceCode, "utf8")
|
||||
themePropertyFilePath,
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=base`,
|
||||
fs.readFileSync(themePropertyFilePath).toString("utf8")
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await npmInstall({
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
|
||||
console.log(chalk.green("Email theme initialized."));
|
||||
console.log(
|
||||
`The \`${pathJoin(
|
||||
".",
|
||||
pathRelative(process.cwd(), emailThemeSrcDirPath)
|
||||
)}\` directory have been created.`
|
||||
);
|
||||
console.log("You can delete any file you don't modify.");
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import { readFileSync } from "fs";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import child_process from "child_process";
|
||||
import { rmSync } from "../../tools/fs.rmSync";
|
||||
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_generatePom & {
|
||||
@ -105,55 +106,29 @@ export async function buildJar(params: {
|
||||
}
|
||||
});
|
||||
|
||||
{
|
||||
const filePath = pathJoin(
|
||||
tmpResourcesDirPath,
|
||||
"META-INF",
|
||||
"keycloak-themes.json"
|
||||
);
|
||||
remove_account_v1_in_meta_inf: {
|
||||
if (!doesImplementAccountV1Theme) {
|
||||
// NOTE: We do not have account v1 anyway
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
await fs.mkdir(pathDirname(filePath));
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
filePath,
|
||||
Buffer.from(
|
||||
JSON.stringify(
|
||||
{
|
||||
themes: await (async () => {
|
||||
const dirPath = pathJoin(tmpResourcesDirPath, "theme");
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath: tmpResourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
assert(metaInfKeycloakTheme !== undefined);
|
||||
|
||||
const themeNames = (await fs.readdir(dirPath)).sort(
|
||||
(a, b) => {
|
||||
const indexA = buildContext.themeNames.indexOf(a);
|
||||
const indexB = buildContext.themeNames.indexOf(b);
|
||||
metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
|
||||
({ name }) => name !== "account-v1"
|
||||
);
|
||||
|
||||
const orderA = indexA === -1 ? Infinity : indexA;
|
||||
const orderB = indexB === -1 ? Infinity : indexB;
|
||||
|
||||
return orderA - orderB;
|
||||
}
|
||||
);
|
||||
|
||||
return Promise.all(
|
||||
themeNames.map(async themeName => {
|
||||
const types = await fs.readdir(
|
||||
pathJoin(dirPath, themeName)
|
||||
);
|
||||
|
||||
return {
|
||||
name: themeName,
|
||||
types
|
||||
};
|
||||
})
|
||||
);
|
||||
})()
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
return metaInfKeycloakTheme;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
route_legacy_pages: {
|
||||
@ -220,39 +195,31 @@ export async function buildJar(params: {
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
mvnBuildCmd,
|
||||
{ cwd: keycloakifyBuildCacheDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
console.error(
|
||||
[
|
||||
`Build jar failed: ${JSON.stringify(
|
||||
{
|
||||
jarFileBasename,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}`,
|
||||
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
|
||||
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
resolve();
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
await fs.rename(
|
||||
pathJoin(
|
||||
|
@ -11,11 +11,7 @@ import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import {
|
||||
type ThemeType,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
|
||||
} from "../../shared/constants";
|
||||
import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
||||
@ -120,7 +116,6 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
.replace("{{themeVersion}}", buildContext.themeVersion)
|
||||
.replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", "))
|
||||
.replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON)
|
||||
.replace("{{KEYCLOAKIFY_SPA_DEV_SERVER_PORT}}", KEYCLOAKIFY_SPA_DEV_SERVER_PORT)
|
||||
.replace(
|
||||
"{{userDefinedExclusions}}",
|
||||
buildContext.kcContextExclusionsFtlCode ?? ""
|
||||
|
@ -84,47 +84,8 @@ attributes_to_attributesByName: {
|
||||
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;
|
||||
|
||||
|
||||
<#if xKeycloakify.themeType == "login" >
|
||||
{
|
||||
const script = document.createElement("script");
|
||||
@ -190,7 +151,7 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||
key == "loginAction" &&
|
||||
areSamePath(path, ["url"]) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||
) || (
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||
|
@ -22,7 +22,7 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
buildContext: BuildContextLike;
|
||||
themeType: Exclude<ThemeType, "admin">;
|
||||
themeType: ThemeType;
|
||||
}): {
|
||||
languageTags: string[];
|
||||
writeMessagePropertiesFiles: (params: {
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename
|
||||
extname as pathExtname,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
@ -18,9 +19,7 @@ import {
|
||||
type ThemeType,
|
||||
LOGIN_THEME_PAGE_IDS,
|
||||
ACCOUNT_THEME_PAGE_IDS,
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME,
|
||||
THEME_TYPES,
|
||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME
|
||||
} from "../../shared/constants";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { readFieldNameUsage } from "./readFieldNameUsage";
|
||||
@ -30,13 +29,15 @@ import {
|
||||
type BuildContextLike as BuildContextLike_generateMessageProperties
|
||||
} from "./generateMessageProperties";
|
||||
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
|
||||
import {
|
||||
writeMetaInfKeycloakThemes,
|
||||
type MetaInfKeycloakTheme
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import * as child_process from "child_process";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import propertiesParser from "properties-parser";
|
||||
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_generateMessageProperties & {
|
||||
@ -57,8 +58,6 @@ export async function generateResources(params: {
|
||||
buildContext: BuildContextLike;
|
||||
resourcesDirPath: string;
|
||||
}): Promise<void> {
|
||||
const start = Date.now();
|
||||
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const [themeName] = buildContext.themeNames;
|
||||
@ -76,53 +75,22 @@ export async function generateResources(params: {
|
||||
};
|
||||
|
||||
const writeMessagePropertiesFilesByThemeType: Partial<
|
||||
Record<
|
||||
ThemeType | "email",
|
||||
(params: { messageDirPath: string; themeName: string }) => void
|
||||
>
|
||||
Record<ThemeType, (params: { messageDirPath: string; themeName: string }) => void>
|
||||
> = {};
|
||||
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
let isNative: boolean;
|
||||
|
||||
{
|
||||
const v = buildContext.implementedThemeTypes[themeType];
|
||||
|
||||
if (!v.isImplemented && !v.isImplemented_native) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isNative = !v.isImplemented && v.isImplemented_native;
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
if (!buildContext.implementedThemeTypes[themeType].isImplemented) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const getAccountThemeType = () => {
|
||||
assert(themeType === "account");
|
||||
|
||||
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||
|
||||
return buildContext.implementedThemeTypes.account.type;
|
||||
};
|
||||
|
||||
const isSpa = (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return false;
|
||||
case "account":
|
||||
return getAccountThemeType() === "Single-Page";
|
||||
case "admin":
|
||||
return true;
|
||||
case "email":
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
const isForAccountSpa =
|
||||
themeType === "account" &&
|
||||
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page");
|
||||
|
||||
const themeTypeDirPath = getThemeTypeDirPath({ themeName, themeType });
|
||||
|
||||
apply_replacers_and_move_to_theme_resources: {
|
||||
if (isNative) {
|
||||
break apply_replacers_and_move_to_theme_resources;
|
||||
}
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
themeTypeDirPath,
|
||||
"resources",
|
||||
@ -133,7 +101,7 @@ export async function generateResources(params: {
|
||||
rmSync(destDirPath, { recursive: true, force: true });
|
||||
|
||||
if (
|
||||
themeType !== "login" &&
|
||||
themeType === "account" &&
|
||||
buildContext.implementedThemeTypes.login.isImplemented
|
||||
) {
|
||||
// NOTE: We prevent doing it twice, it has been done for the login theme.
|
||||
@ -206,94 +174,51 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
generate_ftl_files: {
|
||||
if (isNative) {
|
||||
break generate_ftl_files;
|
||||
}
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(pathJoin(buildContext.projectBuildDirPath, "index.html"))
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
});
|
||||
|
||||
assert(themeType !== "email");
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
themeName,
|
||||
indexHtmlCode: fs
|
||||
.readFileSync(
|
||||
pathJoin(buildContext.projectBuildDirPath, "index.html")
|
||||
)
|
||||
.toString("utf8"),
|
||||
buildContext,
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
themeType,
|
||||
fieldNames: isSpa
|
||||
? []
|
||||
: (assert(themeType !== "admin"),
|
||||
readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}))
|
||||
});
|
||||
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return LOGIN_THEME_PAGE_IDS;
|
||||
case "account":
|
||||
return getAccountThemeType() === "Single-Page"
|
||||
? ["index.ftl"]
|
||||
: ACCOUNT_THEME_PAGE_IDS;
|
||||
case "admin":
|
||||
return ["index.ftl"];
|
||||
}
|
||||
})(),
|
||||
...(isSpa
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
copy_native_theme: {
|
||||
if (!isNative) {
|
||||
break copy_native_theme;
|
||||
}
|
||||
|
||||
const dirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: dirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (isInside({ dirPath: "messages", filePath: fileRelativePath })) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
[
|
||||
...(() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return LOGIN_THEME_PAGE_IDS;
|
||||
case "account":
|
||||
return isForAccountSpa ? ["index.ftl"] : ACCOUNT_THEME_PAGE_IDS;
|
||||
}
|
||||
});
|
||||
}
|
||||
})(),
|
||||
...(isForAccountSpa
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, pageId),
|
||||
Buffer.from(ftlCode, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
let languageTags: string[] | undefined = undefined;
|
||||
|
||||
i18n_multi_page: {
|
||||
if (isNative) {
|
||||
break i18n_multi_page;
|
||||
i18n_messages_generation: {
|
||||
if (isForAccountSpa) {
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
if (isSpa) {
|
||||
break i18n_multi_page;
|
||||
}
|
||||
|
||||
assert(themeType !== "admin" && themeType !== "email");
|
||||
|
||||
const wrap = generateMessageProperties({
|
||||
buildContext,
|
||||
themeType
|
||||
@ -306,43 +231,22 @@ export async function generateResources(params: {
|
||||
writeMessagePropertiesFiles;
|
||||
}
|
||||
|
||||
let isLegacyAccountSpa = false;
|
||||
|
||||
// NOTE: Eventually remove this block.
|
||||
i18n_single_page_account_legacy: {
|
||||
if (!isSpa) {
|
||||
break i18n_single_page_account_legacy;
|
||||
bring_in_account_v3_i18n_messages: {
|
||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
break bring_in_account_v3_i18n_messages;
|
||||
}
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Single-Page") {
|
||||
break bring_in_account_v3_i18n_messages;
|
||||
}
|
||||
|
||||
if (themeType !== "account") {
|
||||
break i18n_single_page_account_legacy;
|
||||
}
|
||||
const accountUiDirPath = child_process
|
||||
.execSync("npm list @keycloakify/keycloak-account-ui --parseable", {
|
||||
cwd: pathDirname(buildContext.packageJsonFilePath)
|
||||
})
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
const [moduleMeta] = await listInstalledModules({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath,
|
||||
filter: ({ moduleName }) =>
|
||||
moduleName === "@keycloakify/keycloak-account-ui"
|
||||
});
|
||||
|
||||
assert(
|
||||
moduleMeta !== undefined,
|
||||
`@keycloakify/keycloak-account-ui is supposed to be installed`
|
||||
);
|
||||
|
||||
{
|
||||
const [majorStr] = moduleMeta.version.split(".");
|
||||
|
||||
if (majorStr.length === 6) {
|
||||
// NOTE: Now we use the format MMmmpp (Major, minor, patch) for example for
|
||||
// 26.0.7 it would be 260007.
|
||||
break i18n_single_page_account_legacy;
|
||||
} else {
|
||||
// 25.0.4-rc.5 or later
|
||||
isLegacyAccountSpa = true;
|
||||
}
|
||||
}
|
||||
|
||||
const messageDirPath_defaults = pathJoin(moduleMeta.dirPath, "messages");
|
||||
const messageDirPath_defaults = pathJoin(accountUiDirPath, "messages");
|
||||
|
||||
if (!fs.existsSync(messageDirPath_defaults)) {
|
||||
throw new Error(
|
||||
@ -350,8 +254,6 @@ export async function generateResources(params: {
|
||||
);
|
||||
}
|
||||
|
||||
isLegacyAccountSpa = true;
|
||||
|
||||
const messagesDirPath_dest = pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType: "account" }),
|
||||
"messages"
|
||||
@ -413,168 +315,8 @@ export async function generateResources(params: {
|
||||
);
|
||||
}
|
||||
|
||||
i18n_for_spas_and_native: {
|
||||
if (!isSpa && !isNative) {
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
if (isLegacyAccountSpa) {
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
const messagesDirPath_theme = pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
themeType,
|
||||
isNative ? "messages" : "i18n"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(messagesDirPath_theme)) {
|
||||
break i18n_for_spas_and_native;
|
||||
}
|
||||
|
||||
const propertiesByLang: Record<
|
||||
string,
|
||||
{
|
||||
base: Buffer;
|
||||
override: Buffer | undefined;
|
||||
overrideByThemeName: Record<string, Buffer>;
|
||||
}
|
||||
> = {};
|
||||
|
||||
fs.readdirSync(messagesDirPath_theme).forEach(basename => {
|
||||
type ParsedBasename = { lang: string } & (
|
||||
| {
|
||||
isOverride: false;
|
||||
}
|
||||
| {
|
||||
isOverride: true;
|
||||
themeName: string | undefined;
|
||||
}
|
||||
);
|
||||
|
||||
const parsedBasename = ((): ParsedBasename | undefined => {
|
||||
const match = basename.match(/^messages_([^.]+)\.properties$/);
|
||||
|
||||
if (match === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const discriminator = match[1];
|
||||
|
||||
const split = discriminator.split("_override");
|
||||
|
||||
if (split.length === 1) {
|
||||
return {
|
||||
lang: discriminator,
|
||||
isOverride: false
|
||||
};
|
||||
}
|
||||
|
||||
assert(split.length === 2);
|
||||
|
||||
if (split[1] === "") {
|
||||
return {
|
||||
lang: split[0],
|
||||
isOverride: true,
|
||||
themeName: undefined
|
||||
};
|
||||
}
|
||||
|
||||
const match2 = split[1].match(/^_(.+)$/);
|
||||
|
||||
assert(match2 !== null);
|
||||
|
||||
return {
|
||||
lang: split[0],
|
||||
isOverride: true,
|
||||
themeName: match2[1]
|
||||
};
|
||||
})();
|
||||
|
||||
if (parsedBasename === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
propertiesByLang[parsedBasename.lang] ??= {
|
||||
base: createObjectThatThrowsIfAccessed<Buffer>({
|
||||
debugMessage: `No base ${parsedBasename.lang} translation for ${themeType} theme`
|
||||
}),
|
||||
override: undefined,
|
||||
overrideByThemeName: {}
|
||||
};
|
||||
|
||||
const buffer = fs.readFileSync(pathJoin(messagesDirPath_theme, basename));
|
||||
|
||||
if (parsedBasename.isOverride === false) {
|
||||
propertiesByLang[parsedBasename.lang].base = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedBasename.themeName === undefined) {
|
||||
propertiesByLang[parsedBasename.lang].override = buffer;
|
||||
return;
|
||||
}
|
||||
|
||||
propertiesByLang[parsedBasename.lang].overrideByThemeName[
|
||||
parsedBasename.themeName
|
||||
] = buffer;
|
||||
});
|
||||
|
||||
languageTags = Object.keys(propertiesByLang);
|
||||
|
||||
writeMessagePropertiesFilesByThemeType[themeType] = ({
|
||||
messageDirPath,
|
||||
themeName
|
||||
}) => {
|
||||
if (!fs.existsSync(messageDirPath)) {
|
||||
fs.mkdirSync(messageDirPath, { recursive: true });
|
||||
}
|
||||
|
||||
Object.entries(propertiesByLang).forEach(
|
||||
([lang, { base, override, overrideByThemeName }]) => {
|
||||
const messages = propertiesParser.parse(base.toString("utf8"));
|
||||
|
||||
if (override !== undefined) {
|
||||
const overrideMessages = propertiesParser.parse(
|
||||
override.toString("utf8")
|
||||
);
|
||||
|
||||
Object.entries(overrideMessages).forEach(
|
||||
([key, value]) => (messages[key] = value)
|
||||
);
|
||||
}
|
||||
|
||||
if (themeName in overrideByThemeName) {
|
||||
const overrideMessages = propertiesParser.parse(
|
||||
overrideByThemeName[themeName].toString("utf8")
|
||||
);
|
||||
|
||||
Object.entries(overrideMessages).forEach(
|
||||
([key, value]) => (messages[key] = value)
|
||||
);
|
||||
}
|
||||
|
||||
const editor = propertiesParser.createEditor();
|
||||
|
||||
Object.entries(messages).forEach(([key, value]) => {
|
||||
editor.set(key, value);
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(messageDirPath, `messages_${lang}.properties`),
|
||||
Buffer.from(editor.toString(), "utf8")
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
keycloak_static_resources: {
|
||||
if (isNative) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
if (isSpa) {
|
||||
if (isForAccountSpa) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
@ -590,167 +332,171 @@ export async function generateResources(params: {
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v1: {
|
||||
if (isNative) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return isForAccountSpa ? "base" : "account-v1";
|
||||
case "login":
|
||||
return "keycloak";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(isForAccountSpa ? ["deprecatedMode=false"] : []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
),
|
||||
...(languageTags === undefined
|
||||
? []
|
||||
: [`locales=${languageTags.join(",")}`])
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (themeType !== "account") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
email: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break email;
|
||||
}
|
||||
|
||||
assert(buildContext.implementedThemeTypes.account.isImplemented);
|
||||
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeSrcDirPath,
|
||||
destDirPath: getThemeTypeDirPath({ themeName, themeType: "email" })
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: "account-v1",
|
||||
themeType: "account"
|
||||
})
|
||||
bring_in_account_v1: {
|
||||
if (!buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.type !== "Multi-Page") {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: "account-v1",
|
||||
themeType: "account"
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
||||
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: themeName,
|
||||
types: objectEntries(buildContext.implementedThemeTypes)
|
||||
.filter(([, { isImplemented }]) => isImplemented)
|
||||
.map(([themeType]) => themeType)
|
||||
});
|
||||
}
|
||||
|
||||
generate_theme_properties: {
|
||||
if (isNative) {
|
||||
break generate_theme_properties;
|
||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: "account-v1",
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
|
||||
});
|
||||
}
|
||||
|
||||
for (const themeVariantName of buildContext.themeNames) {
|
||||
if (themeVariantName === themeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName),
|
||||
destDirPath: pathJoin(resourcesDirPath, "theme", themeVariantName),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
if (
|
||||
pathExtname(fileRelativePath) === ".ftl" &&
|
||||
fileRelativePath.split(pathSep).length === 2
|
||||
) {
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
Buffer.from(sourceCode)
|
||||
.toString("utf-8")
|
||||
.replace(
|
||||
`"themeName": "${themeName}"`,
|
||||
`"themeName": "${themeVariantName}"`
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
assert(themeType !== "email");
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
Buffer.from(
|
||||
[
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
switch (getAccountThemeType()) {
|
||||
case "Multi-Page":
|
||||
return "account-v1";
|
||||
case "Single-Page":
|
||||
return "base";
|
||||
}
|
||||
case "login":
|
||||
return "keycloak";
|
||||
case "admin":
|
||||
return "base";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>;
|
||||
})()}`,
|
||||
...(themeType === "account" &&
|
||||
getAccountThemeType() === "Single-Page"
|
||||
? ["deprecatedMode=false"]
|
||||
: []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...[
|
||||
...buildContext.environmentVariables,
|
||||
{ name: KEYCLOAKIFY_SPA_DEV_SERVER_PORT, default: "" }
|
||||
].map(
|
||||
({ name, default: defaultValue }) =>
|
||||
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
|
||||
),
|
||||
...(languageTags === undefined
|
||||
? []
|
||||
: [`locales=${languageTags.join(",")}`])
|
||||
].join("\n\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
for (const [themeType, writeMessagePropertiesFiles] of objectEntries(
|
||||
writeMessagePropertiesFilesByThemeType
|
||||
)) {
|
||||
// NOTE: This is just a quirk of the type system: We can't really differentiate in a record
|
||||
// between the case where the key isn't present and the case where the value is `undefined`.
|
||||
if (writeMessagePropertiesFiles === undefined) {
|
||||
return;
|
||||
}
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeName, themeType }),
|
||||
"messages"
|
||||
),
|
||||
themeName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
copy_main_theme_to_theme_variant_theme: {
|
||||
let isNative: boolean;
|
||||
modify_email_theme_per_variant: {
|
||||
if (!buildContext.implementedThemeTypes.email.isImplemented) {
|
||||
break modify_email_theme_per_variant;
|
||||
}
|
||||
|
||||
{
|
||||
const v = buildContext.implementedThemeTypes[themeType];
|
||||
|
||||
if (!v.isImplemented && !v.isImplemented_native) {
|
||||
break copy_main_theme_to_theme_variant_theme;
|
||||
}
|
||||
|
||||
isNative = !v.isImplemented && v.isImplemented_native;
|
||||
}
|
||||
|
||||
if (!isNative && themeVariantName === themeName) {
|
||||
break copy_main_theme_to_theme_variant_theme;
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||
destDirPath: getThemeTypeDirPath({
|
||||
themeName: themeVariantName,
|
||||
themeType
|
||||
}),
|
||||
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||
patch_xKeycloakify_themeName: {
|
||||
if (!fileRelativePath.endsWith(".ftl")) {
|
||||
break patch_xKeycloakify_themeName;
|
||||
}
|
||||
|
||||
if (
|
||||
!isNative &&
|
||||
pathBasename(fileRelativePath) !== fileRelativePath
|
||||
) {
|
||||
break patch_xKeycloakify_themeName;
|
||||
}
|
||||
|
||||
const modifiedSourceCode = Buffer.from(
|
||||
Buffer.from(sourceCode)
|
||||
.toString("utf-8")
|
||||
.replace(
|
||||
...id<[string | RegExp, string]>(
|
||||
isNative
|
||||
? [
|
||||
/xKeycloakify\.themeName/g,
|
||||
`"${themeVariantName}"`
|
||||
]
|
||||
: [
|
||||
`"themeName": "${themeName}"`,
|
||||
`"themeName": "${themeVariantName}"`
|
||||
]
|
||||
)
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
if (!isNative) {
|
||||
assert(
|
||||
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||
);
|
||||
}
|
||||
|
||||
return { modifiedSourceCode };
|
||||
}
|
||||
for (const themeName of buildContext.themeNames) {
|
||||
const emailThemeDirPath = getThemeTypeDirPath({
|
||||
themeName,
|
||||
themeType: "email"
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: emailThemeDirPath,
|
||||
destDirPath: emailThemeDirPath,
|
||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
||||
if (!filePath.endsWith(".ftl")) {
|
||||
return { modifiedSourceCode: sourceCode };
|
||||
}
|
||||
});
|
||||
}
|
||||
run_writeMessagePropertiesFiles: {
|
||||
const writeMessagePropertiesFiles =
|
||||
writeMessagePropertiesFilesByThemeType[themeType];
|
||||
|
||||
if (writeMessagePropertiesFiles === undefined) {
|
||||
break run_writeMessagePropertiesFiles;
|
||||
return {
|
||||
modifiedSourceCode: Buffer.from(
|
||||
sourceCode
|
||||
.toString("utf8")
|
||||
.replace(/xKeycloakify\.themeName/g, `"${themeName}"`),
|
||||
"utf8"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
writeMessagePropertiesFiles({
|
||||
messageDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeName: themeVariantName, themeType }),
|
||||
"messages"
|
||||
),
|
||||
themeName: themeVariantName
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Generated resources in ${Date.now() - start}ms`);
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPa
|
||||
/** Assumes the theme type exists */
|
||||
export function readFieldNameUsage(params: {
|
||||
themeSrcDirPath: string;
|
||||
themeType: Exclude<ThemeType, "admin">;
|
||||
themeType: ThemeType;
|
||||
}): string[] {
|
||||
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 * as child_process from "child_process";
|
||||
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 { buildJars } from "./buildJars";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import chalk from "chalk";
|
||||
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
|
||||
import * as os from "os";
|
||||
import { rmSync } from "../tools/fs.rmSync";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
exit_if_maven_not_installed: {
|
||||
let commandOutput: Buffer | undefined = undefined;
|
||||
|
160
src/bin/main.ts
160
src/bin/main.ts
@ -4,9 +4,8 @@ import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
import { getBuildContext } from "./shared/buildContext";
|
||||
|
||||
type CliCommandOptions = {
|
||||
export type CliCommandOptions = {
|
||||
projectDirPath: string | undefined;
|
||||
};
|
||||
|
||||
@ -70,17 +69,17 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./keycloakify");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command<{
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
}>({
|
||||
name: "start-keycloak",
|
||||
@ -131,18 +130,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./start-keycloak");
|
||||
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: {
|
||||
keycloakVersion:
|
||||
keycloakVersion === undefined ? undefined : `${keycloakVersion}`,
|
||||
port,
|
||||
realmJsonFilePath
|
||||
}
|
||||
});
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -153,10 +144,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./eject-page");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -167,10 +158,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./add-story");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -181,38 +172,24 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./initialize-email-theme");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "initialize-account-theme",
|
||||
description: "Initialize an Account Single-Page or Multi-Page custom Account UI."
|
||||
description: "Initialize the account theme."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./initialize-account-theme");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "initialize-admin-theme",
|
||||
description: "Initialize an Admin Console custom UI."
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./initialize-admin-theme");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -220,14 +197,14 @@ program
|
||||
.command({
|
||||
name: "copy-keycloak-resources-to-public",
|
||||
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({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./copy-keycloak-resources-to-public");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
@ -239,107 +216,10 @@ program
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
handler: async cliCommandOptions => {
|
||||
const { command } = await import("./update-kc-gen");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command({
|
||||
name: "sync-extensions",
|
||||
description: [
|
||||
"Synchronizes all installed Keycloakify extension modules with your project.",
|
||||
"",
|
||||
"Example of extension modules: '@keycloakify/keycloak-account-ui', '@keycloakify/keycloak-admin-ui', '@keycloakify/keycloak-ui-shared'",
|
||||
"",
|
||||
"This command ensures that:",
|
||||
"- All required files from installed extensions are copied into your project.",
|
||||
"- The copied files are correctly ignored by Git to help you distinguish between your custom source files",
|
||||
" and those provided by the extensions.",
|
||||
"- Peer dependencies declared by the extensions are automatically added to your package.json.",
|
||||
"",
|
||||
"You can safely run this command multiple times. It will only update the files and dependencies if needed,",
|
||||
"ensuring your project stays in sync with the installed extensions.",
|
||||
"",
|
||||
"Typical usage:",
|
||||
"- Should be run as a postinstall script of your project.",
|
||||
""
|
||||
].join("\n")
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath }) => {
|
||||
const { command } = await import("./sync-extensions");
|
||||
|
||||
await command({ buildContext: getBuildContext({ projectDirPath }) });
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command<{
|
||||
path: string;
|
||||
revert: boolean;
|
||||
}>({
|
||||
name: "own",
|
||||
description: [
|
||||
"Manages ownership of auto-generated files provided by Keycloakify extensions.",
|
||||
"",
|
||||
"This command allows you to take ownership of a specific file or directory generated",
|
||||
"by an extension. Once owned, you can freely modify and version-control the file.",
|
||||
"",
|
||||
"You can also use the --revert flag to relinquish ownership and restore the file",
|
||||
"or directory to its original auto-generated state.",
|
||||
"",
|
||||
"For convenience, the exact command to take ownership of any file is included as a comment",
|
||||
"in the header of each extension-generated file.",
|
||||
"",
|
||||
"Examples:",
|
||||
"$ npx keycloakify own --path admin/KcPage.tsx"
|
||||
].join("\n")
|
||||
})
|
||||
.option({
|
||||
key: "path",
|
||||
name: (() => {
|
||||
const long = "path";
|
||||
const short = "t";
|
||||
|
||||
optionsKeys.push(long, short);
|
||||
|
||||
return { long, short };
|
||||
})(),
|
||||
description: [
|
||||
"Specifies the relative path of the file or directory to take ownership of.",
|
||||
"This path should be relative to your theme directory.",
|
||||
"Example: `--path 'admin/KcPage.tsx'`"
|
||||
].join(" ")
|
||||
})
|
||||
.option({
|
||||
key: "revert",
|
||||
name: (() => {
|
||||
const long = "revert";
|
||||
const short = "r";
|
||||
|
||||
optionsKeys.push(long, short);
|
||||
|
||||
return { long, short };
|
||||
})(),
|
||||
description: [
|
||||
"Restores a file or directory to its original auto-generated state,",
|
||||
"removing your ownership claim and reverting any modifications."
|
||||
].join(" "),
|
||||
defaultValue: false
|
||||
})
|
||||
.task({
|
||||
skip,
|
||||
handler: async ({ projectDirPath, path, revert }) => {
|
||||
const { command } = await import("./own");
|
||||
|
||||
await command({
|
||||
buildContext: getBuildContext({ projectDirPath }),
|
||||
cliCommandOptions: { path, isRevert: revert }
|
||||
});
|
||||
await command({ cliCommandOptions });
|
||||
}
|
||||
});
|
||||
|
||||
|
208
src/bin/own.ts
208
src/bin/own.ts
@ -1,208 +0,0 @@
|
||||
import type { BuildContext } from "./shared/buildContext";
|
||||
import { getExtensionModuleFileSourceCodeReadyToBeCopied } from "./sync-extensions/getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||
import type { ExtensionModuleMeta } from "./sync-extensions/extensionModuleMeta";
|
||||
import { command as command_syncExtensions } from "./sync-extensions/sync-extension";
|
||||
import {
|
||||
readManagedGitignoreFile,
|
||||
writeManagedGitignoreFile
|
||||
} from "./sync-extensions/managedGitignoreFile";
|
||||
import { getExtensionModuleMetas } from "./sync-extensions/extensionModuleMeta";
|
||||
import { getAbsoluteAndInOsFormatPath } from "./tools/getAbsoluteAndInOsFormatPath";
|
||||
import { relative as pathRelative, dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { getInstalledModuleDirPath } from "./tools/getInstalledModuleDirPath";
|
||||
import * as fsPr from "fs/promises";
|
||||
import { isInside } from "./tools/isInside";
|
||||
import chalk from "chalk";
|
||||
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
cliCommandOptions: {
|
||||
path: string;
|
||||
isRevert: boolean;
|
||||
};
|
||||
}) {
|
||||
const { buildContext, cliCommandOptions } = params;
|
||||
|
||||
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||
|
||||
const { targetFileRelativePathsByExtensionModuleMeta } = await (async () => {
|
||||
const fileOrDirectoryRelativePath = pathRelative(
|
||||
buildContext.themeSrcDirPath,
|
||||
getAbsoluteAndInOsFormatPath({
|
||||
cwd: buildContext.themeSrcDirPath,
|
||||
pathIsh: cliCommandOptions.path
|
||||
})
|
||||
);
|
||||
|
||||
const arr = extensionModuleMetas
|
||||
.map(extensionModuleMeta => ({
|
||||
extensionModuleMeta,
|
||||
fileRelativePaths: extensionModuleMeta.files
|
||||
.map(({ fileRelativePath }) => fileRelativePath)
|
||||
.filter(
|
||||
fileRelativePath =>
|
||||
fileRelativePath === fileOrDirectoryRelativePath ||
|
||||
isInside({
|
||||
dirPath: fileOrDirectoryRelativePath,
|
||||
filePath: fileRelativePath
|
||||
})
|
||||
)
|
||||
}))
|
||||
.filter(({ fileRelativePaths }) => fileRelativePaths.length !== 0);
|
||||
|
||||
const targetFileRelativePathsByExtensionModuleMeta = new Map<
|
||||
ExtensionModuleMeta,
|
||||
string[]
|
||||
>();
|
||||
|
||||
for (const { extensionModuleMeta, fileRelativePaths } of arr) {
|
||||
targetFileRelativePathsByExtensionModuleMeta.set(
|
||||
extensionModuleMeta,
|
||||
fileRelativePaths
|
||||
);
|
||||
}
|
||||
|
||||
return { targetFileRelativePathsByExtensionModuleMeta };
|
||||
})();
|
||||
|
||||
if (targetFileRelativePathsByExtensionModuleMeta.size === 0) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
"There is no Keycloakify extension modules files matching the provided path."
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { ownedFilesRelativePaths: ownedFilesRelativePaths_current } =
|
||||
await readManagedGitignoreFile({
|
||||
buildContext
|
||||
});
|
||||
|
||||
await (cliCommandOptions.isRevert ? command_revert : command_own)({
|
||||
extensionModuleMetas,
|
||||
targetFileRelativePathsByExtensionModuleMeta,
|
||||
ownedFilesRelativePaths_current,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
type Params_subcommands = {
|
||||
extensionModuleMetas: ExtensionModuleMeta[];
|
||||
targetFileRelativePathsByExtensionModuleMeta: Map<ExtensionModuleMeta, string[]>;
|
||||
ownedFilesRelativePaths_current: string[];
|
||||
buildContext: BuildContext;
|
||||
};
|
||||
|
||||
async function command_own(params: Params_subcommands) {
|
||||
const {
|
||||
extensionModuleMetas,
|
||||
targetFileRelativePathsByExtensionModuleMeta,
|
||||
ownedFilesRelativePaths_current,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
extensionModuleMetas,
|
||||
ownedFilesRelativePaths: [
|
||||
...ownedFilesRelativePaths_current,
|
||||
...Array.from(targetFileRelativePathsByExtensionModuleMeta.values())
|
||||
.flat()
|
||||
.filter(
|
||||
fileRelativePath =>
|
||||
!ownedFilesRelativePaths_current.includes(fileRelativePath)
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
const writeActions: (() => Promise<void>)[] = [];
|
||||
|
||||
for (const [
|
||||
extensionModuleMeta,
|
||||
fileRelativePaths
|
||||
] of targetFileRelativePathsByExtensionModuleMeta.entries()) {
|
||||
const extensionModuleDirPath = await getInstalledModuleDirPath({
|
||||
moduleName: extensionModuleMeta.moduleName,
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
|
||||
for (const fileRelativePath of fileRelativePaths) {
|
||||
if (ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||
console.log(
|
||||
chalk.grey(`You already have ownership over '${fileRelativePath}'.`)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
writeActions.push(async () => {
|
||||
const sourceCode = await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||
buildContext,
|
||||
fileRelativePath,
|
||||
isOwnershipAction: true,
|
||||
extensionModuleName: extensionModuleMeta.moduleName,
|
||||
extensionModuleDirPath,
|
||||
extensionModuleVersion: extensionModuleMeta.version
|
||||
});
|
||||
|
||||
await fsPr.writeFile(
|
||||
pathJoin(buildContext.themeSrcDirPath, fileRelativePath),
|
||||
sourceCode
|
||||
);
|
||||
|
||||
console.log(chalk.green(`Ownership over '${fileRelativePath}' claimed.`));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (writeActions.length === 0) {
|
||||
console.log(chalk.yellow("No new file claimed."));
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(writeActions.map(action => action()));
|
||||
}
|
||||
|
||||
async function command_revert(params: Params_subcommands) {
|
||||
const {
|
||||
extensionModuleMetas,
|
||||
targetFileRelativePathsByExtensionModuleMeta,
|
||||
ownedFilesRelativePaths_current,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
const ownedFilesRelativePaths_toRemove = Array.from(
|
||||
targetFileRelativePathsByExtensionModuleMeta.values()
|
||||
)
|
||||
.flat()
|
||||
.filter(fileRelativePath => {
|
||||
if (!ownedFilesRelativePaths_current.includes(fileRelativePath)) {
|
||||
console.log(
|
||||
chalk.grey(`Ownership over '${fileRelativePath}' wasn't claimed.`)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(`Ownership over '${fileRelativePath}' relinquished.`)
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (ownedFilesRelativePaths_toRemove.length === 0) {
|
||||
console.log(chalk.yellow("No file relinquished."));
|
||||
return;
|
||||
}
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
extensionModuleMetas,
|
||||
ownedFilesRelativePaths: ownedFilesRelativePaths_current.filter(
|
||||
fileRelativePath =>
|
||||
!ownedFilesRelativePaths_toRemove.includes(fileRelativePath)
|
||||
)
|
||||
});
|
||||
|
||||
await command_syncExtensions({ buildContext });
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
import { dirname as pathDirname, relative as pathRelative, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export function addSyncExtensionsToPostinstallScript(params: {
|
||||
parsedPackageJson: { scripts?: Record<string, string | undefined> };
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { parsedPackageJson, buildContext } = params;
|
||||
|
||||
const cmd_base = "keycloakify sync-extensions";
|
||||
|
||||
const projectCliOptionValue = (() => {
|
||||
const packageJsonDirPath = pathDirname(buildContext.packageJsonFilePath);
|
||||
|
||||
const relativePath = pathRelative(
|
||||
packageJsonDirPath,
|
||||
buildContext.projectDirPath
|
||||
);
|
||||
|
||||
if (relativePath === "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return relativePath.split(pathSep).join("/");
|
||||
})();
|
||||
|
||||
const generateCmd = (params: { cmd_preexisting: string | undefined }) => {
|
||||
const { cmd_preexisting } = params;
|
||||
|
||||
let cmd = cmd_preexisting === undefined ? "" : `${cmd_preexisting} && `;
|
||||
|
||||
cmd += cmd_base;
|
||||
|
||||
if (projectCliOptionValue !== undefined) {
|
||||
cmd += ` -p ${projectCliOptionValue}`;
|
||||
}
|
||||
|
||||
return cmd;
|
||||
};
|
||||
|
||||
{
|
||||
const scripts = (parsedPackageJson.scripts ??= {});
|
||||
|
||||
for (const scriptName of ["postinstall", "prepare"]) {
|
||||
const cmd_preexisting = scripts[scriptName];
|
||||
|
||||
if (cmd_preexisting === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cmd_preexisting.includes(cmd_base)) {
|
||||
scripts[scriptName] = generateCmd({ cmd_preexisting });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsedPackageJson.scripts = {
|
||||
postinstall: generateCmd({ cmd_preexisting: undefined }),
|
||||
...parsedPackageJson.scripts
|
||||
};
|
||||
}
|
@ -7,9 +7,10 @@ import {
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import { z } from "zod";
|
||||
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 {
|
||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||
@ -18,11 +19,13 @@ import {
|
||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||
import { exclude } from "tsafe";
|
||||
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 { type ThemeType } from "./constants";
|
||||
import { id } from "tsafe/id";
|
||||
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 = {
|
||||
themeVersion: string;
|
||||
@ -40,21 +43,16 @@ export type BuildContext = {
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
fetchOptions: FetchOptionsLike;
|
||||
fetchOptions: ProxyFetchOptions;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
implementedThemeTypes: {
|
||||
login:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
email: { isImplemented: false; isImplemented_native: boolean };
|
||||
login: { isImplemented: boolean };
|
||||
email: { isImplemented: boolean };
|
||||
account:
|
||||
| { isImplemented: false; isImplemented_native: boolean }
|
||||
| { isImplemented: false }
|
||||
| { isImplemented: true; type: "Single-Page" | "Multi-Page" };
|
||||
admin:
|
||||
| { isImplemented: true }
|
||||
| { isImplemented: false; isImplemented_native: boolean };
|
||||
};
|
||||
packageJsonFilePath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
@ -131,12 +129,14 @@ export type ResolvedViteConfig = {
|
||||
};
|
||||
|
||||
export function getBuildContext(params: {
|
||||
projectDirPath: string | undefined;
|
||||
cliCommandOptions: CliCommandOptions;
|
||||
}): BuildContext {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const projectDirPath =
|
||||
params.projectDirPath !== undefined
|
||||
cliCommandOptions.projectDirPath !== undefined
|
||||
? getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: params.projectDirPath,
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
})
|
||||
: process.cwd();
|
||||
@ -149,10 +149,7 @@ export function getBuildContext(params: {
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of [
|
||||
KEYCLOAK_THEME,
|
||||
KEYCLOAK_THEME.replace(/-/g, "_")
|
||||
]) {
|
||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
@ -178,7 +175,7 @@ export function getBuildContext(params: {
|
||||
[
|
||||
`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`,
|
||||
`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")
|
||||
)
|
||||
);
|
||||
@ -438,68 +435,24 @@ export function getBuildContext(params: {
|
||||
assert<Equals<typeof bundler, never>>(false);
|
||||
})();
|
||||
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = (() => {
|
||||
const getIsNative = (dirPath: string) =>
|
||||
fs.existsSync(pathJoin(dirPath, "theme.properties"));
|
||||
const implementedThemeTypes: BuildContext["implementedThemeTypes"] = {
|
||||
login: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "login"))
|
||||
},
|
||||
email: {
|
||||
isImplemented: fs.existsSync(pathJoin(themeSrcDirPath, "email"))
|
||||
},
|
||||
account: (() => {
|
||||
if (buildOptions.accountThemeImplementation === "none") {
|
||||
return { isImplemented: false };
|
||||
}
|
||||
|
||||
return {
|
||||
login: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "login");
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
if (getIsNative(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: true };
|
||||
}
|
||||
|
||||
return { isImplemented: true };
|
||||
})(),
|
||||
email: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "email");
|
||||
|
||||
if (!fs.existsSync(dirPath) || !getIsNative(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
return { isImplemented: false, isImplemented_native: true };
|
||||
})(),
|
||||
account: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "account");
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
if (getIsNative(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: true };
|
||||
}
|
||||
|
||||
if (buildOptions.accountThemeImplementation === "none") {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
return {
|
||||
isImplemented: true,
|
||||
type: buildOptions.accountThemeImplementation
|
||||
};
|
||||
})(),
|
||||
admin: (() => {
|
||||
const dirPath = pathJoin(themeSrcDirPath, "admin");
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: false };
|
||||
}
|
||||
|
||||
if (getIsNative(dirPath)) {
|
||||
return { isImplemented: false, isImplemented_native: true };
|
||||
}
|
||||
|
||||
return { isImplemented: true };
|
||||
})()
|
||||
};
|
||||
})();
|
||||
return {
|
||||
isImplemented: true,
|
||||
type: buildOptions.accountThemeImplementation
|
||||
};
|
||||
})()
|
||||
};
|
||||
|
||||
if (
|
||||
implementedThemeTypes.account.isImplemented &&
|
||||
@ -558,15 +511,6 @@ export function getBuildContext(params: {
|
||||
return themeNames;
|
||||
})();
|
||||
|
||||
const relativePathsCwd = (() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return projectDirPath;
|
||||
case "webpack":
|
||||
return pathDirname(packageJsonFilePath);
|
||||
}
|
||||
})();
|
||||
|
||||
const projectBuildDirPath = (() => {
|
||||
webpack: {
|
||||
if (bundler !== "webpack") {
|
||||
@ -578,7 +522,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -622,7 +566,7 @@ export function getBuildContext(params: {
|
||||
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.keycloakifyBuildDirPath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -651,7 +595,7 @@ export function getBuildContext(params: {
|
||||
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -723,7 +667,7 @@ export function getBuildContext(params: {
|
||||
pathIsh:
|
||||
parsedPackageJson.keycloakify
|
||||
.staticDirPathInProjectBuildDirPath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectBuildDirPath
|
||||
});
|
||||
}
|
||||
|
||||
@ -1051,7 +995,7 @@ export function getBuildContext(params: {
|
||||
type: "path",
|
||||
path: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: urlOrPath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectDirPath
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -1061,7 +1005,7 @@ export function getBuildContext(params: {
|
||||
? undefined
|
||||
: getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||
cwd: relativePathsCwd
|
||||
cwd: projectDirPath
|
||||
}),
|
||||
port: buildOptions.startKeycloakOptions?.port
|
||||
}
|
||||
|
@ -4,14 +4,13 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = {
|
||||
DIST: "dist"
|
||||
} 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 const VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES = {
|
||||
RUN_POST_BUILD_SCRIPT: "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
|
||||
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG",
|
||||
READ_KC_CONTEXT_FROM_URL: "KEYCLOAKIFY_READ_KC_CONTEXT_FROM_URL"
|
||||
RESOLVE_VITE_CONFIG: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
|
||||
} as const;
|
||||
|
||||
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 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";
|
||||
|
95
src/bin/shared/copyKeycloakResourcesToPublic.ts
Normal file
95
src/bin/shared/copyKeycloakResourcesToPublic.ts
Normal file
@ -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,43 +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-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,50 +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 async function maybeDelegateCommandToCustomHandler(params: {
|
||||
commandName: CommandName;
|
||||
buildContext: BuildContext;
|
||||
}): Promise<{ hasBeenHandled: boolean }> {
|
||||
const { commandName, buildContext } = params;
|
||||
|
||||
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
175
src/bin/shared/generateKcGenTs.ts
Normal file
175
src/bin/shared/generateKcGenTs.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
201
src/bin/shared/getLatestsSemVersionedTag.ts
Normal file
201
src/bin/shared/getLatestsSemVersionedTag.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import type { ReturnType } from "tsafe";
|
||||
import type { Param0 } from "tsafe";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import * as fs from "fs";
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import type { SemVer } from "../tools/SemVer";
|
||||
import { same } from "evt/tools/inDepth/same";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import fetch from "make-fetch-happen";
|
||||
|
||||
type GetLatestsSemVersionedTag = ReturnType<
|
||||
typeof getLatestsSemVersionedTagFactory
|
||||
>["getLatestsSemVersionedTag"];
|
||||
|
||||
type Params = Param0<GetLatestsSemVersionedTag>;
|
||||
type R = ReturnType<GetLatestsSemVersionedTag>;
|
||||
|
||||
let getLatestsSemVersionedTag_stateless: GetLatestsSemVersionedTag | undefined =
|
||||
undefined;
|
||||
|
||||
const CACHE_VERSION = 1;
|
||||
|
||||
type Cache = {
|
||||
version: typeof CACHE_VERSION;
|
||||
entries: {
|
||||
time: number;
|
||||
params: Params;
|
||||
result: R;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function getLatestsSemVersionedTag({
|
||||
buildContext,
|
||||
...params
|
||||
}: Params & {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<R> {
|
||||
const cacheFilePath = pathJoin(
|
||||
buildContext.cacheDirPath,
|
||||
"latest-sem-versioned-tags.json"
|
||||
);
|
||||
|
||||
const cacheLookupResult = (() => {
|
||||
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
|
||||
hasCachedResult: false as const,
|
||||
currentCache: {
|
||||
version: CACHE_VERSION,
|
||||
entries: currentCacheEntries
|
||||
}
|
||||
});
|
||||
|
||||
if (!fs.existsSync(cacheFilePath)) {
|
||||
return getResult_currentCache([]);
|
||||
}
|
||||
|
||||
let cache_json;
|
||||
|
||||
try {
|
||||
cache_json = fs.readFileSync(cacheFilePath).toString("utf8");
|
||||
} catch {
|
||||
return getResult_currentCache([]);
|
||||
}
|
||||
|
||||
let cache_json_parsed: unknown;
|
||||
|
||||
try {
|
||||
cache_json_parsed = JSON.parse(cache_json);
|
||||
} catch {
|
||||
return getResult_currentCache([]);
|
||||
}
|
||||
|
||||
const zSemVer = (() => {
|
||||
type TargetType = SemVer;
|
||||
|
||||
const zTargetType = z.object({
|
||||
major: z.number(),
|
||||
minor: z.number(),
|
||||
patch: z.number(),
|
||||
rc: z.number().optional(),
|
||||
parsedFrom: z.string()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const zCache = (() => {
|
||||
type TargetType = Cache;
|
||||
|
||||
const zTargetType = z.object({
|
||||
version: z.literal(CACHE_VERSION),
|
||||
entries: z.array(
|
||||
z.object({
|
||||
time: z.number(),
|
||||
params: z.object({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
count: z.number(),
|
||||
doIgnoreReleaseCandidates: z.boolean()
|
||||
}),
|
||||
result: z.array(
|
||||
z.object({
|
||||
tag: z.string(),
|
||||
version: zSemVer
|
||||
})
|
||||
)
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
let cache: Cache;
|
||||
|
||||
try {
|
||||
cache = zCache.parse(cache_json_parsed);
|
||||
} catch {
|
||||
return getResult_currentCache([]);
|
||||
}
|
||||
|
||||
const cacheEntry = cache.entries.find(e => same(e.params, params));
|
||||
|
||||
if (cacheEntry === undefined) {
|
||||
return getResult_currentCache(cache.entries);
|
||||
}
|
||||
|
||||
if (Date.now() - cacheEntry.time > 3_600_000) {
|
||||
return getResult_currentCache(cache.entries.filter(e => e !== cacheEntry));
|
||||
}
|
||||
return {
|
||||
hasCachedResult: true as const,
|
||||
cachedResult: cacheEntry.result
|
||||
};
|
||||
})();
|
||||
|
||||
if (cacheLookupResult.hasCachedResult) {
|
||||
return cacheLookupResult.cachedResult;
|
||||
}
|
||||
|
||||
const { currentCache } = cacheLookupResult;
|
||||
|
||||
getLatestsSemVersionedTag_stateless ??= (() => {
|
||||
const octokit = (() => {
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
|
||||
const octokit = new Octokit({
|
||||
...(githubToken === undefined ? {} : { auth: githubToken }),
|
||||
request: {
|
||||
fetch: (url: string, options?: any) =>
|
||||
fetch(url, {
|
||||
...options,
|
||||
...buildContext.fetchOptions
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return octokit;
|
||||
})();
|
||||
|
||||
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({
|
||||
octokit
|
||||
});
|
||||
|
||||
return getLatestsSemVersionedTag;
|
||||
})();
|
||||
|
||||
const result = await getLatestsSemVersionedTag_stateless(params);
|
||||
|
||||
currentCache.entries.push({
|
||||
time: Date.now(),
|
||||
params,
|
||||
result
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(cacheFilePath);
|
||||
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(cacheFilePath, JSON.stringify(currentCache, null, 2));
|
||||
|
||||
return result;
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
import { dirname as pathDirname, join as pathJoin, relative as pathRelative } from "path";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
import * as fs from "fs";
|
||||
import { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import {
|
||||
addSyncExtensionsToPostinstallScript,
|
||||
type BuildContextLike as BuildContextLike_addSyncExtensionsToPostinstallScript
|
||||
} from "./addSyncExtensionsToPostinstallScript";
|
||||
import { getIsPrettierAvailable, runPrettier } from "../tools/runPrettier";
|
||||
import { npmInstall } from "../tools/npmInstall";
|
||||
import * as child_process from "child_process";
|
||||
import { z } from "zod";
|
||||
import chalk from "chalk";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_addSyncExtensionsToPostinstallScript & {
|
||||
themeSrcDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function initializeSpa(params: {
|
||||
themeType: "account" | "admin";
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { themeType, buildContext } = params;
|
||||
|
||||
{
|
||||
const themeTypeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, themeType);
|
||||
|
||||
if (
|
||||
fs.existsSync(themeTypeSrcDirPath) &&
|
||||
fs.readdirSync(themeTypeSrcDirPath).length > 0
|
||||
) {
|
||||
console.warn(
|
||||
chalk.red(
|
||||
`There is already a ${pathRelative(
|
||||
process.cwd(),
|
||||
themeTypeSrcDirPath
|
||||
)} directory in your project. Aborting.`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type ParsedPackageJson = {
|
||||
scripts?: Record<string, string | undefined>;
|
||||
dependencies?: Record<string, string | undefined>;
|
||||
devDependencies?: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
scripts: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||
dependencies: z.record(z.union([z.string(), z.undefined()])).optional(),
|
||||
devDependencies: z.record(z.union([z.string(), z.undefined()])).optional()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson;
|
||||
})();
|
||||
|
||||
addSyncExtensionsToPostinstallScript({
|
||||
parsedPackageJson,
|
||||
buildContext
|
||||
});
|
||||
|
||||
const uiSharedMajor = (() => {
|
||||
const dependencies = {
|
||||
...parsedPackageJson.devDependencies,
|
||||
...parsedPackageJson.dependencies
|
||||
};
|
||||
|
||||
const version = dependencies["@keycloakify/keycloak-ui-shared"];
|
||||
|
||||
if (version === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const match = version.match(/^[^~]?(\d+)\./);
|
||||
|
||||
if (match === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return match[1];
|
||||
})();
|
||||
|
||||
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||
|
||||
const version = ((): string[] => {
|
||||
const cmdOutput = child_process
|
||||
.execSync(`npm show ${moduleName} versions --json`)
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||
|
||||
// NOTE: Bug in some older npm versions
|
||||
if (typeof versions === "string") {
|
||||
return [versions];
|
||||
}
|
||||
|
||||
return versions;
|
||||
})()
|
||||
.reverse()
|
||||
.filter(version => !version.includes("-"))
|
||||
.find(version =>
|
||||
uiSharedMajor === undefined ? true : version.startsWith(`${uiSharedMajor}.`)
|
||||
);
|
||||
|
||||
assert(version !== undefined);
|
||||
|
||||
(parsedPackageJson.dependencies ??= {})[moduleName] = `~${version}`;
|
||||
|
||||
if (parsedPackageJson.devDependencies !== undefined) {
|
||||
delete parsedPackageJson.devDependencies[moduleName];
|
||||
}
|
||||
|
||||
{
|
||||
let sourceCode = JSON.stringify(parsedPackageJson, undefined, 2);
|
||||
|
||||
if (await getIsPrettierAvailable()) {
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode,
|
||||
filePath: buildContext.packageJsonFilePath
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
buildContext.packageJsonFilePath,
|
||||
Buffer.from(sourceCode, "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
await npmInstall({
|
||||
packageJsonDirPath: pathDirname(buildContext.packageJsonFilePath)
|
||||
});
|
||||
}
|
40
src/bin/shared/metaInfKeycloakThemes.ts
Normal file
40
src/bin/shared/metaInfKeycloakThemes.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import type { ThemeType } from "./constants";
|
||||
import * as fs from "fs";
|
||||
|
||||
export type MetaInfKeycloakTheme = {
|
||||
themes: { name: string; types: (ThemeType | "email")[] }[];
|
||||
};
|
||||
|
||||
export function writeMetaInfKeycloakThemes(params: {
|
||||
resourcesDirPath: string;
|
||||
getNewMetaInfKeycloakTheme: (params: {
|
||||
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
|
||||
}) => MetaInfKeycloakTheme;
|
||||
}) {
|
||||
const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
|
||||
|
||||
const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
|
||||
|
||||
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
|
||||
? undefined
|
||||
: (JSON.parse(
|
||||
fs.readFileSync(filePath).toString("utf8")
|
||||
) as MetaInfKeycloakTheme);
|
||||
|
||||
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
|
||||
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(filePath);
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
filePath,
|
||||
Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
|
||||
);
|
||||
}
|
72
src/bin/shared/promptKeycloakVersion.ts
Normal file
72
src/bin/shared/promptKeycloakVersion.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
getLatestsSemVersionedTag,
|
||||
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||
} from "./getLatestsSemVersionedTag";
|
||||
import cliSelect from "cli-select";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import type { BuildContext } from "./buildContext";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function promptKeycloakVersion(params: {
|
||||
startingFromMajor: number | undefined;
|
||||
excludeMajorVersions: number[];
|
||||
doOmitPatch: boolean;
|
||||
buildContext: BuildContextLike;
|
||||
}) {
|
||||
const { startingFromMajor, excludeMajorVersions, doOmitPatch, buildContext } = params;
|
||||
|
||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
||||
|
||||
const semVersionedTags = await getLatestsSemVersionedTag({
|
||||
count: 50,
|
||||
owner: "keycloak",
|
||||
repo: "keycloak",
|
||||
doIgnoreReleaseCandidates: true,
|
||||
buildContext
|
||||
});
|
||||
|
||||
semVersionedTags.forEach(semVersionedTag => {
|
||||
if (
|
||||
startingFromMajor !== undefined &&
|
||||
semVersionedTag.version.major < startingFromMajor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSemVersionedTag = semVersionedTagByMajor.get(
|
||||
semVersionedTag.version.major
|
||||
);
|
||||
|
||||
if (
|
||||
currentSemVersionedTag !== undefined &&
|
||||
SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
|
||||
});
|
||||
|
||||
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(
|
||||
({ version }) =>
|
||||
`${version.major}.${version.minor}${doOmitPatch ? "" : `.${version.patch}`}`
|
||||
);
|
||||
|
||||
const { value } = await cliSelect<string>({
|
||||
values: lastMajorVersions
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
const keycloakVersion = value.split(" ")[0];
|
||||
|
||||
return { keycloakVersion };
|
||||
}
|
@ -1,17 +1,18 @@
|
||||
import * as child_process from "child_process";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
import { sep as pathSep, join as pathJoin } from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import * as fs from "fs";
|
||||
import { dirname as pathDirname, relative as pathRelative } from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
bundler: BuildContext["bundler"];
|
||||
projectBuildDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
|
||||
@ -22,36 +23,58 @@ export async function appBuild(params: {
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { parsedPackageJson } = (() => {
|
||||
type ParsedPackageJson = {
|
||||
scripts?: Record<string, string>;
|
||||
};
|
||||
switch (buildContext.bundler) {
|
||||
case "vite":
|
||||
return appBuild_vite({ buildContext });
|
||||
case "webpack":
|
||||
return appBuild_webpack({ buildContext });
|
||||
}
|
||||
}
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
async function appBuild_vite(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const zTargetType = z.object({
|
||||
scripts: z.record(z.string()).optional()
|
||||
});
|
||||
assert(buildContext.bundler === "vite");
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
const dIsSuccess = new Deferred<boolean>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
console.log(chalk.blue("$ npx vite build"));
|
||||
|
||||
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(
|
||||
([, scriptCommand]) => scriptCommand.includes("keycloakify build")
|
||||
);
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
||||
|
||||
const isSuccess = await dIsSuccess.pr;
|
||||
|
||||
return { isAppBuildSuccess: isSuccess };
|
||||
}
|
||||
|
||||
async function appBuild_webpack(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
assert(buildContext.bundler === "webpack");
|
||||
|
||||
const entries = Object.entries(
|
||||
(JSON.parse(fs.readFileSync(buildContext.packageJsonFilePath).toString("utf8"))
|
||||
.scripts ?? {}) as Record<string, string>
|
||||
).filter(([, scriptCommand]) => scriptCommand.includes("keycloakify build"));
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log(
|
||||
@ -104,76 +127,6 @@ export async function appBuild(params: {
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
common_case: {
|
||||
if (appBuildSubCommands.length !== 1) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
const [appBuildSubCommand] = appBuildSubCommands;
|
||||
|
||||
const isNpmRunBuild = (() => {
|
||||
for (const packageManager of ["npm", "yarn", "pnpm", "bun", "deno"]) {
|
||||
for (const doUseRun of [true, false]) {
|
||||
if (
|
||||
`${packageManager}${doUseRun ? " run " : " "}build` ===
|
||||
appBuildSubCommand
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (!isNpmRunBuild) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
const { scripts } = parsedPackageJson;
|
||||
|
||||
assert(scripts !== undefined);
|
||||
|
||||
const buildCmd = scripts.build;
|
||||
|
||||
if (buildCmd !== "tsc && vite build") {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
if (scripts.prebuild !== undefined) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
if (scripts.postbuild !== undefined) {
|
||||
break common_case;
|
||||
}
|
||||
|
||||
const dIsSuccess = new Deferred<boolean>();
|
||||
|
||||
console.log(chalk.blue("$ npx vite build"));
|
||||
|
||||
const child = child_process.spawn("npx", ["vite", "build"], {
|
||||
cwd: buildContext.projectDirPath,
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
if (data.toString("utf8").includes("gzip:")) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(data);
|
||||
});
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
||||
|
||||
const isSuccess = await dIsSuccess.pr;
|
||||
|
||||
return { isAppBuildSuccess: isSuccess };
|
||||
}
|
||||
|
||||
let commandCwd = pathDirname(buildContext.packageJsonFilePath);
|
||||
|
||||
for (const subCommand of appBuildSubCommands) {
|
||||
|
@ -1,267 +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";
|
||||
import type { ReturnType } from "tsafe";
|
||||
|
||||
export type BuildContextLike = {
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
cacheDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||
|
||||
export async function getSupportedDockerImageTags(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
allSupportedTags: string[];
|
||||
latestMajorTags: string[];
|
||||
}> {
|
||||
const { buildContext } = params;
|
||||
|
||||
{
|
||||
const result = await getCachedValue({ cacheDirPath: buildContext.cacheDirPath });
|
||||
|
||||
if (result !== undefined) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const tags_queryResponse: string[] = [];
|
||||
|
||||
await (async function callee(url: string) {
|
||||
const r = await fetch(url, buildContext.fetchOptions);
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
tags_queryResponse.push(
|
||||
...z
|
||||
.object({
|
||||
tags: z.array(z.string())
|
||||
})
|
||||
.parse(await r.json()).tags
|
||||
);
|
||||
})(),
|
||||
(async () => {
|
||||
const link = r.headers.get("link");
|
||||
|
||||
if (link === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const split = link.split(";").map(s => s.trim());
|
||||
|
||||
assert(split.length === 2);
|
||||
|
||||
assert(split[1] === 'rel="next"');
|
||||
|
||||
const match = split[0].match(/^<(.+)>$/);
|
||||
|
||||
assert(match !== null);
|
||||
|
||||
const nextUrl = new URL(url).origin + match[1];
|
||||
|
||||
await callee(nextUrl);
|
||||
})()
|
||||
]);
|
||||
})("https://quay.io/v2/keycloak/keycloak/tags/list");
|
||||
|
||||
const supportedKeycloakMajorVersions = getSupportedKeycloakMajorVersions();
|
||||
|
||||
const allSupportedTags_withVersion = tags_queryResponse
|
||||
.map(tag => ({
|
||||
tag,
|
||||
version: (() => {
|
||||
if (tag.includes("-")) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let version: SemVer;
|
||||
|
||||
try {
|
||||
version = SemVer.parse(tag);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (tag.split(".").length !== 3) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return version;
|
||||
})()
|
||||
}))
|
||||
.map(({ tag, version }) => (version === undefined ? undefined : { tag, version }))
|
||||
.filter(exclude(undefined))
|
||||
.sort(({ version: a }, { version: b }) => SemVer.compare(b, a));
|
||||
|
||||
const latestTagByMajor: Record<number, SemVer | undefined> = {};
|
||||
|
||||
for (const { version } of allSupportedTags_withVersion) {
|
||||
const version_current = latestTagByMajor[version.major];
|
||||
|
||||
if (
|
||||
version_current === undefined ||
|
||||
SemVer.compare(version_current, version) === -1
|
||||
) {
|
||||
latestTagByMajor[version.major] = version;
|
||||
}
|
||||
}
|
||||
|
||||
const latestMajorTags = Object.entries(latestTagByMajor)
|
||||
.sort(([a], [b]) => parseInt(b) - parseInt(a))
|
||||
.map(([, version]) => version)
|
||||
.map(version => {
|
||||
assert(version !== undefined);
|
||||
|
||||
if (!supportedKeycloakMajorVersions.includes(version.major)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return SemVer.stringify(version);
|
||||
})
|
||||
.filter(exclude(undefined));
|
||||
|
||||
const allSupportedTags = allSupportedTags_withVersion.map(({ tag }) => tag);
|
||||
|
||||
const result = {
|
||||
latestMajorTags,
|
||||
allSupportedTags
|
||||
};
|
||||
|
||||
await setCachedValue({ cacheDirPath: buildContext.cacheDirPath, result });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const { getCachedValue, setCachedValue } = (() => {
|
||||
type Result = ReturnType<typeof getSupportedDockerImageTags>;
|
||||
|
||||
const zResult = (() => {
|
||||
type TargetType = Result;
|
||||
|
||||
const zTargetType = z.object({
|
||||
allSupportedTags: z.array(z.string()),
|
||||
latestMajorTags: z.array(z.string())
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
type Cache = {
|
||||
keycloakifyVersion: string;
|
||||
time: number;
|
||||
result: Result;
|
||||
};
|
||||
|
||||
const zCache = (() => {
|
||||
type TargetType = Cache;
|
||||
|
||||
const zTargetType = z.object({
|
||||
keycloakifyVersion: z.string(),
|
||||
time: z.number(),
|
||||
result: zResult
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
let inMemoryCachedResult: Cache["result"] | undefined = undefined;
|
||||
|
||||
function getCacheFilePath(params: { cacheDirPath: string }) {
|
||||
const { cacheDirPath } = params;
|
||||
|
||||
return pathJoin(cacheDirPath, "supportedDockerImageTags.json");
|
||||
}
|
||||
|
||||
async function getCachedValue(params: { cacheDirPath: string }) {
|
||||
const { cacheDirPath } = params;
|
||||
|
||||
if (inMemoryCachedResult !== undefined) {
|
||||
return inMemoryCachedResult;
|
||||
}
|
||||
|
||||
const cacheFilePath = getCacheFilePath({ cacheDirPath });
|
||||
|
||||
if (!(await existsAsync(cacheFilePath))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cache: Cache | undefined;
|
||||
|
||||
try {
|
||||
cache = zCache.parse(JSON.parse(await fs.readFile(cacheFilePath, "utf8")));
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cache.keycloakifyVersion !== readThisNpmPackageVersion()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (Date.now() - cache.time > 3_600 * 24) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
inMemoryCachedResult = cache.result;
|
||||
|
||||
return cache.result;
|
||||
}
|
||||
|
||||
async function setCachedValue(params: {
|
||||
cacheDirPath: string;
|
||||
result: Cache["result"];
|
||||
}) {
|
||||
const { cacheDirPath, result } = params;
|
||||
|
||||
inMemoryCachedResult = result;
|
||||
|
||||
const cacheFilePath = getCacheFilePath({ cacheDirPath });
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(cacheFilePath);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
await fs.mkdir(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
cacheFilePath,
|
||||
JSON.stringify(
|
||||
zCache.parse({
|
||||
keycloakifyVersion: readThisNpmPackageVersion(),
|
||||
time: Date.now(),
|
||||
result
|
||||
}),
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
getCachedValue,
|
||||
setCachedValue
|
||||
};
|
||||
})();
|
@ -73,7 +73,7 @@
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"client": {
|
||||
"account": ["view-profile", "manage-account", "delete-account"]
|
||||
"account": ["delete-account", "view-profile", "manage-account"]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@ -398,26 +398,6 @@
|
||||
"otpPolicyLookAheadWindow": 1,
|
||||
"otpPolicyPeriod": 30,
|
||||
"otpSupportedApplications": ["FreeOTP", "Google Authenticator"],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyRpId": "",
|
||||
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
||||
"webAuthnPolicyRequireResidentKey": "not specified",
|
||||
"webAuthnPolicyUserVerificationRequirement": "not specified",
|
||||
"webAuthnPolicyCreateTimeout": 0,
|
||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
|
||||
"webAuthnPolicyAcceptableAaguids": [],
|
||||
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
|
||||
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyPasswordlessRpId": "",
|
||||
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
|
||||
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
|
||||
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
|
||||
"webAuthnPolicyPasswordlessCreateTimeout": 0,
|
||||
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
|
||||
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
|
||||
"users": [
|
||||
{
|
||||
"id": "00a62e75-bcc1-419a-a292-63ee5d161ed3",
|
||||
@ -442,43 +422,30 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"create-client",
|
||||
"view-identity-providers",
|
||||
"manage-realm",
|
||||
"query-groups",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"realm-admin",
|
||||
"view-authorization",
|
||||
"view-events",
|
||||
"view-clients",
|
||||
"view-realm",
|
||||
"manage-events",
|
||||
"query-realms",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"manage-users",
|
||||
"view-users",
|
||||
"impersonation",
|
||||
"manage-authorization"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"view-applications",
|
||||
"manage-consent",
|
||||
"delete-account",
|
||||
"manage-account",
|
||||
"view-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyRpId": "",
|
||||
"webAuthnPolicyAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyAuthenticatorAttachment": "not specified",
|
||||
"webAuthnPolicyRequireResidentKey": "not specified",
|
||||
"webAuthnPolicyUserVerificationRequirement": "not specified",
|
||||
"webAuthnPolicyCreateTimeout": 0,
|
||||
"webAuthnPolicyAvoidSameAuthenticatorRegister": false,
|
||||
"webAuthnPolicyAcceptableAaguids": [],
|
||||
"webAuthnPolicyPasswordlessRpEntityName": "keycloak",
|
||||
"webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyPasswordlessRpId": "",
|
||||
"webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
|
||||
"webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
|
||||
"webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
|
||||
"webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
|
||||
"webAuthnPolicyPasswordlessCreateTimeout": 0,
|
||||
"webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
|
||||
"webAuthnPolicyPasswordlessAcceptableAaguids": [],
|
||||
"scopeMappings": [
|
||||
{
|
||||
"clientScope": "offline_access",
|
||||
@ -538,12 +505,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -555,7 +518,6 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+",
|
||||
"pkce.code.challenge.method": "S256"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
@ -674,7 +636,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"display.on.consent.screen": "false",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
@ -732,12 +694,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -749,31 +707,12 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"post.logout.redirect.uris": "+",
|
||||
"pkce.code.challenge.method": "S256"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"introspection.token.claim": "true",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||
"name": "locale",
|
||||
@ -818,8 +757,7 @@
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"userinfo.token.claim": "true"
|
||||
"access.token.claim": "true"
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -1267,7 +1205,6 @@
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"multivalued": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"user.attribute": "foo",
|
||||
"id.token.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
@ -1334,11 +1271,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1354,14 +1291,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper"
|
||||
"oidc-address-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1410,14 +1347,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"oidc-address-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"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1457,12 +1394,6 @@
|
||||
"providerId": "rsa-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEpAIBAAKCAQEA+VQAcuaRivrzLVI8H/tt8PKbtRznTQKmmxOdLRR37leY/ph7sFnEmZt6K02Rvut7R0dxUFtTdiEHUKxhyM8CADMznGUjDYj/EXQzLfZ3LEwbwmR39zp+fZL/H24UDO03zt23Ov9C8Aly0ufXZ1Ic1c33KW6UtUEK/3M52pU8Y0daWdjx7nBj1eRlzWfVG+BYotTTWEnFJuEoZPFQMiXqeA5ob1zZdXjL5JDuGEiBsYjtiiaKbKL5545+FmEBnoCmWXqGu0qWxI2TzvV2dohxfl5KjNzRoKt40ydraiVk5rtBpoNDpeEApuphbokH5dJVwJ5cvWu1CSTnYPW2jXeG4wIDAQABAoIBAQDHV6AcPbhz8/xlafBkabQXBwHzJi7QZaQrLN1n44uX5jWOqP+LmdoULjjZUmWKzd98t+QjKUFrmzCsEYcE9G1XF5jWHA6Qjc3ReKRKxVm28wrmu0knQ39KizKrQGmLhEYwgRg0dU5heExzz6VrGD2xu8E3QRBocp6GauwAlXz4qcnTPHOl8OBPeDHAc0RUdaL5+jRLgKQzf9nnnKB19imBKP++zwrwFrkOZti2ZPs1I7j/ym27mHUbi8TDI2VepDX4QwjjC5a+v3vTsVAGE+1tUAZtqpxpIP9hiUkLH3ajyvp3typhnmZHklqsSZdwtRcK94WiMzL3TkiY70y8abMhAoGBAP8I4EQRXxcKfBn23eaRw8Cd4PFrOouz4zFbYLrBODsvXfku/jnQOMFD0If4IzT6y0FGgBd+t/yqnFJi98oZOKm3P8w+NZBXTbFLH8rgmsElXyS0+9LVMjVa7+UlqZB1eRZbUeLREp03Fsz1y2rflnoWgUnpDIlyhmJqGhCsJdebAoGBAPpFmJ9P42mUTeDWpCyCxgg0zpp6rlpAP8StqZkcvr7kYjhbWrJfJuxrTXtzTTA1zZ59L9EvEAxuug/gl9BkuZ11Uzg8ZLOr4gSuAJZlAORaxJlcoylmNMYIL1fP/K0dxhdO0eHZOpPVpBmGctgev2HBtWp9ZwzQ3DddKimZfNZZAoGAfNOOWSKbhT6HgXnYIHtl8YgUynUuYaR5ZfYQwTfDWwyTFVzP5+IndUjI71Qff1XlWBy2o0lNqmijPJveJlfz6PWdT01/kBd7GnTnqbgHZtPw3pmKzCW3fm/1DRZDCUbGLpAh4z9rufF1wnnnx3aKQ1VykId1sGySo+bEvTZVC1MCgYAlv6uWk/ksKpdYi2d14z+1aymieVClAj3cD4meM4y9xDrgXz8d2mZHkKO+NBT3aZYbCqzUs3GLPoRH8stTPm4UxuaHe+yAgTN1Gz2xcYih6OLwct2VV/oryH5Dk3Z8Mhp314amtxozxCydQP8/g9vABfS0HDgX4cTlgOLkJWeD+QKBgQDuRtsstQ4Q3yK44himPi1JQMMvbYAqyGgRxWH8G1Kr41DV2sQ4wt9CbYxeh6RwMsE+YYNMkTAw1kksUTugWdcDnYpcSVG7xHLJk8WMti0WTqI/7KlkoRehXXv18WJNEXaCr5mJTtJL9wuQcd8nhkEDrrCZubZiJzX9IDnEqZc4Mg=="
|
||||
],
|
||||
"certificate": [
|
||||
"MIICnTCCAYUCBgGTy58etTANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPlUAHLmkYr68y1SPB/7bfDym7Uc500CppsTnS0Ud+5XmP6Ye7BZxJmbeitNkb7re0dHcVBbU3YhB1CsYcjPAgAzM5xlIw2I/xF0My32dyxMG8Jkd/c6fn2S/x9uFAztN87dtzr/QvAJctLn12dSHNXN9ylulLVBCv9zOdqVPGNHWlnY8e5wY9XkZc1n1RvgWKLU01hJxSbhKGTxUDIl6ngOaG9c2XV4y+SQ7hhIgbGI7Yomimyi+eeOfhZhAZ6Apll6hrtKlsSNk871dnaIcX5eSozc0aCreNMna2olZOa7QaaDQ6XhAKbqYW6JB+XSVcCeXL1rtQkk52D1to13huMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAH/nsEi88hFiNPCWYvTB3lERZpeUCbpDzAXQT/4TONmOw8zi7Cd2OlX8BGBFqjh/fESHv+adlzsY1mUdMvpVaYgHr3gYi8sBSrq5TMUfSYaWp4WCD7utiXXGprG08GCdbye1lpyyNnniWp12Bgjao+rtGamL/M1d6+WZTC+XL+H30u4VHURAiFBsAEoX6tlGV8ynhYOr/b8B43jy0/R0JfrzLjwSKEcA6RfKM7ozbZ0QZuQDALULymPIesrV4mvZ2Qwg4YgpAKaki9Sse45yiIhsIY0p5RnuNZRZnCbukyeBzIyDJobEBGhpui/KT2dqXBlRgRuOhCUf7OGCcPVHKNQ=="
|
||||
],
|
||||
"priority": ["100"]
|
||||
}
|
||||
},
|
||||
@ -1472,12 +1403,6 @@
|
||||
"providerId": "rsa-enc-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEogIBAAKCAQEAn82AU+InXwYlE8u9lMwhQghZB7oQ71Hg3PdFqS9ICGzw1u1JcENooCsZse55V6nqptdYF1oZA8QrxnhHzCVCGIqFHtXSoPGHVtozO3Fe1cVIVFm1D9TNS3JHe1C8SBQQT4hGItO5cjDyfGdK3x09RkoAcelrzH5uQ78zd0FKHkzbsTMsP2V8V94c35+ViIUjyGhH2T2BpIyGRLignL+6d0wHbw463L1Ewj/J9z8BtNLCH9PaVLWiGQARjlWyL9vtWBig9XXL0Z9tZUuoLihjh4StkXt2lQ++DKxUklsAjyenRAG5d72T2rY8MO5a1Z2ZSt8+s86D5esrAEIFZc9mqwIDAQABAoIBAAmmCcqGzCPDpjd0xMSYMqXfBSkfReh9RBtzXqRhc3L2yO/hMd7yYv3QvGNu56qwWreqJup6CSqeDJqWJpef5EbBDlqXRHltO+O1lwROyxATMlPNes4y5hZZFxHOBSBA/d8fdkSiDf9kDzANuIqSJGH7E93M3zJgq92xTLU1nvkHR/VYJQv+j+Pjye7MWvjIePfhwFeBqEWlWPTlw/080Mpfp8Hhbl6JeKjx2inkSphp43v4wR1Wmp+E2JIHF4P4sVXPPuPf3JDwg5uGOrROw1ziloD3jTI+LnQ+kRm6R2EbqRqqVsehXT7mZy2puQNqVc4vVqWQdxIErMBazYEpZOECgYEA+8PEcDiIPr2PTYZk+/jErRVYwsxyLgDJexPak7onLxLBJRNRnp1Uk6b1LXM6af5qp+Y510kyAe1k+9xkQLx1gW8rMka9rvVsM+1A2ACvF99V23sRw29CVxeFV/zNn83MinYPX5biUl6MkOX2PvWUhdwRGhKByjiYcAeBOsXkz3ECgYEAon2yYXGzph8Vb8Fetv0wFFbjQOixuL02OjVp/nU1XVE8Aw9BJ7uzA6GQ7akPG0HsaUq7AEHP1uUOsJWQTNQ8WYD9LDuDOl/JFqkG+zrmdUdm0mAIYyH1/GBqgaTLvMq78qqosua8BBJojEyoXDz69UBHpu7cwtUgmzRNQSYqgdsCgYASvD3JEBvrd1XLsh2ftqKEMtt5G5e/nqVfuFmCts6lrSKcbLSdNh4OItWJ/VIygxFSz0osoDDNfeoO6Ba5zox8BlbTlfoVpAPaVWSG7n4ZK7CK9bybq5LnQkPVCWYP51O6VhDMz0CmWozhV4ucoc/cqkTHiOsJrm6Bn71ZL1LYsQKBgFNb8qgk4YnGhoPHiuSLbR/yFzGUbqAciXZBMrg0vwS5iPT03XMZytOBDk2uHi7YmgTGLrsKCCrxZaDXiaiwdKliD/+iJEdNHmc+nXNDGzltQOWKGKNqp7wqZllOBqs6wkLSpCrrTec03mejZ/ex3Pj2WgvcnGpjVg/pO/zBLKtjAoGACzGQNEF93fabHQJTsHmb/g+jO2iumjF6ZIWzdFh2KzQABONcoBvy1MJNASFQj3iVy/8kEo4SfmexvMWLBW9igi2z1pHeHY32EuImzuc4xnVDm6dkmDdsO43Ex6CFBx8lM40H4l27mXu+EZRzGClUY8TnmV/FBGmX+LPtOiiwT7s="
|
||||
],
|
||||
"certificate": [
|
||||
"MIICnTCCAYUCBgGTy58fHjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MTIxNTE4Mzg0M1oXDTM0MTIxNTE4NDAyM1owEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ/NgFPiJ18GJRPLvZTMIUIIWQe6EO9R4Nz3RakvSAhs8NbtSXBDaKArGbHueVep6qbXWBdaGQPEK8Z4R8wlQhiKhR7V0qDxh1baMztxXtXFSFRZtQ/UzUtyR3tQvEgUEE+IRiLTuXIw8nxnSt8dPUZKAHHpa8x+bkO/M3dBSh5M27EzLD9lfFfeHN+flYiFI8hoR9k9gaSMhkS4oJy/undMB28OOty9RMI/yfc/AbTSwh/T2lS1ohkAEY5Vsi/b7VgYoPV1y9GfbWVLqC4oY4eErZF7dpUPvgysVJJbAI8np0QBuXe9k9q2PDDuWtWdmUrfPrPOg+XrKwBCBWXPZqsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEATwmKBzLiZiUjyB9BWUR4BCXh46DxsiM0BCublewlUFY6FBTn7ea6q3G+X3QP2WM6xa0oAmQz9dq1KChbIoC2WPbceAbwd5XZZfziWsRCv6+xPswtpHPIrsenz8TR4K4P73aeCC+vTVs/y+2tGPEVbnSkcNnOP71hRQGlt0LvjKlEetJSRyYz5depSdJOjl4F3ehpxQtTK/48xUVAytu9ZotJj6AUA7jWFlP5GHgoB+mPk6QTHNWddnc7BQx2FMvg151vxu722ywLh5Dh7WzgFhJNwkX4xpwzhfo0Q1gSygGTdZaJCGj5jfF+KwdiKpN04UxJ8OrRgJqklQgrSVnsgQ=="
|
||||
],
|
||||
"priority": ["100"],
|
||||
"algorithm": ["RSA-OAEP"]
|
||||
}
|
||||
@ -1488,8 +1413,6 @@
|
||||
"providerId": "aes-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["132fb843-59e9-4f36-ad55-5ce2d3a13fb3"],
|
||||
"secret": ["ETyyqapnrkUsNXLQ-tBVKw"],
|
||||
"priority": ["100"]
|
||||
}
|
||||
},
|
||||
@ -1499,10 +1422,6 @@
|
||||
"providerId": "hmac-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["5110d380-c930-49d9-b91b-87f338f6170b"],
|
||||
"secret": [
|
||||
"uCpQrJvP5OBuTxXfDb4JRL0bCKpXUgfGn5vb8UvL-Sfs_sZ9rtvBmd6vuFWARqyezjJQtpoNlMv7sXgxkN-yxQ"
|
||||
],
|
||||
"priority": ["100"],
|
||||
"algorithm": ["HS256"]
|
||||
}
|
||||
@ -1535,7 +1454,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "f664efe4-102d-4ec1-bf11-11af67e3f178",
|
||||
"id": "f7f2b89b-43cb-491d-8e7c-f1814024a6da",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1561,7 +1480,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8a5630c5-eca1-4b6a-8e59-459cb6c84535",
|
||||
"id": "17cdac6f-d2a3-4907-8d44-a42827610b63",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1595,7 +1514,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "c1a3eed3-25ce-44ae-93d1-f0b8148a0f8c",
|
||||
"id": "53a3e43f-9468-401f-8051-40f982d12f85",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1621,7 +1540,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6eb188ad-1041-44dd-bf8f-37cae0d98bf1",
|
||||
"id": "26286808-3b7b-43df-b32e-af55a37af2e9",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1647,7 +1566,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4ee215ac-f4e5-4edb-bf76-65dc9e211543",
|
||||
"id": "8a6a752a-9a9a-4d38-b1f8-edf0a9433490",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1673,7 +1592,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5a1eac7e-06a0-46d8-b9ae-1f2c934331f9",
|
||||
"id": "a6f6804c-4160-4a84-8a1f-c2747a2d3f27",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1699,7 +1618,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ed165166-4521-4a62-b185-c4b51643cbb1",
|
||||
"id": "740baa9e-8328-4035-9e1a-8fc1616d1f0f",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1725,7 +1644,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4788fb1f-fd81-4f5d-9abe-4199dd641c1e",
|
||||
"id": "e60187a8-3e16-4a0c-9daa-f3a4a1fcfdba",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1752,7 +1671,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d778a70f-f472-4dd3-ac40-cb5612ddc171",
|
||||
"id": "d959d0c2-4004-4633-b280-f80d6423f574",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1778,7 +1697,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9c1ea8ea-7c23-4e60-b02d-1900d9dc4109",
|
||||
"id": "ba02689d-b9e8-4a4b-8fdd-0d1386b198fc",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1820,7 +1739,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0ebdf418-d57d-4318-9359-7bd0cb2381f2",
|
||||
"id": "f09ac92a-e091-4e84-9cd1-cb905ca57b89",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1862,7 +1781,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5cc89293-c72e-4c5e-b31c-15558588a60d",
|
||||
"id": "aaf72b22-cec4-4714-93d6-f54d5a986ab8",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1896,7 +1815,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "5ae5a321-ccac-449e-9c19-d6dc22ab8085",
|
||||
"id": "c4a54bb3-f009-4231-a82b-376c2515e07e",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1914,7 +1833,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7737fdd1-0875-47e6-977b-12561cddfdc3",
|
||||
"id": "f55ded54-683a-4f5a-a101-9cfbd7b96781",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1941,7 +1860,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "90f975c3-9826-461f-88ca-27c697aff86b",
|
||||
"id": "931d5a82-378f-4533-8c69-2239a4acd047",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1967,7 +1886,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ce2722d5-9f4f-41a2-8f81-e01f7b6cee57",
|
||||
"id": "22b05374-f480-4ca8-aca8-9db8b6dd1729",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -1993,7 +1912,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "31b5bfa7-98ad-47a2-b8e6-0669022cd8cb",
|
||||
"id": "c0371832-e4b7-485e-bf23-6babe4c6ac83",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2012,7 +1931,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bf8a950b-be3b-4e44-8602-64e0bba492eb",
|
||||
"id": "4d0445da-073e-465e-b25b-af522915c73f",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2054,7 +1973,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e3519800-971b-4b1d-b64e-3983ccd02dea",
|
||||
"id": "740d467f-4203-425b-8203-9bfd3eed25ae",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2096,7 +2015,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9d5a33a2-e777-4beb-95de-b84812f69c56",
|
||||
"id": "cf1a9af9-dadd-4cb9-a26e-fbbba216f8e1",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2116,14 +2035,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "4901c91d-59bd-4727-b585-8e4e44828d0a",
|
||||
"id": "4e65eb4b-9f0a-4ab8-98b2-6daf50cd1bf8",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "5062a078-83a7-4933-b0d5-3f75cc2a5003",
|
||||
"id": "5e8dc1c5-1489-4d39-bb75-9c499583b91b",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
||||
@ -2213,8 +2132,8 @@
|
||||
"attributes": {
|
||||
"cibaBackchannelTokenDeliveryMode": "poll",
|
||||
"cibaAuthRequestedUserHint": "login_hint",
|
||||
"clientOfflineSessionMaxLifespan": "0",
|
||||
"oauth2DevicePollingInterval": "5",
|
||||
"clientOfflineSessionMaxLifespan": "0",
|
||||
"clientSessionIdleTimeout": "0",
|
||||
"userProfileEnabled": "true",
|
||||
"clientOfflineSessionIdleTimeout": "0",
|
@ -73,7 +73,7 @@
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"client": {
|
||||
"account": ["view-profile", "manage-account", "delete-account"]
|
||||
"account": ["delete-account", "view-profile", "manage-account"]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@ -435,46 +435,13 @@
|
||||
"type": "password",
|
||||
"userLabel": "My password",
|
||||
"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\":{}}"
|
||||
}
|
||||
],
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"create-client",
|
||||
"view-identity-providers",
|
||||
"manage-realm",
|
||||
"query-groups",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"realm-admin",
|
||||
"view-authorization",
|
||||
"view-events",
|
||||
"view-clients",
|
||||
"view-realm",
|
||||
"manage-events",
|
||||
"query-realms",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"manage-users",
|
||||
"view-users",
|
||||
"impersonation",
|
||||
"manage-authorization"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"view-applications",
|
||||
"manage-consent",
|
||||
"delete-account",
|
||||
"manage-account",
|
||||
"view-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -540,12 +507,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -680,6 +643,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"display.on.consent.screen": "false",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
@ -740,12 +704,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -764,24 +724,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"introspection.token.claim": "true",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||
"name": "locale",
|
||||
@ -1342,11 +1284,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1362,14 +1304,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1419,13 +1361,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper"
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1543,7 +1485,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "8ccfe057-5ce6-499b-9fae-3cd89b62bf01",
|
||||
"id": "e134634e-f219-4df4-867c-8110688d8e56",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1569,7 +1511,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f3b9ab2e-41c2-4e73-876b-e2c275d6d14e",
|
||||
"id": "a611a8eb-9626-4aa4-8b54-ee565ea6e5dc",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1603,7 +1545,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "df1329cc-777c-42d8-aa2f-c5d5ddaaf5a4",
|
||||
"id": "d87cbb31-5c69-45c8-888d-f9649ebbbf97",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1629,7 +1571,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f78a4cbc-66ff-4caa-8066-67aff94946f4",
|
||||
"id": "752ba282-a369-4592-92e8-b4287192dbbf",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1655,7 +1597,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "4b20995b-5553-45db-86b0-05c3fe14edb1",
|
||||
"id": "2349282e-40ff-431a-984d-53911511e3d3",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1681,7 +1623,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0a7cc6b7-e427-4f72-b44e-a02133241bad",
|
||||
"id": "4ff5463d-26d9-4219-ba85-41464401098f",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1707,7 +1649,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e24e73c0-dd51-4fdc-a916-284f11f38487",
|
||||
"id": "87bb6c6d-cca8-4832-b5ab-67ecb9454a42",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1733,7 +1675,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "37ee5a12-01c2-41b0-aafa-e9c6661ff544",
|
||||
"id": "1fc3d028-0e0a-43a4-aaf9-ba7f7d60b409",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1760,7 +1702,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "8902a1a7-c2ee-4648-869f-dd5ef89184fc",
|
||||
"id": "036aae59-641f-4799-9124-c7e5034af6c1",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1786,7 +1728,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77c78eed-4bcd-4779-b39f-10135be84946",
|
||||
"id": "2e8b9f28-93b8-4368-84b0-1a8326daafe0",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1828,7 +1770,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "c6398883-01e6-47a1-bb97-c09f2983155d",
|
||||
"id": "0b826105-8493-45ce-87b3-7d917d190b39",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1870,7 +1812,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "78ab5fb8-f35b-4053-b264-94b208000b13",
|
||||
"id": "bf6d9edd-48d8-4392-bbc8-4b17a6866074",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1904,7 +1846,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "959e154b-034e-413d-9b19-211e7d9ba33d",
|
||||
"id": "97e31722-dd11-42be-aa99-88788fa2dde6",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1922,7 +1864,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "001e253d-bdbd-41e2-81c7-1c7b239feeb1",
|
||||
"id": "3f45cf34-231f-4ea1-8e58-d636c451a76b",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1949,7 +1891,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "45481bb0-18fe-4a26-a77c-35a5afe58436",
|
||||
"id": "9bef2f7c-f989-4871-aaa7-18e2cfa73f22",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1975,7 +1917,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bb47b847-5a55-4c08-909e-9f6f8d8a0636",
|
||||
"id": "0bfaa325-acde-4443-8bd8-1dc2ae759c5f",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -2001,7 +1943,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77e6e169-05b7-4b89-af00-09cfe1604eed",
|
||||
"id": "37ddbe8c-abf3-4654-bd6d-ffabbeefbb98",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2020,7 +1962,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "aef03fe8-1a70-40c3-879f-25588f75c119",
|
||||
"id": "5d7b4bc9-e93b-40da-aeb6-ba0c38392f1a",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2062,7 +2004,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "990abff7-e2ba-4217-984e-8890cbc2b3a9",
|
||||
"id": "ee7a56e4-c827-4f24-8b8b-8476050b0b64",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2104,7 +2046,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d9894cf6-2f99-493e-ac47-853f54bfc9c6",
|
||||
"id": "360f0031-4c3b-4272-84ca-2172d430b4bc",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2124,14 +2066,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "101ed8ff-4383-4539-aa52-2d1e69698b78",
|
||||
"id": "53630acd-a33a-40e3-8786-cf85464c6f9e",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "049042a5-3551-4c16-81a1-64d86f5aa1e5",
|
||||
"id": "c0d2b6a0-caad-4e90-b040-17cacdaf70bb",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
@ -73,7 +73,7 @@
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"client": {
|
||||
"account": ["view-profile", "manage-account", "delete-account"]
|
||||
"account": ["delete-account", "view-profile", "manage-account"]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@ -407,7 +407,7 @@
|
||||
"otpPolicyLookAheadWindow": 1,
|
||||
"otpPolicyPeriod": 30,
|
||||
"otpPolicyCodeReusable": false,
|
||||
"otpSupportedApplications": ["totpAppFreeOTPName", "totpAppGoogleName"],
|
||||
"otpSupportedApplications": ["totpAppGoogleName", "totpAppFreeOTPName"],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
"webAuthnPolicyRpId": "",
|
||||
@ -452,40 +452,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"create-client",
|
||||
"view-identity-providers",
|
||||
"manage-realm",
|
||||
"query-groups",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"realm-admin",
|
||||
"view-authorization",
|
||||
"view-events",
|
||||
"view-clients",
|
||||
"view-realm",
|
||||
"manage-events",
|
||||
"query-realms",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"manage-users",
|
||||
"view-users",
|
||||
"impersonation",
|
||||
"manage-authorization"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"view-applications",
|
||||
"manage-consent",
|
||||
"delete-account",
|
||||
"manage-account",
|
||||
"view-groups",
|
||||
"view-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -551,12 +517,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -691,6 +653,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"display.on.consent.screen": "false",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
@ -751,12 +714,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -775,24 +734,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"introspection.token.claim": "true",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||
"name": "locale",
|
||||
@ -1353,11 +1294,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1373,14 +1314,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper"
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1429,14 +1370,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper"
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1554,7 +1495,7 @@
|
||||
"defaultLocale": "en",
|
||||
"authenticationFlows": [
|
||||
{
|
||||
"id": "30a878f0-57aa-4d20-bab0-6cf1d7317a5c",
|
||||
"id": "19317acb-fe8e-4c79-82bc-90e159273075",
|
||||
"alias": "Account verification options",
|
||||
"description": "Method with which to verity the existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1580,7 +1521,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d386affe-d1fe-472a-bee6-54105d0101f5",
|
||||
"id": "122857d2-33da-4086-8acb-cb0e303aaf1b",
|
||||
"alias": "Authentication Options",
|
||||
"description": "Authentication options.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1614,7 +1555,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "77b95bc0-bd0c-46b7-8240-3182023e9d50",
|
||||
"id": "abf5dd35-4791-4268-a10c-5f4b6a06b84a",
|
||||
"alias": "Browser - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1640,7 +1581,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bc96d3d6-29a1-42af-a63e-bb67a8c6d78f",
|
||||
"id": "a18daeec-a33c-4a43-b014-10c84ec69b81",
|
||||
"alias": "Direct Grant - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1666,7 +1607,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "7697ca74-5c2b-45ab-9335-e0f6dec59b5c",
|
||||
"id": "e9f032a7-32f7-457c-becf-011a1a35cc6a",
|
||||
"alias": "First broker login - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP is required for the authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1692,7 +1633,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "534cb120-f600-4f40-9707-7b781bdbce48",
|
||||
"id": "9db65b7c-98ca-4003-beea-611038831ffe",
|
||||
"alias": "Handle Existing Account",
|
||||
"description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
|
||||
"providerId": "basic-flow",
|
||||
@ -1718,7 +1659,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f884b048-b223-4ed6-ae16-e49a4255131e",
|
||||
"id": "7bd0854c-d7ae-43d7-a1ae-7b759a34cb1d",
|
||||
"alias": "Reset - Conditional OTP",
|
||||
"description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1744,7 +1685,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "61c7966c-ad72-49f5-84dd-376152348092",
|
||||
"id": "2de1a450-fe98-443a-9c6c-d24d8a7ebcb3",
|
||||
"alias": "User creation or linking",
|
||||
"description": "Flow for the existing/non-existing user alternatives",
|
||||
"providerId": "basic-flow",
|
||||
@ -1771,7 +1712,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "72412d0f-dd1b-49fe-bb0b-9dad99eb0491",
|
||||
"id": "7b3efad5-4b7d-4385-a41c-fecc73afdcc4",
|
||||
"alias": "Verify Existing Account by Re-authentication",
|
||||
"description": "Reauthentication of existing account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1797,7 +1738,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6b76613e-0d39-440d-aab4-98eaffb1e96a",
|
||||
"id": "de93418e-8f28-4099-b15e-ad36ec194796",
|
||||
"alias": "browser",
|
||||
"description": "browser based authentication",
|
||||
"providerId": "basic-flow",
|
||||
@ -1839,7 +1780,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "0ff60395-fa89-41be-ad22-fab339e67c49",
|
||||
"id": "0dd3345c-6e82-4c3a-a39a-d49ae1f5c409",
|
||||
"alias": "clients",
|
||||
"description": "Base authentication for clients",
|
||||
"providerId": "client-flow",
|
||||
@ -1881,7 +1822,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "bbb3ece7-7dbf-4aba-80c3-dde4b9cdd0b6",
|
||||
"id": "87fb4dd0-5326-47a1-b670-982f4872ff89",
|
||||
"alias": "direct grant",
|
||||
"description": "OpenID Connect Resource Owner Grant",
|
||||
"providerId": "basic-flow",
|
||||
@ -1915,7 +1856,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "f5f2c0f6-7dbf-4978-845e-6cacac23aa13",
|
||||
"id": "344723b3-4ab1-4999-abdd-32398e82327b",
|
||||
"alias": "docker auth",
|
||||
"description": "Used by Docker clients to authenticate against the IDP",
|
||||
"providerId": "basic-flow",
|
||||
@ -1933,7 +1874,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cf463104-19e2-41a8-8a53-d3dd30b75344",
|
||||
"id": "f3341938-caf9-4c8a-9cd5-eb34609809ab",
|
||||
"alias": "first broker login",
|
||||
"description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
|
||||
"providerId": "basic-flow",
|
||||
@ -1960,7 +1901,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "b99b60dc-41ad-487d-be69-a2eefa954a9d",
|
||||
"id": "ba7b7357-e324-4b71-9bda-f8512a760e02",
|
||||
"alias": "forms",
|
||||
"description": "Username, password, otp and other auth forms.",
|
||||
"providerId": "basic-flow",
|
||||
@ -1986,7 +1927,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "18731296-2c96-4f98-a884-027e629e4f9d",
|
||||
"id": "134971e6-bf63-432c-806e-74ca4fb09963",
|
||||
"alias": "http challenge",
|
||||
"description": "An authentication flow based on challenge-response HTTP Authentication Schemes",
|
||||
"providerId": "basic-flow",
|
||||
@ -2012,7 +1953,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "9a9dce17-5425-4fd5-b3b8-81410e1dbce4",
|
||||
"id": "6ea9e2cf-5684-4c65-8c07-930d1cbb0b46",
|
||||
"alias": "registration",
|
||||
"description": "registration flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2031,7 +1972,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "d0a24e08-cb69-4949-9518-50ae7a96ee49",
|
||||
"id": "67e3c8c7-1b5e-4119-84a2-e90876293150",
|
||||
"alias": "registration form",
|
||||
"description": "registration form",
|
||||
"providerId": "form-flow",
|
||||
@ -2073,7 +2014,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "6a9aa554-afba-487f-9c82-e94c81c15b3b",
|
||||
"id": "fc6d48ec-a1f1-41b1-9310-54f58861d5aa",
|
||||
"alias": "reset credentials",
|
||||
"description": "Reset credentials for a user if they forgot their password or something",
|
||||
"providerId": "basic-flow",
|
||||
@ -2115,7 +2056,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "e0361d46-eab4-41a6-bb2e-1dc6a5a6b073",
|
||||
"id": "80b1d464-c2ec-4eb1-82e8-32cbede779a8",
|
||||
"alias": "saml ecp",
|
||||
"description": "SAML ECP Profile Authentication Flow",
|
||||
"providerId": "basic-flow",
|
||||
@ -2135,14 +2076,14 @@
|
||||
],
|
||||
"authenticatorConfig": [
|
||||
{
|
||||
"id": "053d6017-e54c-418a-abe7-44dd4752eacb",
|
||||
"id": "86b1d5fa-450c-40d8-899c-725861ac39fc",
|
||||
"alias": "create unique user config",
|
||||
"config": {
|
||||
"require.password.update.after.registration": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8b545cf4-ab9e-4226-b3c0-d7ac773eae2f",
|
||||
"id": "ea724f02-029a-493d-b4d3-08972be21cfb",
|
||||
"alias": "review profile config",
|
||||
"config": {
|
||||
"update.profile.on.first.login": "missing"
|
@ -73,7 +73,7 @@
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"client": {
|
||||
"account": ["view-profile", "manage-account", "delete-account"]
|
||||
"account": ["delete-account", "view-profile", "manage-account"]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@ -408,9 +408,9 @@
|
||||
"otpPolicyPeriod": 30,
|
||||
"otpPolicyCodeReusable": false,
|
||||
"otpSupportedApplications": [
|
||||
"totpAppGoogleName",
|
||||
"totpAppFreeOTPName",
|
||||
"totpAppMicrosoftAuthenticatorName",
|
||||
"totpAppGoogleName"
|
||||
"totpAppMicrosoftAuthenticatorName"
|
||||
],
|
||||
"webAuthnPolicyRpEntityName": "keycloak",
|
||||
"webAuthnPolicySignatureAlgorithms": ["ES256"],
|
||||
@ -456,40 +456,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"create-client",
|
||||
"view-identity-providers",
|
||||
"manage-realm",
|
||||
"query-groups",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"realm-admin",
|
||||
"view-authorization",
|
||||
"view-events",
|
||||
"view-clients",
|
||||
"view-realm",
|
||||
"manage-events",
|
||||
"query-realms",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"manage-users",
|
||||
"view-users",
|
||||
"impersonation",
|
||||
"manage-authorization"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"view-applications",
|
||||
"manage-consent",
|
||||
"delete-account",
|
||||
"manage-account",
|
||||
"view-groups",
|
||||
"view-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -555,12 +521,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -695,6 +657,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"display.on.consent.screen": "false",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
@ -755,12 +718,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -779,24 +738,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"introspection.token.claim": "true",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "7779f8fa-c2fe-4e68-be56-66ee97bf8f13",
|
||||
"name": "locale",
|
||||
@ -1357,11 +1298,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1377,13 +1318,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper"
|
||||
]
|
||||
}
|
||||
@ -1433,14 +1374,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
@ -55,7 +55,7 @@
|
||||
"composites": {
|
||||
"realm": ["offline_access", "uma_authorization"],
|
||||
"client": {
|
||||
"account": ["view-profile", "delete-account", "manage-account"]
|
||||
"account": ["delete-account", "view-profile", "manage-account"]
|
||||
}
|
||||
},
|
||||
"clientRole": false,
|
||||
@ -459,40 +459,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"create-client",
|
||||
"view-users",
|
||||
"query-groups",
|
||||
"view-realm",
|
||||
"manage-authorization",
|
||||
"view-authorization",
|
||||
"query-users",
|
||||
"impersonation",
|
||||
"realm-admin",
|
||||
"manage-users",
|
||||
"view-identity-providers",
|
||||
"manage-realm",
|
||||
"manage-clients",
|
||||
"query-realms",
|
||||
"view-events",
|
||||
"manage-events",
|
||||
"view-clients"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"manage-account",
|
||||
"view-consent",
|
||||
"view-groups",
|
||||
"delete-account",
|
||||
"view-applications",
|
||||
"manage-account-links",
|
||||
"view-profile",
|
||||
"manage-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -539,6 +505,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
@ -565,12 +532,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -686,11 +649,7 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"https://my-theme.keycloakify.dev/*",
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*"
|
||||
],
|
||||
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
|
||||
"webOrigins": ["*"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
@ -705,7 +664,8 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"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",
|
||||
"display.on.consent.screen": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
@ -765,12 +725,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -789,24 +745,6 @@
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "59cde7ae-2218-4a8e-83af-cad992c3a700",
|
||||
"name": "locale",
|
||||
@ -1398,12 +1336,12 @@
|
||||
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"loginTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1420,13 +1358,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper"
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1495,14 +1433,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
}
|
@ -468,40 +468,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"manage-clients",
|
||||
"manage-users",
|
||||
"view-identity-providers",
|
||||
"view-users",
|
||||
"impersonation",
|
||||
"manage-identity-providers",
|
||||
"query-users",
|
||||
"query-realms",
|
||||
"realm-admin",
|
||||
"view-events",
|
||||
"view-realm",
|
||||
"manage-events",
|
||||
"manage-authorization",
|
||||
"manage-realm",
|
||||
"query-clients",
|
||||
"query-groups",
|
||||
"view-clients",
|
||||
"create-client",
|
||||
"view-authorization"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"manage-consent",
|
||||
"manage-account-links",
|
||||
"view-applications",
|
||||
"view-consent",
|
||||
"manage-account",
|
||||
"view-profile",
|
||||
"view-groups",
|
||||
"delete-account"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -548,6 +514,7 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"login_theme": "keycloakify-starter",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
@ -574,12 +541,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -695,11 +658,7 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"https://my-theme.keycloakify.dev/*",
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*"
|
||||
],
|
||||
"redirectUris": ["https://my-theme.keycloakify.dev/*", "http://localhost*"],
|
||||
"webOrigins": ["*"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
@ -714,7 +673,8 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"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",
|
||||
"display.on.consent.screen": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
@ -880,12 +840,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -919,24 +875,6 @@
|
||||
"claim.name": "locale",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"],
|
||||
@ -1513,12 +1451,12 @@
|
||||
"strictTransportSecurity": "max-age=31536000; includeSubDomains"
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"loginTheme": "keycloak",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1563,14 +1501,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1603,13 +1541,13 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper"
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
]
|
||||
}
|
||||
},
|
@ -538,10 +538,10 @@
|
||||
"emailVerified": true,
|
||||
"attributes": {
|
||||
"additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
|
||||
"favorite_pet": ["cats"],
|
||||
"gender": ["prefer_not_to_say"],
|
||||
"bio": ["Hello I'm Test User and I do not exist."],
|
||||
"favorite_pet": ["cats"],
|
||||
"favourite_pet": ["cat"],
|
||||
"bio": ["Hello I'm Test User and I do not exist."],
|
||||
"phone_number": ["1111111111"],
|
||||
"locale": ["en"],
|
||||
"favorite_media": ["movies", "series"]
|
||||
@ -562,40 +562,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"manage-users",
|
||||
"create-client",
|
||||
"view-users",
|
||||
"view-realm",
|
||||
"query-realms",
|
||||
"impersonation",
|
||||
"view-events",
|
||||
"realm-admin",
|
||||
"manage-authorization",
|
||||
"manage-events",
|
||||
"view-authorization",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"query-groups",
|
||||
"manage-realm",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"view-clients",
|
||||
"view-identity-providers"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"delete-account",
|
||||
"view-applications",
|
||||
"manage-account",
|
||||
"view-consent",
|
||||
"view-groups",
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"manage-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -662,16 +628,14 @@
|
||||
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
|
||||
"clientId": "account-console",
|
||||
"name": "${client_account-console}",
|
||||
"description": "",
|
||||
"rootUrl": "${authBaseUrl}",
|
||||
"adminUrl": "",
|
||||
"baseUrl": "/realms/myrealm/account/",
|
||||
"surrogateAuthRequired": false,
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": ["http://localhost*", "http://127.0.0.1*", "*"],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -683,13 +647,8 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"pkce.code.challenge.method": "S256",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
"pkce.code.challenge.method": "S256"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
@ -832,7 +791,8 @@
|
||||
"attributes": {
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"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",
|
||||
"display.on.consent.screen": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
@ -925,12 +885,8 @@
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -964,24 +920,6 @@
|
||||
"claim.name": "locale",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false",
|
||||
"claim.value": "[\"*\"]",
|
||||
"lightweight.claim": "true"
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": [
|
||||
@ -1606,11 +1544,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1636,14 +1574,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1673,14 +1611,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1734,10 +1672,11 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEowIBAAKCAQEAso89qpvLhf9DIcCb2JAbxItRLSIvP/NCZhMdAExTHyrhM5B27ZQ6MZ7dJQbnMu7QJ7yiClsD1XnDN7Wlj07sY2As3lY3v9kjODBeADYlPuN1m7/fXFHX3qfRT+PwVSaAhMykmqvWp86UTg7t7rNjVBnXPPXItmRLIF+jZUMWQduwNznr6Jh54ZdIwEy4hvX1bpNw0nPl4KXiOi2elvg+rk7BhFywGwQ/HUCGkrcq0XS/aNOy1ChmqDbtq817mYpVeteCDe8xP3MPrZ/s2LiEt4Ip1cNo0dY+a4JwOzwL42h3GaR+80iK3pZNo+Mr0KBOY9GXvdV/MvcPHLQ7VujUGQIDAQABAoIBAAHV0OQwmDxUazqiVGe61Bzmcqs5q03SC1K/FmCi/YVikdskvGLaOmk5UQa4+1uDEq7J30onH9ML8+qeFRQek0rn2ZDfxtBpDqsx7LwTUmQtqc8z6buKQs37db5ctnhlk34UmAotQyDz5wMmCkzWWVUWCT02PdMev5qW/mKuIxaCWLHUFiMJaGrYCCwB/Ra8KLcadKgRbytSUth9qILC4krFfmWtzIx1P6nM1pzQ1nydxNnNPJKjoWtLRJ5b701Y5/h2vAAg6Mr+jKe1DPa9QmAqhQudjGbZ31av+0f1/I+XkflpZfokfU+MrAqNYRTYkevRYgc3wakK5mfVYUiMuOECgYEA7fk55O2OJFsR0Vjy4Dx4eSIwgwobvwEuHxlyWn0RC7nFb00eh6OPuc5sHrOk8bK3P367q67sEhxGyBF16nwxgX/T+c8gTC8QRuwNymosA4Je/zJHbKvyzLGOouCP5gYwq/wUmVWzNApVC7LBfxbsqYyivHABc5xgPmTgecY0VWkCgYEAwBXcUKoyq1KZegyNJcTuwuvBXoYVveFGm6QKKKwzojCCKaR3XXtdSon1qYfuKT0MLxgEDyyBks9DgfCodSsTmajX90Yolhyz3ptcOmRURqTRoJhM4g6qA+Ybd3uy8vAz32RdS+4rCTgnMG/5Xpn5B4ojOnhRcnA2TPCJgWz6QzECgYEAhj1FjD75JMb+mRJNB3L1HpfLt8+28RsQUli/ag4M1Il5txxQsYDxbYXk9biuvezrc/Tglqs43cp3nxpCYwClyIA8KjnN5UvTKb601M7pfx1GyzwokEO61f7/ECAO7FnnkMzFLe3rBdsiOFQg1LkwzT/Y+OVR3E6E+A1dlzPYh6kCgYBIP3CwfnO0cMr9Vv8394x+kEIZFYHT+4mdPOP9TFfXZztuAkhLRv1d7eoSq+fuZuHQTM4qDullmMOhei1CdMNYhmNExIS7gWw+DF1yMQ5py9B1ARPZ6v4TnVczZ7l1GtfH7G4TAy/4tcA3vcYjyPIb3d9GPL8VthMWeVqe7ahr4QKBgEwA7ASbs4NxfBsStEGQYQYAeWOoKnTc50FeYz38O4KrOirtTFPNsJcyCiTE0o4cqu/OebSA5irrauV7SEDl/gfH54g3ZWusQbLt2uMnZYtkd2+Ka3T9XM0QfQW/vYl3eJtdQj89TqzLzyP0AgvAyIgeG3RMH8ojqCh3YKY0FTv/"
|
||||
"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": [
|
||||
"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"]
|
||||
}
|
||||
@ -1749,10 +1688,11 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEowIBAAKCAQEAxoEvnv+YHCqUWANGuku5QYscAZyUE0WHSlcAzZ0bQugPow63piQsuxPz0cpPIuLab6adssXUqKEFheT1H0BqtmT9L/7iOKB6MRuInN4aRzzTH9q02TKPkcpSAzAHTGcsJBMMawlbnIdMu5+mevMPxqeVVxvrnKG27S8H3W5jqIkQw8bo646Hr3l5Dxq/jY7slcSXXXe4ZdefeCvnSqea+fy5c+r/r546nX4FTGiklu6KLQaDc9SfGccrZDmljY7DX1kHrmvIdLShcuukTHc0hi2qbgMcUte/7/svSJLUWOZObKxetd4y1OA49v36xrMqGhwGDdwrWf0VuMBN8eHOCQIDAQABAoIBABz/hUXnFRZURWHKxLvKpnBZPTOiZzfzfxfl4tOmq54CtDoVQyXNq2J+6oOPWC/X+ky3hy+1BQ5x9hJrx+qTU04m2EfOe8da8M7DX28kZlauyjF2loG+MvP7ctn4BluWcip+RTZOYn2DfxBPpRcunR409V+JesoMY7fSwtrfA/Gm0PrXgBK7OuE0nxqFFWnsLOc+HxZECS5r0n1MHEBHe774HkqGcK91j8S+QU+/diTnK+N/ClnKWnabMK8bUO5wAUuKwf2deYkGP91pCEJlVnVZyaXshEM+uxTuMRUlq9h1QAIUatvdQwfOKqZ9XvmTVC8b79qLwmezjoDxNCKbaMMCgYEA71WDpMnA2uS2wCJ/MVwzWGSBDjfeKUPRy33BeUfwLGp4Dro+S1sTrLHgi1HGmvmC8ReZrifUlUHUi3ZHauR6vbNsEoSQ3hplO013kj12EfcBpvKYFg1ODCwevb/JtBTWbDG1P+E9DGiF/2u0aicoJoPolNeNVzgO6YK1OI/S/LMCgYEA1FPTqFPulXxcOK12LgYap8typqJ7zu4fByr42010yrKM+LLNA3bT/i/oRkKc7J1ztKSqlVckADWgK4Y27lI4j1tSgTOxFzwxnTZOeF7ZwGSxq9iy9A84nDiW+m6Hj5RDyBjTSoP2Qqv6d5kTUx+pczZvOVTWRlIEnFETbbxOoFMCgYEA0r1etHx+V4AqtxXpH6KLB5s/1DA3a+hu1BrAgLVqcwGxA27VKW9h7J+YE7UHBzELLpVUWfhyhJa5u6+DhUj4Fw/k6o1WLmvZlZVJ4zhBPeJczw8wAcLnZWp4CybUScBLamt+qGgBZGqpCtZgv1QJU5i09FK0/wa6grz4K3zhEGcCgYAlnGe8xIlZr3rCi2+IvYoROQepHtUhlaqnYWRNrI3IrhIsp7eLKoxo1WGmuHwFqepqEFUrORFmfBlQPGkUlDnyovGdc2OmQwJi39DMn7igzPVwBGXGt7+GZLvRxqx6sX/EPSmIZJHFw6MNdm8m5U/l2bmgBTgjormwWug/IwEmgwKBgEouISIuXsjGxeLmhrOXHKXb6IfKglNJeBM6lTQ6MLaVOso7KdelIntwZNtZwMIi3hlwaUb1X1QmztFbnrvnPhWwJR4ZgMEWanRHthtm0SHzg8EHKT40S91oKabsgHk3wpOvq/iWs+k8qWN4HYp6UO603uLMOfxPYJCFxRtg2TsJ"
|
||||
"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": [
|
||||
"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"],
|
||||
"algorithm": ["RSA-OAEP"]
|
||||
@ -1764,8 +1704,8 @@
|
||||
"providerId": "aes-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["95db7eb8-b57b-475e-90cd-58841a9388d3"],
|
||||
"secret": ["dp6bv53YrC2PZuJCxa3aNA"],
|
||||
"kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
|
||||
"secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
|
||||
"priority": ["100"]
|
||||
}
|
||||
},
|
||||
@ -1775,9 +1715,9 @@
|
||||
"providerId": "hmac-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["d0254883-059e-4fdd-bf03-704c76650aab"],
|
||||
"kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
|
||||
"secret": [
|
||||
"bcW7E4rcbgSKZIQysWOSuhezRGYs5Kzmp3ZESthdTUMyFivK8RbBAdBE4PhFPk5B9TuByDO2RWvd8F7F5YhGJitf6cfYB1BfDuAk-2iBAtdZA98g7a2h4jpwzh-GIgtoRbGbH9qnquUn52f5qteo34g5WifKE2bWjOELza9FrTo"
|
||||
"j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
|
||||
],
|
||||
"priority": ["100"],
|
||||
"algorithm": ["HS512"]
|
||||
@ -2448,7 +2388,7 @@
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"organizationsEnabled": "false"
|
||||
},
|
||||
"keycloakVersion": "25.0.6",
|
||||
"keycloakVersion": "25.0.0",
|
||||
"userManagedAccessAllowed": false,
|
||||
"organizationsEnabled": false,
|
||||
"clientProfiles": {
|
@ -38,7 +38,6 @@
|
||||
"bruteForceProtected": false,
|
||||
"permanentLockout": false,
|
||||
"maxTemporaryLockouts": 0,
|
||||
"bruteForceStrategy": "MULTIPLE",
|
||||
"maxFailureWaitSeconds": 900,
|
||||
"minimumQuickLoginWaitSeconds": 60,
|
||||
"waitIncrementSeconds": 60,
|
||||
@ -563,40 +562,6 @@
|
||||
"disableableCredentialTypes": [],
|
||||
"requiredActions": [],
|
||||
"realmRoles": ["default-roles-myrealm"],
|
||||
"clientRoles": {
|
||||
"realm-management": [
|
||||
"manage-users",
|
||||
"create-client",
|
||||
"view-users",
|
||||
"view-realm",
|
||||
"query-realms",
|
||||
"impersonation",
|
||||
"view-events",
|
||||
"realm-admin",
|
||||
"manage-authorization",
|
||||
"view-authorization",
|
||||
"manage-events",
|
||||
"manage-clients",
|
||||
"query-users",
|
||||
"query-groups",
|
||||
"manage-realm",
|
||||
"query-clients",
|
||||
"manage-identity-providers",
|
||||
"view-identity-providers",
|
||||
"view-clients"
|
||||
],
|
||||
"broker": ["read-token"],
|
||||
"account": [
|
||||
"delete-account",
|
||||
"view-applications",
|
||||
"manage-account",
|
||||
"view-consent",
|
||||
"view-groups",
|
||||
"view-profile",
|
||||
"manage-account-links",
|
||||
"manage-consent"
|
||||
]
|
||||
},
|
||||
"notBefore": 0,
|
||||
"groups": []
|
||||
}
|
||||
@ -639,7 +604,6 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
@ -664,20 +628,14 @@
|
||||
"id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
|
||||
"clientId": "account-console",
|
||||
"name": "${client_account-console}",
|
||||
"description": "",
|
||||
"rootUrl": "${authBaseUrl}",
|
||||
"adminUrl": "",
|
||||
"baseUrl": "/realms/myrealm/account/",
|
||||
"surrogateAuthRequired": false,
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/realms/myrealm/account/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/realms/myrealm/account/*"],
|
||||
"webOrigins": [],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -689,14 +647,8 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"pkce.code.challenge.method": "S256",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
"pkce.code.challenge.method": "S256"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": false,
|
||||
@ -747,12 +699,10 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"client.use.lightweight.access.token.enabled": "true",
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"defaultClientScopes": [
|
||||
"web-origins",
|
||||
@ -790,7 +740,6 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "true",
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
@ -840,10 +789,10 @@
|
||||
"frontchannelLogout": true,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"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",
|
||||
"display.on.consent.screen": "false",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
@ -906,7 +855,6 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "true",
|
||||
"post.logout.redirect.uris": "+"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
@ -931,20 +879,14 @@
|
||||
"id": "fce8a109-6f32-4814-9a20-2ff2435d2da6",
|
||||
"clientId": "security-admin-console",
|
||||
"name": "${client_security-admin-console}",
|
||||
"description": "",
|
||||
"rootUrl": "${authAdminUrl}",
|
||||
"adminUrl": "",
|
||||
"baseUrl": "/admin/myrealm/console/",
|
||||
"surrogateAuthRequired": false,
|
||||
"enabled": true,
|
||||
"alwaysDisplayInConsole": false,
|
||||
"clientAuthenticatorType": "client-secret",
|
||||
"redirectUris": [
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*",
|
||||
"/admin/myrealm/console/*"
|
||||
],
|
||||
"webOrigins": ["*"],
|
||||
"redirectUris": ["/admin/myrealm/console/*"],
|
||||
"webOrigins": ["+"],
|
||||
"notBefore": 0,
|
||||
"bearerOnly": false,
|
||||
"consentRequired": false,
|
||||
@ -956,18 +898,11 @@
|
||||
"frontchannelLogout": false,
|
||||
"protocol": "openid-connect",
|
||||
"attributes": {
|
||||
"realm_client": "false",
|
||||
"oidc.ciba.grant.enabled": "false",
|
||||
"client.use.lightweight.access.token.enabled": "true",
|
||||
"backchannel.logout.session.required": "true",
|
||||
"post.logout.redirect.uris": "+",
|
||||
"oauth2.device.authorization.grant.enabled": "false",
|
||||
"display.on.consent.screen": "false",
|
||||
"pkce.code.challenge.method": "S256",
|
||||
"backchannel.logout.revoke.offline.tokens": "false"
|
||||
"pkce.code.challenge.method": "S256"
|
||||
},
|
||||
"authenticationFlowBindingOverrides": {},
|
||||
"fullScopeAllowed": true,
|
||||
"fullScopeAllowed": false,
|
||||
"nodeReRegistrationTimeout": 0,
|
||||
"protocolMappers": [
|
||||
{
|
||||
@ -985,24 +920,6 @@
|
||||
"claim.name": "locale",
|
||||
"jsonType.label": "String"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
"name": "allowed-origins",
|
||||
"protocol": "openid-connect",
|
||||
"protocolMapper": "oidc-hardcoded-claim-mapper",
|
||||
"consentRequired": false,
|
||||
"config": {
|
||||
"introspection.token.claim": "true",
|
||||
"claim.value": "[\"*\"]",
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"lightweight.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false"
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultClientScopes": [
|
||||
@ -1627,11 +1544,11 @@
|
||||
},
|
||||
"smtpServer": {},
|
||||
"loginTheme": "keycloakify-starter",
|
||||
"accountTheme": "",
|
||||
"adminTheme": "keycloakify-starter",
|
||||
"accountTheme": "keycloakify-starter",
|
||||
"adminTheme": "",
|
||||
"emailTheme": "",
|
||||
"eventsEnabled": false,
|
||||
"eventsListeners": ["keycloakify-logging", "jboss-logging"],
|
||||
"eventsListeners": ["jboss-logging"],
|
||||
"enabledEventTypes": [],
|
||||
"adminEventsEnabled": false,
|
||||
"adminEventsDetailsEnabled": false,
|
||||
@ -1657,14 +1574,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper"
|
||||
"oidc-sha256-pairwise-sub-mapper"
|
||||
]
|
||||
}
|
||||
},
|
||||
@ -1694,14 +1611,14 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper",
|
||||
"saml-user-attribute-mapper"
|
||||
"oidc-address-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": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEoQIBAAKCAQEAxTFMvRiNiQjY9zajvLsah6Vy4pn8U7smsnBcHS9SkLJ1j9O8+90B90tIZk4IqEE4gdJA/mbbeUnou1vWuc0k69diQMFelzdIaDqJaFFeOS+J1DoApjThjGIz7FIgmGi6qoN8xnrPVD/6oMYAuxTvQaJH7mENiIG0198dvaufV1mFPg+krTsh7Womo2CJeZmNuAXv7RDQYxwPYDCFZLbppez48D7+2D+1V6Stk6Xwz8IDQZvljxDF6W2P9rhPWV1C5tcJpC/9RPyGDo+ke8UN3fM6X7YOgpbMztVrg8J0aTqPXZ7dt6QFUqVOufo+5wYL2jCafpYNV8cmaGlY+Q3d5QIDAQABAoH/DIPcaZaJTLG4FeUKGOaT40nesEiINRY99aeIkp+hdGj1EgTEn49TyLENGnhrrdbIvOJDeD6Z6dbpJBDvfFevxa589EnVKaGaaW5U91FDyVYH2YPU411dAeOp0z1xwxXzlJqX3h42ZJnvLAp/2l1Xo64vGCoTJtYlppAvpe2MjANxPNObAc65Phdi/sConAlwMeBylWXJ574uryFrJ64W/sUuIUMSunGGz0db4Y1hfkX9U2YnxB3DdXCBH09jQJyKDSj6feNXR87+1KhqcFMd5DUiGSAOqRBzuBMsDf1QDJd8A/DDlK7e/PA1Yk/Dii4hsf+LCeOdmhlifuyROqJBAoGBAOEm4gLvaBWwnUhmr4sW8xywIhGGbU+MX6vm/KkGtScres7pPhmfy6ARUzCxxyBqIE+nhCRNBpOEPhP7dv8naJhZZ4fRvNzuXpUMT2X3bc5yNzdhaOxBJl95YQbrYUHhjcIw2kdXnIkpdbB/RqmY0F5BUTYECrd0tKWbjuL5RIRNAoGBAOA1wTXrYyVorouxV+mGNb62Py+utHJQKSa5cxF9nbbwWJd+FdreiBOJddjATmH8ovKjueQFVqK7koDveOb+pgRY2bpT88/NW8UF6a2wMiI0p6pxrR+hgzas480YiOCWr6XlsprqsSKBbEu4W97GicleZ6P5Iso/gBr9aHj9EWv5AoGAYhRzHj42RESUr4Zz8A5GR3f+z02U7rNCtfrAk80lOvP44ou+jqEKrib961d2XAt/GdPqf3nCZJ6WAFRp6Qq8yKkhrYvTTxbTwvAC4nNftTASF6DqeQiEc9DHUKFW08Ey5KYtYCitOx8BcqpvGNBF7NldTD+Ef5hqXT4fh4Z4r30CgYEAy2OYGMymTRowNKK06C+Kc62plhy6rnRPUESswLIeLwTKqOqE8t4pvOdWk0CoGjVusAOcLuA03jyfwvz5xTo96fWb1W4w31IgLJOXjqsmX2c6reCfNvFyMVgW8keOa4XmYu0C34uFEpMrZWkhVe7usVBFXjczuxptoI4+hnqzoikCgYBICBVR9Z7n2LvmWH19/Nnns8dsMn5peL7H6Mey76Lo9RMEMp4qhiJTqVZzWgxEyVjr0KFCHmdmwkTOm6A1yYmkqqXDdiJ9v4J4fXe0lRAoUoYPTOWynrCyd6uqq+3zlzTKW8jY9luywHq6msn07D636PvveeZ93DNCcO8Whw36rQ=="
|
||||
"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": [
|
||||
"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"]
|
||||
}
|
||||
@ -1770,10 +1688,11 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"privateKey": [
|
||||
"MIIEogIBAAKCAQEAungL4osLyP8bE6MSKj8ZMJTG8WBh3K2/xB5BJYCYc7P1CIORZI9o/vKQx1QnP+CXkIKnnR2kzIzC0rnTqlIOkaZfhmSn50jG5vNBS9qPT+WU7Ue3qKxuWJFwcaFU5SEJawJHqnDPK+pktkkxkudeMHz6iaKPs+wKcbfrRJ6+3a3FqQQdHEQg4IjVU8pBZmag1c7JHayiM56OT5y6jmE5JvY60959iPrZPXSTMU3hNoiVwdyK6QwdK+/0wrO681VhIP+u2pe92nQ+hsgMSSQJegLx1UsEEyU87syblG+p3zAKSS+kt2nviV/a2cYiiME0LdlQ3lnKsQ4t1Y6yZBiS2QIDAQABAoIBABhozI18TC+kjWPVrfQPzHlakGxahJUBvZ+rojWJjutefE4AAxFZ4JG3KRKexoCLIuwM3monzkHkj0BMiRO7qCKS1+Bc3snc8gSbhUmrs6Tu1b7162nOIKfBainFx7oyx+vVIZKDL+t8xHBERpQHa4IHajiIKi2QUZGvVMHn0e5srkPK0eSMjb5Z5j61aFb8InQzs7tczr99ke4VavOPT1gmRWGnbTavUbw/zIQ9sxAuMiD2v0nrGlOLZrMhaqzsT6PjIWVCSZrWex1pin9gA4XwGZ39E7+zFWgg+2OX0dEvehVDluAQR0K4PBUknuL1LFFW8dpvCrUSTmGGQOSVuB0CgYEA+bQjbjTNiMTEfoxx/WvVDgtLRL/x9RVyeYTPia2TGNBwpEcU64lLMOwUt5X/QuGXayPr0EGAxMA8kwq/E8Wj2t9+SuqkGK9SIwvghi2fOh0KWghuQbKYMogG5hsJAI8+/mBIOJJ8pyh0RX58vaTlYctbThO22aVahhZQ2weaW58CgYEAvyu4vIe44/7F19Hjh2BW+9lHsHA2zwHvC5T1kFaEdBYEwGsLMW6leCsiEMfpc2Uq3k9+buZgVpTE5APs9cSJX1aUXEG5QHQmYDxAAMiTyvpj0o2cKbDi1A5QZCRo23lC+uDyR7g2zLDJuHek0uyCtd83hbgyxIVFUnfvI9EmfocCgYBtpcZxHEqspgrKrw1XBMTXl+oDVG4A+tv7tHAVutx+5vivim8LRox3/RLT0s/2JG2DJJDmL/1FaEyxHOTu37il4cHpT8Oi+0mMDikXgm0K7bmf81fHDY97kPPGk1SOpFg7BzhvbxPBqyfzZCmOdRwsp0l+rXV7ePqZKq9ynpIPbQKBgFO/LZC5zE9k/vrK4egeVjzCNNugbQJGkJf8S49Nt3y7YJ2Cx0aCeE6qZqP/T8/Tk/IL1RF0LuP/DDnvVlFcJen0Hc5EpIkN2Pnzqv4s4EHdavmEO9MvwE6xbppQMPdkqekJvlmY47jMAbKkBzq3jZNrFAGqbeMVlwbHr6V7LGflAoGANFbzOnUMJwUfIdoI9uEG2QOTAcBb7vzt9MurO67wiTexOYadOSlcV1lQX3RKR9mCFJwy4kud0TN0gD++Ggl10eNB6f8JOF95e5+tWrtz88xZ5EalBOMfh+ATdKq8Q9MBSWZvO9bizhW1dhZZds/QmHgEItdwsTKDAq1PEiXhD0c="
|
||||
"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": [
|
||||
"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"],
|
||||
"algorithm": ["RSA-OAEP"]
|
||||
@ -1785,8 +1704,8 @@
|
||||
"providerId": "aes-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["c36222c6-6a43-4d32-9d44-d5d355e5cabd"],
|
||||
"secret": ["rzL4qUQ7wTEkZDbgt595VA"],
|
||||
"kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
|
||||
"secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
|
||||
"priority": ["100"]
|
||||
}
|
||||
},
|
||||
@ -1796,9 +1715,9 @@
|
||||
"providerId": "hmac-generated",
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"kid": ["06532a54-c310-41c1-829c-58776ce2ab4a"],
|
||||
"kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
|
||||
"secret": [
|
||||
"9v1ZjFhEFH6UpY6ncFkaCbqJYHMyI4tA0cvx4GuQ5KtMXYbimitSSVDqxIKwa-gBC_8bY2O4FQfpmp1Qn1-L4fFmPFfIF3ZKsO16263BwpADo_FNSBTte8Le4gJLylqFULdsn3ye17FHyq5Jjms_OTt3opzcDLNduCuK22GBBsU"
|
||||
"j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
|
||||
],
|
||||
"priority": ["100"],
|
||||
"algorithm": ["HS512"]
|
||||
@ -2469,7 +2388,7 @@
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"organizationsEnabled": "false"
|
||||
},
|
||||
"keycloakVersion": "26.0.7",
|
||||
"keycloakVersion": "25.0.0",
|
||||
"userManagedAccessAllowed": false,
|
||||
"organizationsEnabled": false,
|
||||
"clientProfiles": {
|
@ -1,118 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export type ParsedRealmJson = {
|
||||
realm: string;
|
||||
loginTheme?: string;
|
||||
accountTheme?: string;
|
||||
adminTheme?: string;
|
||||
emailTheme?: string;
|
||||
eventsListeners: string[];
|
||||
users: {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
credentials: {
|
||||
type: string /* "password" or something else */;
|
||||
}[];
|
||||
clientRoles?: Record<string, string[]>;
|
||||
}[];
|
||||
roles: {
|
||||
client: Record<
|
||||
string,
|
||||
{
|
||||
name: string;
|
||||
containerId: string; // client id
|
||||
}[]
|
||||
>;
|
||||
};
|
||||
clients: {
|
||||
id: string;
|
||||
clientId: string; // example: realm-management
|
||||
baseUrl?: string;
|
||||
redirectUris?: string[];
|
||||
webOrigins?: string[];
|
||||
attributes?: {
|
||||
"post.logout.redirect.uris"?: string;
|
||||
};
|
||||
protocol?: string;
|
||||
protocolMappers?: {
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: string; // "openid-connect" or something else
|
||||
protocolMapper: string; // "oidc-hardcoded-claim-mapper" or something else
|
||||
consentRequired: boolean;
|
||||
config?: Record<string, string>;
|
||||
}[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export const zParsedRealmJson = (() => {
|
||||
type TargetType = ParsedRealmJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
realm: z.string(),
|
||||
loginTheme: z.string().optional(),
|
||||
accountTheme: z.string().optional(),
|
||||
adminTheme: z.string().optional(),
|
||||
emailTheme: z.string().optional(),
|
||||
eventsListeners: z.array(z.string()),
|
||||
users: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
username: z.string(),
|
||||
credentials: z.array(
|
||||
z.object({
|
||||
type: z.string()
|
||||
})
|
||||
),
|
||||
clientRoles: z.record(z.array(z.string())).optional()
|
||||
})
|
||||
),
|
||||
roles: z.object({
|
||||
client: z.record(
|
||||
z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
containerId: z.string()
|
||||
})
|
||||
)
|
||||
)
|
||||
}),
|
||||
clients: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
clientId: z.string(),
|
||||
baseUrl: z.string().optional(),
|
||||
redirectUris: z.array(z.string()).optional(),
|
||||
webOrigins: z.array(z.string()).optional(),
|
||||
attributes: z
|
||||
.object({
|
||||
"post.logout.redirect.uris": z.string().optional()
|
||||
})
|
||||
.optional(),
|
||||
protocol: z.string().optional(),
|
||||
protocolMappers: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
protocol: z.string(),
|
||||
protocolMapper: z.string(),
|
||||
consentRequired: z.boolean(),
|
||||
config: z.record(z.string()).optional()
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
@ -1,3 +0,0 @@
|
||||
export type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
export { readRealmJsonFile } from "./readRealmJsonFile";
|
||||
export { writeRealmJsonFile } from "./writeRealmJsonFile";
|
@ -1,20 +0,0 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import * as fs from "fs";
|
||||
import { type ParsedRealmJson, zParsedRealmJson } from "./ParsedRealmJson";
|
||||
|
||||
export function readRealmJsonFile(params: {
|
||||
realmJsonFilePath: string;
|
||||
}): ParsedRealmJson {
|
||||
const { realmJsonFilePath } = params;
|
||||
|
||||
const parsedRealmJson = JSON.parse(
|
||||
fs.readFileSync(realmJsonFilePath).toString("utf8")
|
||||
) as unknown;
|
||||
|
||||
zParsedRealmJson.parse(parsedRealmJson);
|
||||
|
||||
assert(is<ParsedRealmJson>(parsedRealmJson));
|
||||
|
||||
return parsedRealmJson;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import * as fsPr from "fs/promises";
|
||||
import { getIsPrettierAvailable, runPrettier } from "../../../tools/runPrettier";
|
||||
import { canonicalStringify } from "../../../tools/canonicalStringify";
|
||||
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
import { getDefaultConfig } from "../defaultConfig";
|
||||
|
||||
export async function writeRealmJsonFile(params: {
|
||||
realmJsonFilePath: string;
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): Promise<void> {
|
||||
const { realmJsonFilePath, parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||
|
||||
let sourceCode = canonicalStringify({
|
||||
data: parsedRealmJson,
|
||||
referenceData: getDefaultConfig({
|
||||
keycloakMajorVersionNumber
|
||||
})
|
||||
});
|
||||
|
||||
if (await getIsPrettierAvailable()) {
|
||||
sourceCode = await runPrettier({
|
||||
sourceCode: sourceCode,
|
||||
filePath: realmJsonFilePath
|
||||
});
|
||||
}
|
||||
|
||||
await fsPr.writeFile(realmJsonFilePath, Buffer.from(sourceCode, "utf8"));
|
||||
}
|
@ -1,74 +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 { readRealmJsonFile } from "../ParsedRealmJson/readRealmJsonFile";
|
||||
import type { ParsedRealmJson } from "../ParsedRealmJson/ParsedRealmJson";
|
||||
|
||||
function getDefaultRealmJsonFilePath(params: { keycloakMajorVersionNumber: number }) {
|
||||
const { keycloakMajorVersionNumber } = params;
|
||||
|
||||
return pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
"realmConfig",
|
||||
"defaultConfig",
|
||||
`realm-kc-${keycloakMajorVersionNumber}.json`
|
||||
);
|
||||
}
|
||||
|
||||
export const { getSupportedKeycloakMajorVersions } = (() => {
|
||||
let cache: number[] | undefined = undefined;
|
||||
|
||||
function getSupportedKeycloakMajorVersions(): number[] {
|
||||
if (cache !== undefined) {
|
||||
return cache;
|
||||
}
|
||||
|
||||
cache = fs
|
||||
.readdirSync(
|
||||
pathDirname(
|
||||
getDefaultRealmJsonFilePath({ keycloakMajorVersionNumber: 0 })
|
||||
)
|
||||
)
|
||||
.map(fileBasename => {
|
||||
const match = fileBasename.match(/^realm-kc-(\d+)\.json$/);
|
||||
|
||||
if (match === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const n = parseInt(match[1]);
|
||||
|
||||
assert(!isNaN(n));
|
||||
|
||||
return n;
|
||||
})
|
||||
.filter(exclude(undefined))
|
||||
.sort((a, b) => b - a);
|
||||
|
||||
return cache;
|
||||
}
|
||||
|
||||
return { getSupportedKeycloakMajorVersions };
|
||||
})();
|
||||
|
||||
export function getDefaultConfig(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): ParsedRealmJson {
|
||||
const { keycloakMajorVersionNumber } = params;
|
||||
|
||||
assert(
|
||||
getSupportedKeycloakMajorVersions().includes(keycloakMajorVersionNumber),
|
||||
`We do not have a default config for Keycloak ${keycloakMajorVersionNumber}`
|
||||
);
|
||||
|
||||
return readRealmJsonFile({
|
||||
realmJsonFilePath: getDefaultRealmJsonFilePath({
|
||||
keycloakMajorVersionNumber
|
||||
})
|
||||
});
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./defaultConfig";
|
File diff suppressed because it is too large
Load Diff
@ -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,353 +0,0 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ParsedRealmJson } from "./ParsedRealmJson";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import { TEST_APP_URL, type ThemeType, THEME_TYPES } from "../../shared/constants";
|
||||
import { sameFactory } from "evt/tools/inDepth/same";
|
||||
|
||||
export function prepareRealmConfig(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
}): {
|
||||
realmName: string;
|
||||
clientName: string;
|
||||
username: string;
|
||||
} {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber, parsedKeycloakThemesJsonEntry } =
|
||||
params;
|
||||
|
||||
const { username } = addOrEditTestUser({
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
const { clientId } = addOrEditClient({
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
editAccountConsoleAndSecurityAdminConsole({ parsedRealmJson });
|
||||
|
||||
enableCustomThemes({
|
||||
parsedRealmJson,
|
||||
parsedKeycloakThemesJsonEntry
|
||||
});
|
||||
|
||||
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;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
}) {
|
||||
const { parsedRealmJson, parsedKeycloakThemesJsonEntry } = params;
|
||||
|
||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||
parsedRealmJson[`${themeType}Theme` as const] =
|
||||
!parsedKeycloakThemesJsonEntry.types.includes(themeType)
|
||||
? ""
|
||||
: parsedKeycloakThemesJsonEntry.name;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
delete_existing_password_credential_if_any: {
|
||||
const i = newUser.credentials.findIndex(
|
||||
credential => credential.type === "password"
|
||||
);
|
||||
|
||||
if (i === -1) {
|
||||
break delete_existing_password_credential_if_any;
|
||||
}
|
||||
|
||||
newUser.credentials.splice(i, 1);
|
||||
}
|
||||
|
||||
{
|
||||
const credential = defaultUser_default.credentials.find(
|
||||
credential => credential.type === "password"
|
||||
);
|
||||
|
||||
assert(credential !== undefined);
|
||||
|
||||
newUser.credentials.push(credential);
|
||||
}
|
||||
|
||||
{
|
||||
const nameByClientId = Object.fromEntries(
|
||||
parsedRealmJson.clients.map(client => [client.id, client.clientId] as const)
|
||||
);
|
||||
|
||||
const newClientRoles: NonNullable<
|
||||
ParsedRealmJson["users"][number]["clientRoles"]
|
||||
> = {};
|
||||
|
||||
for (const clientRole of Object.values(parsedRealmJson.roles.client).flat()) {
|
||||
const clientName = nameByClientId[clientRole.containerId];
|
||||
|
||||
assert(clientName !== undefined);
|
||||
|
||||
(newClientRoles[clientName] ??= []).push(clientRole.name);
|
||||
}
|
||||
|
||||
const { same: sameSet } = sameFactory({
|
||||
takeIntoAccountArraysOrdering: false
|
||||
});
|
||||
|
||||
for (const [clientName, roles] of Object.entries(newClientRoles)) {
|
||||
keep_previous_ordering_if_possible: {
|
||||
const roles_previous = newUser.clientRoles?.[clientName];
|
||||
|
||||
if (roles_previous === undefined) {
|
||||
break keep_previous_ordering_if_possible;
|
||||
}
|
||||
|
||||
if (!sameSet(roles_previous, roles)) {
|
||||
break keep_previous_ordering_if_possible;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
(newUser.clientRoles ??= {})[clientName] = roles;
|
||||
}
|
||||
}
|
||||
|
||||
if (defaultUser_preexisting === undefined) {
|
||||
parsedRealmJson.users.push(newUser);
|
||||
} else {
|
||||
const i = parsedRealmJson.users.indexOf(defaultUser_preexisting);
|
||||
assert(i !== -1);
|
||||
parsedRealmJson.users[i] = newUser;
|
||||
}
|
||||
|
||||
return { username: newUser.username };
|
||||
}
|
||||
|
||||
function addOrEditClient(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
keycloakMajorVersionNumber: number;
|
||||
}): { clientId: string } {
|
||||
const { parsedRealmJson, keycloakMajorVersionNumber } = params;
|
||||
|
||||
const parsedRealmJson_default = getDefaultConfig({ keycloakMajorVersionNumber });
|
||||
|
||||
const testClient_default = (() => {
|
||||
const clients = parsedRealmJson_default.clients.filter(client => {
|
||||
return JSON.stringify(client).includes(TEST_APP_URL);
|
||||
});
|
||||
|
||||
assert(clients.length === 1);
|
||||
|
||||
return clients[0];
|
||||
})();
|
||||
|
||||
const clientIds_builtIn = parsedRealmJson_default.clients
|
||||
.map(client => client.clientId)
|
||||
.filter(clientId => clientId !== testClient_default.clientId);
|
||||
|
||||
const testClient_preexisting = (() => {
|
||||
const clients = parsedRealmJson.clients
|
||||
.filter(client => !clientIds_builtIn.includes(client.clientId))
|
||||
.filter(client => client.protocol === "openid-connect");
|
||||
|
||||
{
|
||||
const client = clients.find(
|
||||
client => client.clientId === testClient_default.clientId
|
||||
);
|
||||
|
||||
if (client !== undefined) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const client = clients.find(
|
||||
client =>
|
||||
client.redirectUris?.find(redirectUri =>
|
||||
redirectUri.startsWith(TEST_APP_URL)
|
||||
) !== undefined
|
||||
);
|
||||
|
||||
if (client !== undefined) {
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
const [client] = clients;
|
||||
|
||||
if (client === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return client;
|
||||
})();
|
||||
|
||||
let testClient: typeof testClient_default;
|
||||
|
||||
if (testClient_preexisting !== undefined) {
|
||||
testClient = testClient_preexisting;
|
||||
} else {
|
||||
testClient = structuredClone(testClient_default);
|
||||
delete testClient.protocolMappers;
|
||||
parsedRealmJson.clients.push(testClient);
|
||||
}
|
||||
|
||||
testClient.redirectUris = [
|
||||
`${TEST_APP_URL}/*`,
|
||||
"http://localhost*",
|
||||
"http://127.0.0.1*"
|
||||
]
|
||||
.sort()
|
||||
.reverse();
|
||||
|
||||
(testClient.attributes ??= {})["post.logout.redirect.uris"] = "+";
|
||||
|
||||
testClient.webOrigins = ["*"];
|
||||
|
||||
return { clientId: testClient.clientId };
|
||||
}
|
||||
|
||||
function editAccountConsoleAndSecurityAdminConsole(params: {
|
||||
parsedRealmJson: ParsedRealmJson;
|
||||
}) {
|
||||
const { parsedRealmJson } = params;
|
||||
|
||||
for (const clientId of ["account-console", "security-admin-console"] as const) {
|
||||
const client = parsedRealmJson.clients.find(
|
||||
client => client.clientId === clientId
|
||||
);
|
||||
|
||||
assert(client !== undefined);
|
||||
|
||||
{
|
||||
const arr = (client.redirectUris ??= []);
|
||||
|
||||
for (const value of ["http://localhost*", "http://127.0.0.1*"]) {
|
||||
if (!arr.includes(value)) {
|
||||
arr.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
client.redirectUris?.sort().reverse();
|
||||
}
|
||||
|
||||
(client.attributes ??= {})["post.logout.redirect.uris"] = "+";
|
||||
|
||||
client.webOrigins = ["*"];
|
||||
|
||||
admin_specific: {
|
||||
if (clientId !== "security-admin-console") {
|
||||
break admin_specific;
|
||||
}
|
||||
|
||||
const protocolMapper_preexisting = client.protocolMappers?.find(
|
||||
protocolMapper => {
|
||||
if (protocolMapper.protocolMapper !== "oidc-hardcoded-claim-mapper") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (protocolMapper.protocol !== "openid-connect") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (protocolMapper.config === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (protocolMapper.config["claim.name"] !== "allowed-origins") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
let protocolMapper: NonNullable<typeof protocolMapper_preexisting>;
|
||||
|
||||
const config = {
|
||||
"introspection.token.claim": "true",
|
||||
"claim.value": '["*"]',
|
||||
"userinfo.token.claim": "true",
|
||||
"id.token.claim": "false",
|
||||
"lightweight.claim": "true",
|
||||
"access.token.claim": "true",
|
||||
"claim.name": "allowed-origins",
|
||||
"jsonType.label": "JSON",
|
||||
"access.tokenResponse.claim": "false"
|
||||
};
|
||||
|
||||
if (protocolMapper_preexisting !== undefined) {
|
||||
protocolMapper = protocolMapper_preexisting;
|
||||
} else {
|
||||
protocolMapper = {
|
||||
id: "8fd0d584-7052-4d04-a615-d18a71050873",
|
||||
name: "allowed-origins",
|
||||
protocol: "openid-connect",
|
||||
protocolMapper: "oidc-hardcoded-claim-mapper",
|
||||
consentRequired: false,
|
||||
config
|
||||
};
|
||||
|
||||
(client.protocolMappers ??= []).push(protocolMapper);
|
||||
}
|
||||
|
||||
assert(protocolMapper.config !== undefined);
|
||||
|
||||
if (config !== protocolMapper.config) {
|
||||
Object.assign(protocolMapper.config, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { getDefaultConfig } from "./defaultConfig";
|
||||
import { prepareRealmConfig } from "./prepareRealmConfig";
|
||||
import * as fs from "fs";
|
||||
import {
|
||||
join as pathJoin,
|
||||
dirname as pathDirname,
|
||||
relative as pathRelative,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { existsAsync } from "../../tools/fs.existsAsync";
|
||||
import {
|
||||
readRealmJsonFile,
|
||||
writeRealmJsonFile,
|
||||
type ParsedRealmJson
|
||||
} from "./ParsedRealmJson";
|
||||
import {
|
||||
dumpContainerConfig,
|
||||
type BuildContextLike as BuildContextLike_dumpContainerConfig
|
||||
} from "./dumpContainerConfig";
|
||||
import * as runExclusive from "run-exclusive";
|
||||
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import chalk from "chalk";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_dumpContainerConfig & {
|
||||
projectDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>;
|
||||
|
||||
export async function getRealmConfig(params: {
|
||||
keycloakMajorVersionNumber: number;
|
||||
realmJsonFilePath_userProvided: string | undefined;
|
||||
parsedKeycloakThemesJsonEntry: { name: string; types: (ThemeType | "email")[] };
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{
|
||||
realmJsonFilePath: string;
|
||||
clientName: string;
|
||||
realmName: string;
|
||||
username: string;
|
||||
onRealmConfigChange: () => Promise<void>;
|
||||
}> {
|
||||
const {
|
||||
keycloakMajorVersionNumber,
|
||||
realmJsonFilePath_userProvided,
|
||||
parsedKeycloakThemesJsonEntry,
|
||||
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,
|
||||
keycloakMajorVersionNumber,
|
||||
parsedKeycloakThemesJsonEntry
|
||||
});
|
||||
|
||||
{
|
||||
const dirPath = pathDirname(realmJsonFilePath);
|
||||
|
||||
if (!(await existsAsync(dirPath))) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
await writeRealmJsonFile({
|
||||
realmJsonFilePath,
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
const { onRealmConfigChange } = (() => {
|
||||
const run = runExclusive.build(async () => {
|
||||
const start = Date.now();
|
||||
|
||||
console.log(
|
||||
chalk.grey(`Changes detected to the '${realmName}' config, backing up...`)
|
||||
);
|
||||
|
||||
let parsedRealmJson: ParsedRealmJson;
|
||||
|
||||
try {
|
||||
parsedRealmJson = await dumpContainerConfig({
|
||||
buildContext,
|
||||
realmName,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(chalk.red(`Failed to backup '${realmName}' config:`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await writeRealmJsonFile({
|
||||
realmJsonFilePath,
|
||||
parsedRealmJson,
|
||||
keycloakMajorVersionNumber
|
||||
});
|
||||
|
||||
console.log(
|
||||
[
|
||||
chalk.grey(
|
||||
`Save changed to \`.${pathSep}${pathRelative(buildContext.projectDirPath, realmJsonFilePath)}\``
|
||||
),
|
||||
chalk.grey(
|
||||
`Next time you'll be running \`keycloakify start-keycloak\`, the realm '${realmName}' will be restored to this state.`
|
||||
),
|
||||
chalk.green(
|
||||
`✓ '${realmName}' config backed up completed in ${Date.now() - start}ms`
|
||||
)
|
||||
].join("\n")
|
||||
);
|
||||
});
|
||||
|
||||
const { waitForDebounce } = waitForDebounceFactory({
|
||||
delay: 1_000
|
||||
});
|
||||
|
||||
async function onRealmConfigChange() {
|
||||
await waitForDebounce();
|
||||
|
||||
run();
|
||||
}
|
||||
|
||||
return { onRealmConfigChange };
|
||||
})();
|
||||
|
||||
return {
|
||||
realmJsonFilePath,
|
||||
clientName,
|
||||
realmName,
|
||||
username,
|
||||
onRealmConfigChange
|
||||
};
|
||||
}
|
@ -1,12 +1,8 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { getBuildContext } from "../shared/buildContext";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import {
|
||||
CONTAINER_NAME,
|
||||
KEYCLOAKIFY_SPA_DEV_SERVER_PORT,
|
||||
KEYCLOAKIFY_LOGIN_JAR_BASENAME,
|
||||
TEST_APP_URL,
|
||||
ThemeType
|
||||
} from "../shared/constants";
|
||||
import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||
import { CONTAINER_NAME } from "../shared/constants";
|
||||
import { SemVer } from "../tools/SemVer";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
@ -14,7 +10,8 @@ import {
|
||||
join as pathJoin,
|
||||
relative as pathRelative,
|
||||
sep as pathSep,
|
||||
basename as pathBasename
|
||||
basename as pathBasename,
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import * as child_process from "child_process";
|
||||
import chalk from "chalk";
|
||||
@ -31,20 +28,14 @@ import { isInside } from "../tools/isInside";
|
||||
import { existsAsync } from "../tools/fs.existsAsync";
|
||||
import { rm } from "../tools/fs.rm";
|
||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
import { startViteDevServer } from "./startViteDevServer";
|
||||
import { getSupportedKeycloakMajorVersions } from "./realmConfig/defaultConfig";
|
||||
import { getSupportedDockerImageTags } from "./getSupportedDockerImageTags";
|
||||
import { getRealmConfig } from "./realmConfig";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export async function command(params: {
|
||||
buildContext: BuildContext;
|
||||
cliCommandOptions: {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
}) {
|
||||
export type CliCommandOptions = CliCommandOptions_common & {
|
||||
port: number | undefined;
|
||||
keycloakVersion: string | undefined;
|
||||
realmJsonFilePath: string | undefined;
|
||||
};
|
||||
|
||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||
exit_if_docker_not_installed: {
|
||||
let commandOutput: string | undefined = undefined;
|
||||
|
||||
@ -53,17 +44,11 @@ export async function command(params: {
|
||||
.execSync("docker --version", {
|
||||
stdio: ["ignore", "pipe", "ignore"]
|
||||
})
|
||||
.toString("utf8");
|
||||
} catch {
|
||||
commandOutput = "";
|
||||
}
|
||||
?.toString("utf8");
|
||||
} catch {}
|
||||
|
||||
commandOutput = commandOutput.trim().toLowerCase();
|
||||
|
||||
for (const term of ["docker", "podman"]) {
|
||||
if (commandOutput.includes(term)) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
||||
break exit_if_docker_not_installed;
|
||||
}
|
||||
|
||||
console.log(
|
||||
@ -103,34 +88,13 @@ export async function command(params: {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { cliCommandOptions, buildContext } = params;
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const { allSupportedTags, latestMajorTags } = await getSupportedDockerImageTags({
|
||||
buildContext
|
||||
});
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
const { dockerImageTag } = await (async () => {
|
||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||
const cliCommandOptions_keycloakVersion = cliCommandOptions.keycloakVersion;
|
||||
|
||||
const tag = allSupportedTags.find(tag =>
|
||||
tag.startsWith(cliCommandOptions_keycloakVersion)
|
||||
);
|
||||
|
||||
if (tag === undefined) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
`We could not find a Keycloak Docker image for ${cliCommandOptions_keycloakVersion}`,
|
||||
`Example of valid values: --keycloak-version 26, --keycloak-version 26.0.7`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return { dockerImageTag: tag };
|
||||
return { dockerImageTag: cliCommandOptions.keycloakVersion };
|
||||
}
|
||||
|
||||
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
||||
@ -145,134 +109,45 @@ 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 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")
|
||||
);
|
||||
|
||||
const tag_userSelected = await (async () => {
|
||||
let tag: string;
|
||||
const { keycloakVersion } = await promptKeycloakVersion({
|
||||
startingFromMajor: 18,
|
||||
excludeMajorVersions: [22],
|
||||
doOmitPatch: true,
|
||||
buildContext
|
||||
});
|
||||
|
||||
let latestMajorTags_copy = [...latestMajorTags];
|
||||
console.log(`→ ${keycloakVersion}`);
|
||||
|
||||
while (true) {
|
||||
const { value } = await cliSelect<string>({
|
||||
values: latestMajorTags_copy
|
||||
}).catch(() => {
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
tag = value;
|
||||
|
||||
{
|
||||
const doImplementAccountMpa =
|
||||
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||
buildContext.implementedThemeTypes.account.type === "Multi-Page";
|
||||
|
||||
if (doImplementAccountMpa && tag.startsWith("22.")) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`You are implementing a Multi-Page Account theme. Keycloak 22 is not supported, select another version`
|
||||
)
|
||||
);
|
||||
latestMajorTags_copy = latestMajorTags_copy.filter(
|
||||
tag => !tag.startsWith("22.")
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const readMajor = (tag: string) => {
|
||||
const major = parseInt(tag.split(".")[0]);
|
||||
assert(!isNaN(major));
|
||||
return major;
|
||||
};
|
||||
|
||||
{
|
||||
const major = readMajor(tag);
|
||||
|
||||
const doImplementAdminTheme =
|
||||
buildContext.implementedThemeTypes.admin.isImplemented;
|
||||
|
||||
const getIsSupported = (major: number) => major >= 23;
|
||||
|
||||
if (doImplementAdminTheme && !getIsSupported(major)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`You are implementing an Admin theme. Only Keycloak 23 and later are supported, select another version`
|
||||
)
|
||||
);
|
||||
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||
getIsSupported(readMajor(tag))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const doImplementAccountSpa =
|
||||
buildContext.implementedThemeTypes.account.isImplemented &&
|
||||
buildContext.implementedThemeTypes.account.type === "Single-Page";
|
||||
|
||||
const major = readMajor(tag);
|
||||
|
||||
const getIsSupported = (major: number) => major >= 19;
|
||||
|
||||
if (doImplementAccountSpa && !getIsSupported(major)) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`You are implementing a Single-Page Account theme. Only Keycloak 19 and later are supported, select another version`
|
||||
)
|
||||
);
|
||||
latestMajorTags_copy = latestMajorTags_copy.filter(tag =>
|
||||
getIsSupported(readMajor(tag))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return tag;
|
||||
})();
|
||||
|
||||
console.log(`→ ${tag_userSelected}`);
|
||||
|
||||
return { dockerImageTag: tag_userSelected };
|
||||
return { dockerImageTag: keycloakVersion };
|
||||
})();
|
||||
|
||||
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 => ({
|
||||
majorVersionNumber,
|
||||
index: dockerImageTag.indexOf(`${majorVersionNumber}`)
|
||||
index: tag.indexOf(`${majorVersionNumber}`)
|
||||
}))
|
||||
.filter(({ index }) => index !== -1)
|
||||
.sort((a, b) => a.index - b.index);
|
||||
|
||||
if (wrap === undefined) {
|
||||
try {
|
||||
const version = SemVer.parse(dockerImageTag);
|
||||
|
||||
console.error(
|
||||
chalk.yellow(
|
||||
`Keycloak version ${version.major} is not supported, supported versions are ${getSupportedKeycloakMajorVersions().join(", ")}`
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
} catch {
|
||||
// NOTE: Latest version
|
||||
const [n] = getSupportedKeycloakMajorVersions();
|
||||
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Could not determine the major Keycloak version number from the docker image tag ${dockerImageTag}. Assuming ${n}`
|
||||
)
|
||||
);
|
||||
return n;
|
||||
}
|
||||
console.warn(
|
||||
chalk.yellow(
|
||||
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
|
||||
)
|
||||
);
|
||||
return 25;
|
||||
}
|
||||
|
||||
return wrap.majorVersionNumber;
|
||||
@ -315,67 +190,159 @@ export async function command(params: {
|
||||
|
||||
assert(jarFilePath !== undefined);
|
||||
|
||||
const extensionJarFilePaths = [
|
||||
...(keycloakMajorVersionNumber <= 20
|
||||
? (console.log(
|
||||
chalk.yellow(
|
||||
"WARNING: With older version of keycloak your changes to the realm configuration are not persisted"
|
||||
)
|
||||
),
|
||||
[])
|
||||
: [
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
"bin",
|
||||
"start-keycloak",
|
||||
KEYCLOAKIFY_LOGIN_JAR_BASENAME
|
||||
)
|
||||
]),
|
||||
...(await Promise.all(
|
||||
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
||||
switch (extensionJar.type) {
|
||||
case "path": {
|
||||
assert(
|
||||
await existsAsync(extensionJar.path),
|
||||
`${extensionJar.path} does not exist`
|
||||
);
|
||||
return extensionJar.path;
|
||||
}
|
||||
case "url": {
|
||||
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
url: extensionJar.url,
|
||||
uniqueIdOfOnArchiveFile: "no extraction",
|
||||
onArchiveFile: async () => {}
|
||||
});
|
||||
return archiveFilePath;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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);
|
||||
})
|
||||
);
|
||||
|
||||
let parsedKeycloakThemesJson = id<
|
||||
{ themes: { name: string; types: (ThemeType | "email")[] }[] } | undefined
|
||||
>(undefined);
|
||||
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() {
|
||||
await extractArchive({
|
||||
archiveFilePath: jarFilePath,
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile, readFile }) => {
|
||||
if (
|
||||
relativeFilePathInArchive ===
|
||||
pathJoin("META-INF", "keycloak-themes.json") &&
|
||||
parsedKeycloakThemesJson === undefined
|
||||
) {
|
||||
parsedKeycloakThemesJson = JSON.parse(
|
||||
(await readFile()).toString("utf8")
|
||||
);
|
||||
}
|
||||
|
||||
onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
|
||||
if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
|
||||
await writeFile({
|
||||
filePath: pathJoin(
|
||||
@ -397,43 +364,6 @@ export async function command(params: {
|
||||
|
||||
await extractThemeResourcesFromJar();
|
||||
|
||||
assert(parsedKeycloakThemesJson !== undefined);
|
||||
|
||||
const { clientName, onRealmConfigChange, realmJsonFilePath, realmName, username } =
|
||||
await getRealmConfig({
|
||||
keycloakMajorVersionNumber,
|
||||
parsedKeycloakThemesJsonEntry: (() => {
|
||||
const entry = parsedKeycloakThemesJson.themes.find(
|
||||
({ name }) => name === buildContext.themeNames[0]
|
||||
);
|
||||
|
||||
assert(entry !== undefined);
|
||||
|
||||
return entry;
|
||||
})(),
|
||||
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 jarFilePath_cacheDir = pathJoin(
|
||||
buildContext.cacheDirPath,
|
||||
pathBasename(jarFilePath)
|
||||
@ -447,76 +377,17 @@ export async function command(params: {
|
||||
});
|
||||
} catch {}
|
||||
|
||||
const port = cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? 8080;
|
||||
|
||||
const doStartDevServer = (() => {
|
||||
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 DEFAULT_PORT = 8080;
|
||||
const port =
|
||||
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
|
||||
|
||||
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
||||
|
||||
const dockerRunArgs: string[] = [
|
||||
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
||||
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
||||
...(keycloakMajorVersionNumber >= 26
|
||||
? [
|
||||
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_USERNAME=admin`,
|
||||
`-e${SPACE_PLACEHOLDER}KC_BOOTSTRAP_ADMIN_PASSWORD=admin`
|
||||
]
|
||||
: [
|
||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`
|
||||
]),
|
||||
...(devServerPort === undefined
|
||||
? []
|
||||
: [
|
||||
`-e${SPACE_PLACEHOLDER}${KEYCLOAKIFY_SPA_DEV_SERVER_PORT}=${devServerPort}`
|
||||
]),
|
||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
||||
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
|
||||
...(buildContext.startKeycloakOptions.dockerExtraArgs.length === 0
|
||||
? []
|
||||
: [
|
||||
@ -527,12 +398,12 @@ export async function command(params: {
|
||||
...(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(
|
||||
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
|
||||
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
||||
@ -555,7 +426,7 @@ export async function command(params: {
|
||||
}))
|
||||
.map(
|
||||
({ localDirPath, containerDirPath }) =>
|
||||
`-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
|
||||
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
|
||||
),
|
||||
...buildContext.environmentVariables
|
||||
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||
@ -602,14 +473,7 @@ export async function command(params: {
|
||||
{ shell: true }
|
||||
);
|
||||
|
||||
child.stdout.on("data", async data => {
|
||||
if (data.toString("utf8").includes("keycloakify-logging: REALM_CONFIG_CHANGED")) {
|
||||
await onRealmConfigChange();
|
||||
return;
|
||||
}
|
||||
|
||||
process.stdout.write(data);
|
||||
});
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
@ -656,9 +520,9 @@ export async function command(params: {
|
||||
`${chalk.green("Your theme is accessible at:")}`,
|
||||
`${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}`);
|
||||
}
|
||||
if (kcHttpRelativePath !== undefined) {
|
||||
@ -667,20 +531,13 @@ 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(username)}`,
|
||||
`- username: ${chalk.cyan.bold("testuser")}`,
|
||||
`- password: ${chalk.cyan.bold("password123")}`,
|
||||
"",
|
||||
`Watching for changes in ${chalk.bold(
|
||||
@ -737,92 +594,6 @@ export async function command(params: {
|
||||
}
|
||||
)
|
||||
.on("all", async (...[, filePath]) => {
|
||||
ignore_path_covered_by_hmr: {
|
||||
if (filePath.endsWith(".properties")) {
|
||||
break ignore_path_covered_by_hmr;
|
||||
}
|
||||
|
||||
if (!doStartDevServer) {
|
||||
break ignore_path_covered_by_hmr;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
ignore_patternfly: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"shared",
|
||||
"@patternfly"
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_patternfly;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ignore_keycloak_ui_shared: {
|
||||
if (
|
||||
!isInside({
|
||||
dirPath: pathJoin(
|
||||
buildContext.themeSrcDirPath,
|
||||
"shared",
|
||||
"keycloak-ui-shared"
|
||||
),
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
break ignore_keycloak_ui_shared;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Detected changes in ${filePath}`);
|
||||
|
||||
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,319 +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 {
|
||||
getExtensionModuleFileSourceCodeReadyToBeCopied,
|
||||
type BuildContextLike as BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied
|
||||
} from "./getExtensionModuleFileSourceCodeReadyToBeCopied";
|
||||
import * as crypto from "crypto";
|
||||
import { KEYCLOAK_THEME } from "../shared/constants";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { isAmong } from "tsafe/isAmong";
|
||||
|
||||
export type ExtensionModuleMeta = {
|
||||
moduleName: string;
|
||||
version: string;
|
||||
files: {
|
||||
fileRelativePath: string;
|
||||
hash: string;
|
||||
copyableFilePath: string;
|
||||
}[];
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
const zExtensionModuleMeta = (() => {
|
||||
type ExpectedType = ExtensionModuleMeta;
|
||||
|
||||
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;
|
||||
extensionModuleMetas: ExtensionModuleMeta[];
|
||||
};
|
||||
|
||||
const zParsedCacheFile = (() => {
|
||||
type ExpectedType = ParsedCacheFile;
|
||||
|
||||
const zTargetType = z.object({
|
||||
keycloakifyVersion: z.string(),
|
||||
prettierConfigHash: z.union([z.string(), z.null()]),
|
||||
thisFilePath: z.string(),
|
||||
extensionModuleMetas: z.array(zExtensionModuleMeta)
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<InferredType, ExpectedType>>();
|
||||
|
||||
return id<z.ZodType<ExpectedType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const CACHE_FILE_RELATIVE_PATH = pathJoin("extension-modules", "cache.json");
|
||||
|
||||
export type BuildContextLike =
|
||||
BuildContextLike_getExtensionModuleFileSourceCodeReadyToBeCopied & {
|
||||
cacheDirPath: string;
|
||||
packageJsonFilePath: string;
|
||||
projectDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
export async function getExtensionModuleMetas(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<ExtensionModuleMeta[]> {
|
||||
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 installedExtensionModules = await (async () => {
|
||||
const installedModulesWithKeycloakifyInTheName = await listInstalledModules({
|
||||
packageJsonFilePath: 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 extensionModuleMetas_cacheUpToDate: ExtensionModuleMeta[] = 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 extensionModuleMetas_cacheUpToDate =
|
||||
parsedCacheFile.extensionModuleMetas.filter(extensionModuleMeta => {
|
||||
const correspondingInstalledExtensionModule =
|
||||
installedExtensionModules.find(
|
||||
installedExtensionModule =>
|
||||
installedExtensionModule.moduleName ===
|
||||
extensionModuleMeta.moduleName
|
||||
);
|
||||
|
||||
if (correspondingInstalledExtensionModule === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
correspondingInstalledExtensionModule.version ===
|
||||
extensionModuleMeta.version
|
||||
);
|
||||
});
|
||||
|
||||
return extensionModuleMetas_cacheUpToDate;
|
||||
})();
|
||||
|
||||
const extensionModuleMetas = await Promise.all(
|
||||
installedExtensionModules.map(
|
||||
async ({
|
||||
moduleName,
|
||||
version,
|
||||
peerDependencies,
|
||||
dirPath
|
||||
}): Promise<ExtensionModuleMeta> => {
|
||||
use_cache: {
|
||||
const extensionModuleMeta_cache =
|
||||
extensionModuleMetas_cacheUpToDate.find(
|
||||
extensionModuleMeta =>
|
||||
extensionModuleMeta.moduleName === moduleName
|
||||
);
|
||||
|
||||
if (extensionModuleMeta_cache === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return extensionModuleMeta_cache;
|
||||
}
|
||||
|
||||
const files: ExtensionModuleMeta["files"] = [];
|
||||
|
||||
{
|
||||
const srcDirPath = pathJoin(dirPath, KEYCLOAK_THEME);
|
||||
|
||||
await crawlAsync({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath",
|
||||
onFileFound: async fileRelativePath => {
|
||||
const sourceCode =
|
||||
await getExtensionModuleFileSourceCodeReadyToBeCopied({
|
||||
buildContext,
|
||||
fileRelativePath,
|
||||
isOwnershipAction: false,
|
||||
extensionModuleDirPath: dirPath,
|
||||
extensionModuleName: moduleName,
|
||||
extensionModuleVersion: 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<ExtensionModuleMeta>({
|
||||
moduleName,
|
||||
version,
|
||||
files,
|
||||
peerDependencies: Object.fromEntries(
|
||||
Object.entries(peerDependencies).filter(
|
||||
([moduleName]) =>
|
||||
!isAmong(["react", "@types/react"], moduleName)
|
||||
)
|
||||
)
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
update_cache: {
|
||||
const parsedCacheFile = id<ParsedCacheFile>({
|
||||
keycloakifyVersion,
|
||||
prettierConfigHash,
|
||||
thisFilePath: cacheFilePath,
|
||||
extensionModuleMetas
|
||||
});
|
||||
|
||||
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 extensionModuleMetas;
|
||||
}
|
||||
|
||||
export function computeHash(data: Buffer) {
|
||||
return crypto.createHash("sha256").update(data).digest("hex");
|
||||
}
|
@ -1,151 +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 getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
|
||||
buildContext: BuildContextLike;
|
||||
fileRelativePath: string;
|
||||
isOwnershipAction: boolean;
|
||||
extensionModuleDirPath: string;
|
||||
extensionModuleName: string;
|
||||
extensionModuleVersion: string;
|
||||
}): Promise<Buffer> {
|
||||
const {
|
||||
buildContext,
|
||||
extensionModuleDirPath,
|
||||
fileRelativePath,
|
||||
isOwnershipAction,
|
||||
extensionModuleName,
|
||||
extensionModuleVersion
|
||||
} = params;
|
||||
|
||||
let sourceCode = (
|
||||
await fsPr.readFile(
|
||||
pathJoin(extensionModuleDirPath, KEYCLOAK_THEME, fileRelativePath)
|
||||
)
|
||||
).toString("utf8");
|
||||
|
||||
sourceCode = addCommentToSourceCode({
|
||||
sourceCode,
|
||||
fileRelativePath,
|
||||
commentLines: (() => {
|
||||
const path = fileRelativePath.split(pathSep).join("/");
|
||||
|
||||
return isOwnershipAction
|
||||
? [
|
||||
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
||||
``,
|
||||
`$ npx keycloakify own --path "${path}" --revert`
|
||||
]
|
||||
: [
|
||||
`WARNING: Before modifying this file, run the following command:`,
|
||||
``,
|
||||
`$ npx keycloakify own --path "${path}"`,
|
||||
``,
|
||||
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
||||
];
|
||||
})()
|
||||
});
|
||||
|
||||
const destFilePath = pathJoin(buildContext.themeSrcDirPath, fileRelativePath);
|
||||
|
||||
format: {
|
||||
if (!(await getIsPrettierAvailable())) {
|
||||
break format;
|
||||
}
|
||||
|
||||
sourceCode = await runPrettier({
|
||||
filePath: destFilePath,
|
||||
sourceCode
|
||||
});
|
||||
}
|
||||
|
||||
return Buffer.from(sourceCode, "utf8");
|
||||
}
|
||||
|
||||
function addCommentToSourceCode(params: {
|
||||
sourceCode: string;
|
||||
fileRelativePath: string;
|
||||
commentLines: string[];
|
||||
}): string {
|
||||
const { sourceCode, fileRelativePath, commentLines } = params;
|
||||
|
||||
const toResult = (comment: string) => {
|
||||
return [comment, ``, sourceCode].join("\n");
|
||||
};
|
||||
|
||||
for (const ext of [".ts", ".tsx", ".css", ".less", ".sass", ".js", ".jsx"]) {
|
||||
if (!fileRelativePath.endsWith(ext)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return toResult(
|
||||
[`/**`, ...commentLines.map(line => ` * ${line}`), ` */`].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".properties")) {
|
||||
return toResult(commentLines.map(line => `# ${line}`).join("\n"));
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".ftl")) {
|
||||
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
|
||||
"\n"
|
||||
);
|
||||
|
||||
if (sourceCode.trim().startsWith("<#ftl")) {
|
||||
const [first, ...rest] = sourceCode.split(">");
|
||||
|
||||
const last = rest.join(">");
|
||||
|
||||
return [`${first}>`, comment, last].join("\n");
|
||||
}
|
||||
|
||||
return toResult(comment);
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
||||
const comment = [
|
||||
`<!--`,
|
||||
...commentLines.map(
|
||||
line =>
|
||||
` ${line
|
||||
.replace("--path", "-t")
|
||||
.replace("--revert", "-r")
|
||||
.replace("Before modifying", "Before modifying or replacing")}`
|
||||
),
|
||||
`-->`
|
||||
].join("\n");
|
||||
|
||||
if (fileRelativePath.endsWith(".html") && sourceCode.trim().startsWith("<!")) {
|
||||
const [first, ...rest] = sourceCode.split(">");
|
||||
|
||||
const last = rest.join(">");
|
||||
|
||||
return [`${first}>`, comment, last].join("\n");
|
||||
}
|
||||
|
||||
if (fileRelativePath.endsWith(".svg") && sourceCode.trim().startsWith("<?")) {
|
||||
const [first, ...rest] = sourceCode.split("?>");
|
||||
|
||||
const last = rest.join("?>");
|
||||
|
||||
return [`${first}?>`, comment, last].join("\n");
|
||||
}
|
||||
|
||||
return toResult(comment);
|
||||
}
|
||||
|
||||
return sourceCode;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from "./sync-extension";
|
@ -1,159 +0,0 @@
|
||||
import { assert, type Equals, is } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import type { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||
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 ExtensionModuleMetaLike = {
|
||||
moduleName: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
};
|
||||
|
||||
assert<ExtensionModuleMeta extends ExtensionModuleMetaLike ? true : false>();
|
||||
|
||||
export async function installExtensionModulesPeerDependencies(params: {
|
||||
buildContext: BuildContextLike;
|
||||
extensionModuleMetas: ExtensionModuleMetaLike[];
|
||||
}): Promise<void | never> {
|
||||
const { buildContext, extensionModuleMetas } = params;
|
||||
|
||||
const { extensionModulesPerDependencies } = (() => {
|
||||
const extensionModulesPerDependencies: Record<string, string> = {};
|
||||
|
||||
for (const { peerDependencies } of extensionModuleMetas) {
|
||||
for (const [peerDependencyName, versionRange_candidate] of Object.entries(
|
||||
peerDependencies
|
||||
)) {
|
||||
const versionRange = (() => {
|
||||
const versionRange_current =
|
||||
extensionModulesPerDependencies[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;
|
||||
})();
|
||||
|
||||
extensionModulesPerDependencies[peerDependencyName] = versionRange;
|
||||
}
|
||||
}
|
||||
|
||||
return { extensionModulesPerDependencies };
|
||||
})();
|
||||
|
||||
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(
|
||||
extensionModulesPerDependencies
|
||||
)) {
|
||||
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);
|
||||
|
||||
await 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 { ExtensionModuleMeta } from "./extensionModuleMeta";
|
||||
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 = `# === Owned files start ===`;
|
||||
const DELIMITER_END = `# === Owned files end =====`;
|
||||
|
||||
export async function writeManagedGitignoreFile(params: {
|
||||
buildContext: BuildContextLike;
|
||||
extensionModuleMetas: ExtensionModuleMeta[];
|
||||
ownedFilesRelativePaths: string[];
|
||||
}): Promise<void> {
|
||||
const { buildContext, extensionModuleMetas, ownedFilesRelativePaths } = params;
|
||||
|
||||
if (extensionModuleMetas.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,
|
||||
...ownedFilesRelativePaths
|
||||
.map(fileRelativePath => fileRelativePath.split(pathSep).join("/"))
|
||||
.map(line => `# ${line}`),
|
||||
DELIMITER_END,
|
||||
``,
|
||||
...extensionModuleMetas
|
||||
.map(extensionModuleMeta => [
|
||||
`# === ${extensionModuleMeta.moduleName} v${extensionModuleMeta.version} ===`,
|
||||
...extensionModuleMeta.files
|
||||
.map(({ fileRelativePath }) => fileRelativePath)
|
||||
.filter(
|
||||
fileRelativePath =>
|
||||
!ownedFilesRelativePaths.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<{
|
||||
ownedFilesRelativePaths: string[];
|
||||
}> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const filePath = pathJoin(buildContext.themeSrcDirPath, ".gitignore");
|
||||
|
||||
if (!(await existsAsync(filePath))) {
|
||||
return { ownedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
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 { ownedFilesRelativePaths: [] };
|
||||
}
|
||||
|
||||
const ownedFilesRelativePaths = 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 { ownedFilesRelativePaths };
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import { getExtensionModuleMetas, computeHash } from "./extensionModuleMeta";
|
||||
import { installExtensionModulesPeerDependencies } from "./installExtensionModulesPeerDependencies";
|
||||
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 { getIsKnownByGit } from "../tools/isKnownByGit";
|
||||
import { untrackFromGit } from "../tools/untrackFromGit";
|
||||
|
||||
export async function command(params: { buildContext: BuildContext }) {
|
||||
const { buildContext } = params;
|
||||
|
||||
const extensionModuleMetas = await getExtensionModuleMetas({ buildContext });
|
||||
|
||||
await installExtensionModulesPeerDependencies({
|
||||
buildContext,
|
||||
extensionModuleMetas
|
||||
});
|
||||
|
||||
const { ownedFilesRelativePaths } = await readManagedGitignoreFile({
|
||||
buildContext
|
||||
});
|
||||
|
||||
await writeManagedGitignoreFile({
|
||||
buildContext,
|
||||
ownedFilesRelativePaths,
|
||||
extensionModuleMetas
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
extensionModuleMetas
|
||||
.map(extensionModuleMeta =>
|
||||
Promise.all(
|
||||
extensionModuleMeta.files.map(
|
||||
async ({ fileRelativePath, copyableFilePath, hash }) => {
|
||||
if (ownedFilesRelativePaths.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;
|
||||
}
|
||||
|
||||
if (await getIsKnownByGit({ filePath: destFilePath })) {
|
||||
await untrackFromGit({
|
||||
filePath: destFilePath
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const dirName = pathDirname(destFilePath);
|
||||
|
||||
if (!(await existsAsync(dirName))) {
|
||||
await fsPr.mkdir(dirName, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
await fsPr.copyFile(copyableFilePath, destFilePath);
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
);
|
||||
}
|
12
src/bin/tools/OptionalIfCanBeUndefined.ts
Normal file
12
src/bin/tools/OptionalIfCanBeUndefined.ts
Normal file
@ -0,0 +1,12 @@
|
||||
type PropertiesThatCanBeUndefined<T extends Record<string, unknown>> = {
|
||||
[Key in keyof T]: undefined extends T[Key] ? Key : never;
|
||||
}[keyof T];
|
||||
|
||||
/**
|
||||
* OptionalIfCanBeUndefined<{ p1: string | undefined; p2: string; }>
|
||||
* is
|
||||
* { p1?: string | undefined; p2: string }
|
||||
*/
|
||||
export type OptionalIfCanBeUndefined<T extends Record<string, unknown>> = {
|
||||
[K in PropertiesThatCanBeUndefined<T>]?: T[K];
|
||||
} & { [K in Exclude<keyof T, PropertiesThatCanBeUndefined<T>>]: T[K] };
|
@ -1,99 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { same } from "evt/tools/inDepth/same";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
export type Stringifyable =
|
||||
| StringifyableAtomic
|
||||
| StringifyableObject
|
||||
| StringifyableArray;
|
||||
|
||||
export type StringifyableAtomic = string | number | boolean | null;
|
||||
|
||||
// NOTE: Use Record<string, Stringifyable>
|
||||
interface StringifyableObject {
|
||||
[key: string]: Stringifyable;
|
||||
}
|
||||
|
||||
// NOTE: Use Stringifyable[]
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface StringifyableArray extends Array<Stringifyable> {}
|
||||
|
||||
export const zStringifyableAtomic = (() => {
|
||||
type TargetType = StringifyableAtomic;
|
||||
|
||||
const zTargetType = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
export const zStringifyable: z.ZodType<Stringifyable> = z
|
||||
.any()
|
||||
.superRefine((val, ctx) => {
|
||||
const isStringifyable = same(JSON.parse(JSON.stringify(val)), val);
|
||||
if (!isStringifyable) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: "Not stringifyable"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export function getIsAtomic(
|
||||
stringifyable: Stringifyable
|
||||
): stringifyable is StringifyableAtomic {
|
||||
return (
|
||||
["string", "number", "boolean"].includes(typeof stringifyable) ||
|
||||
stringifyable === null
|
||||
);
|
||||
}
|
||||
|
||||
export const { getValueAtPath } = (() => {
|
||||
function getValueAtPath_rec(
|
||||
stringifyable: Stringifyable,
|
||||
path: (string | number)[]
|
||||
): Stringifyable | undefined {
|
||||
if (path.length === 0) {
|
||||
return stringifyable;
|
||||
}
|
||||
|
||||
if (getIsAtomic(stringifyable)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const [first, ...rest] = path;
|
||||
|
||||
let dereferenced: Stringifyable | undefined;
|
||||
|
||||
if (stringifyable instanceof Array) {
|
||||
if (typeof first !== "number") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dereferenced = stringifyable[first];
|
||||
} else {
|
||||
if (typeof first !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
dereferenced = stringifyable[first];
|
||||
}
|
||||
|
||||
if (dereferenced === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getValueAtPath_rec(dereferenced, rest);
|
||||
}
|
||||
|
||||
function getValueAtPath(
|
||||
stringifyableObjectOrArray: Record<string, Stringifyable> | Stringifyable[],
|
||||
path: (string | number)[]
|
||||
): Stringifyable | undefined {
|
||||
return getValueAtPath_rec(stringifyableObjectOrArray, path);
|
||||
}
|
||||
|
||||
return { getValueAtPath };
|
||||
})();
|
@ -1,164 +0,0 @@
|
||||
import { getIsAtomic, getValueAtPath, type Stringifyable } from "./Stringifyable";
|
||||
|
||||
export function canonicalStringify(params: {
|
||||
data: Record<string, Stringifyable> | Stringifyable[];
|
||||
referenceData: Record<string, Stringifyable> | Stringifyable[];
|
||||
}): string {
|
||||
const { data, referenceData } = params;
|
||||
|
||||
return JSON.stringify(
|
||||
makeDeterministicCopy({
|
||||
path: [],
|
||||
data,
|
||||
getCanonicalKeys: path => {
|
||||
const referenceValue = (() => {
|
||||
const path_patched: (string | number)[] = [];
|
||||
|
||||
for (let i = 0; i < path.length; i++) {
|
||||
let value_i = getValueAtPath(referenceData, [
|
||||
...path_patched,
|
||||
path[i]
|
||||
]);
|
||||
|
||||
if (value_i !== undefined) {
|
||||
path_patched.push(path[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof path[i] !== "number") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
value_i = getValueAtPath(referenceData, [...path_patched, 0]);
|
||||
|
||||
if (value_i !== undefined) {
|
||||
path_patched.push(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getValueAtPath(referenceData, path_patched);
|
||||
})();
|
||||
|
||||
if (referenceValue === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (getIsAtomic(referenceValue)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (referenceValue instanceof Array) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Object.keys(referenceValue);
|
||||
}
|
||||
}),
|
||||
null,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
function makeDeterministicCopy(params: {
|
||||
path: (string | number)[];
|
||||
data: Stringifyable;
|
||||
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||
}): Stringifyable {
|
||||
const { path, data, getCanonicalKeys } = params;
|
||||
|
||||
if (getIsAtomic(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data instanceof Array) {
|
||||
return makeDeterministicCopy_array({
|
||||
path,
|
||||
data,
|
||||
getCanonicalKeys
|
||||
});
|
||||
}
|
||||
|
||||
return makeDeterministicCopy_record({
|
||||
path,
|
||||
data,
|
||||
getCanonicalKeys
|
||||
});
|
||||
}
|
||||
|
||||
function makeDeterministicCopy_record(params: {
|
||||
path: (string | number)[];
|
||||
data: Record<string, Stringifyable>;
|
||||
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||
}): Record<string, Stringifyable> {
|
||||
const { path, data, getCanonicalKeys } = params;
|
||||
|
||||
const keysOfAtomicValues: string[] = [];
|
||||
const keysOfNonAtomicValues: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(data)) {
|
||||
if (getIsAtomic(value)) {
|
||||
keysOfAtomicValues.push(key);
|
||||
} else {
|
||||
keysOfNonAtomicValues.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysOfAtomicValues.sort();
|
||||
keysOfNonAtomicValues.sort();
|
||||
|
||||
const keys = [...keysOfAtomicValues, ...keysOfNonAtomicValues];
|
||||
|
||||
reorder_according_to_canonical: {
|
||||
const canonicalKeys = getCanonicalKeys(path);
|
||||
|
||||
if (canonicalKeys === undefined) {
|
||||
break reorder_according_to_canonical;
|
||||
}
|
||||
|
||||
const keys_toPrepend: string[] = [];
|
||||
|
||||
for (const key of canonicalKeys) {
|
||||
const indexOfKey = keys.indexOf(key);
|
||||
|
||||
if (indexOfKey === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
keys.splice(indexOfKey, 1);
|
||||
keys_toPrepend.push(key);
|
||||
}
|
||||
|
||||
keys.unshift(...keys_toPrepend);
|
||||
}
|
||||
|
||||
const result: Record<string, Stringifyable> = {};
|
||||
|
||||
for (const key of keys) {
|
||||
result[key] = makeDeterministicCopy({
|
||||
path: [...path, key],
|
||||
data: data[key],
|
||||
getCanonicalKeys
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function makeDeterministicCopy_array(params: {
|
||||
path: (string | number)[];
|
||||
data: Stringifyable[];
|
||||
getCanonicalKeys: (path: (string | number)[]) => string[] | undefined;
|
||||
}): Stringifyable[] {
|
||||
const { path, data, getCanonicalKeys } = params;
|
||||
|
||||
return [...data].map((entry, i) =>
|
||||
makeDeterministicCopy({
|
||||
path: [...path, i],
|
||||
data: entry,
|
||||
getCanonicalKeys
|
||||
})
|
||||
);
|
||||
}
|
@ -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 });
|
||||
})
|
||||
);
|
||||
}
|
73
src/bin/tools/crc32.ts
Normal file
73
src/bin/tools/crc32.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { Readable } from "stream";
|
||||
|
||||
const crc32tab = [
|
||||
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535,
|
||||
0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, 0x09b64c2b, 0x7eb17cbd,
|
||||
0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d,
|
||||
0x6ddde4eb, 0xf4d4b551, 0x83d385c7, 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
|
||||
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4,
|
||||
0xa2677172, 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
|
||||
0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, 0x26d930ac,
|
||||
0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
|
||||
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, 0x2f6f7c87, 0x58684c11, 0xc1611dab,
|
||||
0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f,
|
||||
0x9fbfe4a5, 0xe8b8d433, 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb,
|
||||
0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea,
|
||||
0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, 0x4db26158, 0x3ab551ce,
|
||||
0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a,
|
||||
0x346ed9fc, 0xad678846, 0xda60b8d0, 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
|
||||
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409,
|
||||
0xce61e49f, 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
|
||||
0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, 0xead54739,
|
||||
0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
|
||||
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, 0xf00f9344, 0x8708a3d2, 0x1e01f268,
|
||||
0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0,
|
||||
0x10da7a5a, 0x67dd4acc, 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8,
|
||||
0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef,
|
||||
0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, 0xcc0c7795, 0xbb0b4703,
|
||||
0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7,
|
||||
0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
|
||||
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae,
|
||||
0x0cb61b38, 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
|
||||
0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, 0x88085ae6,
|
||||
0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
|
||||
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, 0xa7672661, 0xd06016f7, 0x4969474d,
|
||||
0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5,
|
||||
0x47b2cf7f, 0x30b5ffe9, 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605,
|
||||
0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||
];
|
||||
|
||||
/**
|
||||
*
|
||||
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
||||
* @returns a promise for a checksum (uint32)
|
||||
*/
|
||||
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||
if (typeof input === "string") {
|
||||
let crc = ~0;
|
||||
for (let i = 0; i < input.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||
return Promise.resolve((crc ^ -1) >>> 0);
|
||||
} else if (input instanceof Buffer) {
|
||||
let crc = ~0;
|
||||
for (let i = 0; i < input.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||
return Promise.resolve((crc ^ -1) >>> 0);
|
||||
} else if (input instanceof Readable) {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
let crc = ~0;
|
||||
input.setMaxListeners(Infinity);
|
||||
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
||||
input.on("error", e => reject(e));
|
||||
input.on("data", (chunk: Buffer) => {
|
||||
for (let i = 0; i < chunk.length; i++)
|
||||
crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unsupported input " + typeof input);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
const keyIsTrapped = "isTrapped_zSskDe9d";
|
||||
|
||||
export class AccessError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, new.target.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export function createObjectThatThrowsIfAccessed<T extends object>(params?: {
|
||||
debugMessage?: string;
|
||||
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||
}): T {
|
||||
const { debugMessage = "", isPropertyWhitelisted = () => false } = params ?? {};
|
||||
|
||||
const get: NonNullable<ProxyHandler<T>["get"]> = (...args) => {
|
||||
const [, prop] = args;
|
||||
|
||||
if (isPropertyWhitelisted(prop)) {
|
||||
return Reflect.get(...args);
|
||||
}
|
||||
|
||||
if (prop === keyIsTrapped) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new AccessError(`Cannot access ${String(prop)} yet ${debugMessage}`);
|
||||
};
|
||||
|
||||
const trappedObject = new Proxy<T>({} as any, {
|
||||
get,
|
||||
set: get
|
||||
});
|
||||
|
||||
return trappedObject;
|
||||
}
|
||||
|
||||
export function createObjectThatThrowsIfAccessedFactory(params: {
|
||||
isPropertyWhitelisted?: (prop: string | number | symbol) => boolean;
|
||||
}) {
|
||||
const { isPropertyWhitelisted } = params;
|
||||
|
||||
return {
|
||||
createObjectThatThrowsIfAccessed: <T extends object>(params?: {
|
||||
debugMessage?: string;
|
||||
}) => {
|
||||
const { debugMessage } = params ?? {};
|
||||
|
||||
return createObjectThatThrowsIfAccessed<T>({
|
||||
debugMessage,
|
||||
isPropertyWhitelisted
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function isObjectThatThrowIfAccessed(obj: object) {
|
||||
return (obj as any)[keyIsTrapped] === true;
|
||||
}
|
||||
|
||||
export const THROW_IF_ACCESSED = {
|
||||
__brand: "THROW_IF_ACCESSED"
|
||||
};
|
||||
|
||||
export function createObjectWithSomePropertiesThatThrowIfAccessed<
|
||||
T extends Record<string, unknown>
|
||||
>(obj: { [K in keyof T]: T[K] | typeof THROW_IF_ACCESSED }, debugMessage?: string): T {
|
||||
return Object.defineProperties(
|
||||
obj,
|
||||
Object.fromEntries(
|
||||
Object.entries(obj)
|
||||
.filter(([, value]) => value === THROW_IF_ACCESSED)
|
||||
.map(([key]) => {
|
||||
const getAndSet = () => {
|
||||
throw new AccessError(
|
||||
`Cannot access ${key} yet ${debugMessage ?? ""}`
|
||||
);
|
||||
};
|
||||
|
||||
const pd = {
|
||||
get: getAndSet,
|
||||
set: getAndSet,
|
||||
enumerable: true
|
||||
};
|
||||
|
||||
return [key, pd];
|
||||
})
|
||||
)
|
||||
) as any;
|
||||
}
|
61
src/bin/tools/deflate.ts
Normal file
61
src/bin/tools/deflate.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
||||
import { pipeline } from "stream/promises";
|
||||
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
||||
import { promisify } from "util";
|
||||
|
||||
import { crc32 } from "./crc32";
|
||||
import tee from "./tee";
|
||||
|
||||
const deflateRaw = promisify(deflateRawCb);
|
||||
|
||||
/**
|
||||
* A stream transformer that records the number of bytes
|
||||
* passed in its `size` property.
|
||||
*/
|
||||
class ByteCounter extends PassThrough {
|
||||
size: number = 0;
|
||||
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
||||
if ("length" in chunk) this.size += chunk.length;
|
||||
super._transform(chunk, encoding, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param data buffer containing the data to be compressed
|
||||
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
||||
* of the source data
|
||||
*/
|
||||
export async function deflateBuffer(data: Buffer) {
|
||||
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
||||
return { deflated, crc32: checksum };
|
||||
}
|
||||
|
||||
/**
|
||||
* @param input a byte stream, containing data to be compressed
|
||||
* @param sink a method that will accept chunks of compressed data; We don't pass
|
||||
* a writable here, since we don't want the writablestream to be closed after
|
||||
* a single file
|
||||
* @returns a promise, which will resolve with the crc32 checksum and the
|
||||
* compressed size
|
||||
*/
|
||||
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
||||
const deflateWriter = new Writable({
|
||||
write(chunk, _, callback) {
|
||||
sink(chunk);
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
// tee the input stream, so we can compress and calc crc32 in parallel
|
||||
const [rs1, rs2] = tee(input);
|
||||
const byteCounter = new ByteCounter();
|
||||
const [_, crc] = await Promise.all([
|
||||
// pipe input into zip compressor, count the bytes
|
||||
// returned and pass compressed data to the sink
|
||||
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
||||
// calc checksum
|
||||
crc32(rs2)
|
||||
]);
|
||||
|
||||
return { crc32: crc, compressedSize: byteCounter.size };
|
||||
}
|
@ -1,18 +1,16 @@
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
|
||||
export type FetchOptionsLike = {
|
||||
proxy: string | undefined;
|
||||
noProxy: string | string[];
|
||||
strictSSL: boolean;
|
||||
cert: string | string[] | undefined;
|
||||
ca: string[] | undefined;
|
||||
};
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
|
||||
export function getProxyFetchOptions(params: {
|
||||
npmConfigGetCwd: string;
|
||||
}): FetchOptionsLike {
|
||||
}): ProxyFetchOptions {
|
||||
const { npmConfigGetCwd } = params;
|
||||
|
||||
const cfg = (() => {
|
||||
|
@ -14,8 +14,6 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
||||
|
||||
let pathOut = pathIsh;
|
||||
|
||||
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||
|
||||
pathOut = pathOut.replace(/\//g, pathSep);
|
||||
|
||||
if (pathOut.startsWith("~")) {
|
||||
|
@ -1,53 +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";
|
||||
import { getIsRootPath } from "../tools/isRootPath";
|
||||
|
||||
export async function getInstalledModuleDirPath(params: {
|
||||
moduleName: string;
|
||||
packageJsonDirPath: string;
|
||||
}) {
|
||||
const { moduleName, packageJsonDirPath } = params;
|
||||
|
||||
{
|
||||
let dirPath = packageJsonDirPath;
|
||||
|
||||
while (true) {
|
||||
const dirPath_candidate = pathJoin(
|
||||
dirPath,
|
||||
"node_modules",
|
||||
...moduleName.split("/")
|
||||
);
|
||||
|
||||
let doesExist: boolean;
|
||||
|
||||
try {
|
||||
doesExist = await existsAsync(dirPath_candidate);
|
||||
} catch {
|
||||
doesExist = false;
|
||||
}
|
||||
|
||||
if (doesExist) {
|
||||
return dirPath_candidate;
|
||||
}
|
||||
|
||||
if (getIsRootPath(dirPath)) {
|
||||
break;
|
||||
}
|
||||
|
||||
dirPath = pathJoin(dirPath, "..");
|
||||
}
|
||||
}
|
||||
|
||||
const dirPath = child_process
|
||||
.execSync(`npm list ${moduleName}`, {
|
||||
cwd: packageJsonDirPath
|
||||
})
|
||||
.toString("utf8")
|
||||
.trim();
|
||||
|
||||
assert(dirPath !== "");
|
||||
|
||||
return dirPath;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import * as child_process from "child_process";
|
||||
import {
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename,
|
||||
join as pathJoin,
|
||||
sep as pathSep
|
||||
} from "path";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import * as fs from "fs";
|
||||
|
||||
export function getIsKnownByGit(params: { filePath: string }): Promise<boolean> {
|
||||
const { filePath } = params;
|
||||
|
||||
const dIsKnownByGit = new Deferred<boolean>();
|
||||
|
||||
let relativePath = pathBasename(filePath);
|
||||
|
||||
let dirPath = pathDirname(filePath);
|
||||
|
||||
while (!fs.existsSync(dirPath)) {
|
||||
relativePath = pathJoin(pathBasename(dirPath), relativePath);
|
||||
|
||||
dirPath = pathDirname(dirPath);
|
||||
}
|
||||
|
||||
child_process.exec(
|
||||
`git ls-files --error-unmatch '${relativePath.split(pathSep).join("/")}'`,
|
||||
{ cwd: dirPath },
|
||||
error => {
|
||||
if (error === null) {
|
||||
dIsKnownByGit.resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (error.code === 1) {
|
||||
dIsKnownByGit.resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
dIsKnownByGit.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return dIsKnownByGit.pr;
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
import { normalize as pathNormalize } from "path";
|
||||
|
||||
export function getIsRootPath(filePath: string): boolean {
|
||||
const path_normalized = pathNormalize(filePath);
|
||||
|
||||
// Unix-like root ("/")
|
||||
if (path_normalized === "/") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for Windows drive root (e.g., "C:\\")
|
||||
if (/^[a-zA-Z]:\\$/.test(path_normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for UNC root (e.g., "\\server\share")
|
||||
if (/^\\\\[^\\]+\\[^\\]+\\?$/.test(path_normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
@ -1,128 +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;
|
||||
filter: (params: { moduleName: string }) => boolean;
|
||||
}): Promise<
|
||||
{
|
||||
moduleName: string;
|
||||
version: string;
|
||||
dirPath: string;
|
||||
peerDependencies: Record<string, string>;
|
||||
}[]
|
||||
> {
|
||||
const { packageJsonFilePath, filter } = params;
|
||||
|
||||
const parsedPackageJson = await readPackageJsonDependencies({
|
||||
packageJsonFilePath
|
||||
});
|
||||
|
||||
const extensionModuleNames = (
|
||||
[parsedPackageJson.dependencies, parsedPackageJson.devDependencies] as const
|
||||
)
|
||||
.filter(exclude(undefined))
|
||||
.map(obj => Object.keys(obj))
|
||||
.flat()
|
||||
.filter(moduleName => filter({ moduleName }));
|
||||
|
||||
const result = await Promise.all(
|
||||
extensionModuleNames.map(async moduleName => {
|
||||
const dirPath = await getInstalledModuleDirPath({
|
||||
moduleName,
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||
});
|
||||
|
||||
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,170 +0,0 @@
|
||||
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||
import { getInstalledModuleDirPath } from "./getInstalledModuleDirPath";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs/promises";
|
||||
import { assert, is, type Equals } from "tsafe/assert";
|
||||
import { id } from "tsafe/id";
|
||||
|
||||
let cache_bestEffort: string | undefined = undefined;
|
||||
|
||||
/** NOTE: Careful, this function can fail when the binary
|
||||
* Used is not in the node_modules directory of the project
|
||||
* (for example when running tests with vscode extension we'll get
|
||||
* '/Users/dylan/.vscode/extensions/vitest.explorer-1.16.0/dist/worker.js'
|
||||
*
|
||||
* instead of
|
||||
* '/Users/joseph/.nvm/versions/node/v22.12.0/bin/node'
|
||||
* or
|
||||
* '/Users/joseph/github/keycloakify-starter/node_modules/.bin/vite'
|
||||
*
|
||||
* as the value of process.argv[1]
|
||||
*/
|
||||
function getNodeModulesBinDirPath_bestEffort() {
|
||||
if (cache_bestEffort !== undefined) {
|
||||
return cache_bestEffort;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (!foundNodeModules) {
|
||||
throw new Error(`Could not find node_modules in path ${binPath}`);
|
||||
}
|
||||
|
||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||
|
||||
cache_bestEffort = nodeModulesBinDirPath;
|
||||
|
||||
return nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
let cache_withPackageJsonFileDirPath:
|
||||
| { packageJsonFilePath: string; nodeModulesBinDirPath: string }
|
||||
| undefined = undefined;
|
||||
|
||||
async function getNodeModulesBinDirPath_withPackageJsonFileDirPath(params: {
|
||||
packageJsonFilePath: string;
|
||||
}): Promise<string> {
|
||||
const { packageJsonFilePath } = params;
|
||||
|
||||
use_cache: {
|
||||
if (cache_withPackageJsonFileDirPath === undefined) {
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
if (
|
||||
cache_withPackageJsonFileDirPath.packageJsonFilePath !== packageJsonFilePath
|
||||
) {
|
||||
cache_withPackageJsonFileDirPath = undefined;
|
||||
break use_cache;
|
||||
}
|
||||
|
||||
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
// [...]node_modules/keycloakify
|
||||
const installedModuleDirPath = await getInstalledModuleDirPath({
|
||||
// Here it will always be "keycloakify" but since we are in tools/ we make something generic
|
||||
moduleName: await (async () => {
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = (() => {
|
||||
type TargetType = ParsedPackageJson;
|
||||
|
||||
const zTargetType = z.object({
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||
|
||||
return id<z.ZodType<TargetType>>(zTargetType);
|
||||
})();
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
(
|
||||
await fs.readFile(
|
||||
pathJoin(getThisCodebaseRootDirPath(), "package.json")
|
||||
)
|
||||
).toString("utf8")
|
||||
);
|
||||
|
||||
zParsedPackageJson.parse(parsedPackageJson);
|
||||
|
||||
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||
|
||||
return parsedPackageJson.name;
|
||||
})(),
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||
});
|
||||
|
||||
const segments = installedModuleDirPath.split(pathSep);
|
||||
|
||||
while (true) {
|
||||
const segment = segments.pop();
|
||||
|
||||
if (segment === undefined) {
|
||||
throw new Error(
|
||||
`Could not find .bin directory relative to ${packageJsonFilePath}`
|
||||
);
|
||||
}
|
||||
|
||||
if (segment !== "node_modules") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate = pathJoin(segments.join(pathSep), segment, ".bin");
|
||||
|
||||
if (!(await existsAsync(candidate))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cache_withPackageJsonFileDirPath = {
|
||||
packageJsonFilePath,
|
||||
nodeModulesBinDirPath: candidate
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||
}
|
||||
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: string;
|
||||
}): Promise<string>;
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: undefined;
|
||||
}): string;
|
||||
export function getNodeModulesBinDirPath(params: {
|
||||
packageJsonFilePath: string | undefined;
|
||||
}): string | Promise<string> {
|
||||
const { packageJsonFilePath } = params ?? {};
|
||||
|
||||
return packageJsonFilePath === undefined
|
||||
? getNodeModulesBinDirPath_bestEffort()
|
||||
: getNodeModulesBinDirPath_withPackageJsonFileDirPath({ packageJsonFilePath });
|
||||
}
|
@ -1,17 +1,9 @@
|
||||
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 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";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
|
||||
export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
export function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
const { packageJsonDirPath } = params;
|
||||
|
||||
const packageManagerBinName = (() => {
|
||||
@ -31,10 +23,6 @@ export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
{
|
||||
binName: "bun",
|
||||
lockFileBasename: "bun.lockdb"
|
||||
},
|
||||
{
|
||||
binName: "deno",
|
||||
lockFileBasename: "deno.lock"
|
||||
}
|
||||
] as const;
|
||||
|
||||
@ -49,447 +37,27 @@ export async function npmInstall(params: { packageJsonDirPath: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"No lock file found, cannot tell which package manager to use for installing dependencies."
|
||||
);
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
console.log(`Installing the new dependencies...`);
|
||||
|
||||
install_without_breaking_links: {
|
||||
if (packageManagerBinName !== "yarn") {
|
||||
break install_without_breaking_links;
|
||||
install_dependencies: {
|
||||
if (packageManagerBinName === undefined) {
|
||||
break install_dependencies;
|
||||
}
|
||||
|
||||
const garronejLinkInfos = getGarronejLinkInfos({ packageJsonDirPath });
|
||||
|
||||
if (garronejLinkInfos === undefined) {
|
||||
break install_without_breaking_links;
|
||||
}
|
||||
|
||||
console.log(chalk.green("Installing in a way that won't break the links..."));
|
||||
|
||||
await installWithoutBreakingLinks({
|
||||
packageJsonDirPath,
|
||||
garronejLinkInfos
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await runPackageManagerInstall({
|
||||
packageManagerBinName,
|
||||
cwd: packageJsonDirPath
|
||||
});
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function runPackageManagerInstall(params: {
|
||||
packageManagerBinName: string;
|
||||
cwd: string;
|
||||
}) {
|
||||
const { packageManagerBinName, cwd } = params;
|
||||
|
||||
const dCompleted = new Deferred<void>();
|
||||
|
||||
const child = child_process.spawn(packageManagerBinName, ["install"], {
|
||||
cwd,
|
||||
env: process.env,
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => {
|
||||
if (data.toString("utf8").includes("peer dependency")) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.stderr.write(data);
|
||||
});
|
||||
|
||||
child.on("exit", code => {
|
||||
if (code !== 0) {
|
||||
dCompleted.reject(new Error(`Failed with code ${code}`));
|
||||
return;
|
||||
}
|
||||
|
||||
dCompleted.resolve();
|
||||
});
|
||||
|
||||
await dCompleted.pr;
|
||||
}
|
||||
|
||||
function getGarronejLinkInfos(params: {
|
||||
packageJsonDirPath: string;
|
||||
}): { linkedModuleNames: string[]; yarnHomeDirPath: string } | undefined {
|
||||
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;
|
||||
console.log(`Installing the new dependencies...`);
|
||||
|
||||
try {
|
||||
realPath = fs.readlinkSync(path);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async 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()
|
||||
child_process.execSync(`${packageManagerBinName} install`, {
|
||||
cwd: packageJsonDirPath,
|
||||
stdio: "inherit"
|
||||
});
|
||||
|
||||
type InferredType = z.infer<typeof zTargetType>;
|
||||
|
||||
assert<Equals<TargetType, InferredType>>;
|
||||
|
||||
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;
|
||||
} catch {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`\`${packageManagerBinName} install\` failed, continuing anyway...`
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
|
||||
await runPackageManagerInstall({
|
||||
packageManagerBinName: "yarn",
|
||||
cwd: tmpProjectDirPath
|
||||
});
|
||||
|
||||
// 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"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
47
src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
Normal file
47
src/bin/tools/octokit-addons/getLatestsSemVersionedTag.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { listTagsFactory } from "./listTags";
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import { SemVer } from "../SemVer";
|
||||
|
||||
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
|
||||
const { octokit } = params;
|
||||
|
||||
async function getLatestsSemVersionedTag(params: {
|
||||
owner: string;
|
||||
repo: string;
|
||||
count: number;
|
||||
doIgnoreReleaseCandidates: boolean;
|
||||
}): Promise<
|
||||
{
|
||||
tag: string;
|
||||
version: SemVer;
|
||||
}[]
|
||||
> {
|
||||
const { owner, repo, count, doIgnoreReleaseCandidates } = params;
|
||||
|
||||
const semVersionedTags: { tag: string; version: SemVer }[] = [];
|
||||
|
||||
const { listTags } = listTagsFactory({ octokit });
|
||||
|
||||
for await (const tag of listTags({ owner, repo })) {
|
||||
let version: SemVer;
|
||||
|
||||
try {
|
||||
version = SemVer.parse(tag.replace(/^[vV]?/, ""));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (doIgnoreReleaseCandidates && version.rc !== undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
semVersionedTags.push({ tag, version });
|
||||
}
|
||||
|
||||
return semVersionedTags
|
||||
.sort(({ version: vX }, { version: vY }) => SemVer.compare(vY, vX))
|
||||
.slice(0, count);
|
||||
}
|
||||
|
||||
return { getLatestsSemVersionedTag };
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user