Compare commits

..

124 Commits

Author SHA1 Message Date
aba725372e Release candidate 2024-06-18 22:41:08 +02:00
a61aa9dd5d Add missing fonts from the account theme's default assets 2024-06-18 16:41:09 +02:00
74349b20ce Adding missing font from default theme resources 2024-06-17 13:26:32 +02:00
09ab9a1c8f Fix storybook build 2024-06-17 13:03:39 +02:00
abfe5789a3 Publish new storybook 2024-06-17 12:53:06 +02:00
67ebac496d Release candidate 2024-06-17 00:07:53 +02:00
60a2bf173b Add missing base font face 2024-06-17 00:07:38 +02:00
4e03f07864 Do not includes all shared source, it's bundled already 2024-06-17 00:00:41 +02:00
aef1709d7f Release candidate 2024-06-16 18:27:37 +02:00
2f590f7be2 Add missing file in npm bundle 2024-06-16 18:27:18 +02:00
d5fa6ca89a Fix unit tests 2024-06-16 17:55:06 +02:00
8eaaffb25a Release candidate 2024-06-16 15:19:44 +02:00
28c5e2bab2 Rename use 'dist' instead of 'build' for basenameOfTheKeycloakifyResourcesDir 2024-06-16 15:19:27 +02:00
e212039f2c Release cadidate 2024-06-16 14:59:11 +02:00
99b0b67f77 Add publicDirpath option for webpack 2024-06-16 14:58:51 +02:00
6ec9ba3c01 Add version in build options 2024-06-16 14:53:18 +02:00
d7960a7dcf Release candidate 2024-06-16 14:05:38 +02:00
2a6e9af9c9 Enable to use an other directory than build/assets in webpack 2024-06-16 14:05:23 +02:00
327e4d1f90 Add doc link 2024-06-16 11:48:39 +02:00
fffadd7b9e Release candidate 2024-06-16 11:11:53 +02:00
aaaf0d2e77 Add missing declaration files 2024-06-16 11:11:35 +02:00
9f9a9b8c90 Release candidate 2024-06-16 02:30:09 +02:00
1f6edb3c0c Use the configured jar file basename if any 2024-06-16 02:19:56 +02:00
142efb4f99 Do leave artifact in the build directory when using start-keycloak 2024-06-16 01:41:47 +02:00
532655d2d5 Rename jarTargets -> keycloakVersionTargets 2024-06-16 01:34:06 +02:00
287edabd90 Enable to build only for specific keycloak version 2024-06-16 01:29:15 +02:00
7aaedbe2ce Release candidate 2024-06-15 17:40:51 +02:00
4cae1c673c Use getAlgorithmKey in account 2024-06-15 17:33:27 +02:00
8e01d836a9 Cherrypick what resource from the default theme we keep 2024-06-15 17:32:58 +02:00
f6dc8f0741 Memoize getImplementThemeTypes 2024-06-15 14:45:22 +02:00
3a976d08d2 Release candidate 2024-06-15 14:40:56 +02:00
50e83b1eb5 Only build for specific keycloak version in start-keycloak 2024-06-15 14:30:18 +02:00
61fbbb0b09 Refactor how we update META-INF and how we read what theme types are implemented 2024-06-15 14:23:35 +02:00
9e70e5c12e Suggest 'npm run' instead of 'yarn' to be more generic 2024-06-15 11:27:03 +02:00
69d9b64468 Use tsx instead of ts-node 2024-06-15 11:23:53 +02:00
0620d29880 spawn in shell in local scripts 2024-06-15 01:06:06 +02:00
b52dc74d9b Release candidate 2024-06-14 23:59:16 +02:00
a46aef2e7e Use shell for Window resolution of envs 2024-06-14 23:58:54 +02:00
736806a53d Relase candidate 2024-06-14 22:25:23 +02:00
f1475e5cdf Settle on calling the global 'kcContext' and reduce levels of indirections 2024-06-14 22:24:51 +02:00
d04724c70a fetchProxyOptions compatibility Window OS 2024-06-14 21:53:17 +02:00
bacaadc16d Remove dead file 2024-06-14 21:52:46 +02:00
c51dd235f0 Release candidate 2024-06-14 21:31:26 +02:00
92f2c9857e Fix the linking script 2024-06-14 21:31:03 +02:00
3998cc7f8b Fix for the linking script on windows OS 2024-06-14 20:45:52 +02:00
c126d080bc Make tests pass on windows OS 2024-06-14 19:06:48 +02:00
bc05f1714d Fix windows OS compatibility issue 2024-06-14 18:59:25 +02:00
e98becb94b Release candidate 2024-06-13 22:58:50 +02:00
250b94c8b5 Fix missing build option for webpack 2024-06-13 22:58:32 +02:00
47f03f6833 Improve stories 2024-06-13 00:47:18 +02:00
6e7ae48f78 Update sotry 2024-06-13 00:30:07 +02:00
526dbcc0e7 Improve stories 2024-06-12 23:22:21 +02:00
1abc5a5643 Release candidate 2024-06-12 23:11:46 +02:00
c81c350136 Improve mock and stories 2024-06-12 23:11:06 +02:00
f90dc8bc7e fix syntax error 2024-06-12 22:52:53 +02:00
072e22d072 Exclude kcContext.execution 2024-06-12 22:18:55 +02:00
59807c1bb0 Patch only required on the login page 2024-06-12 22:17:58 +02:00
7c19e1f1f7 Fix wrong condition for displaying error in the template 2024-06-12 21:38:48 +02:00
3b9f915f57 Fix logical error in generating pom file 2024-06-12 20:39:03 +02:00
d85cc530d4 remove debug log 2024-06-12 20:25:44 +02:00
2bb27c7642 More compact ftl output 2024-06-12 20:13:44 +02:00
e90e003204 Fully remove comments #542 2024-06-12 20:12:11 +02:00
b1e58e1add Refactor how userFromField is passed down to the client 2024-06-12 19:41:05 +02:00
0fd836314a Release candidate 2024-06-12 14:48:26 +02:00
0bc3f08cc1 Rename generateSrcMainResources -> generateResources 2024-06-12 14:48:08 +02:00
a78af5080a Fix environement variables all on the same line 2024-06-12 14:43:53 +02:00
074e465284 Release candidate 2024-06-12 12:02:13 +02:00
bc8165d0ae Fix usage of dirname instead of basename 2024-06-12 12:01:55 +02:00
ba8561d75a Release candidate 2024-06-12 10:50:13 +02:00
b2d381ba4b Apply the name of the theme in the preconfigured realm 2024-06-12 10:50:00 +02:00
d39353d332 Release candidate 2024-06-12 09:20:25 +02:00
ee916af48e Provide default message for the info page 2024-06-12 09:20:10 +02:00
da1dc0309b Release candidate 2024-06-12 08:57:59 +02:00
30f4e7d833 Add PasswordPolicies on every page where there's user profile 2024-06-12 08:57:40 +02:00
cf3a86fb9b Release candidate 2024-06-11 21:22:34 +02:00
e1633f43f4 Apply same strategy for UserProfileFormField than for TempateProps for extendability 2024-06-11 21:21:58 +02:00
5b64cfc23c Release candidate 2024-06-11 20:50:31 +02:00
19709cf085 Only types are capitalized 2024-06-11 20:50:11 +02:00
b8bb6c4f02 Fix build 2024-06-11 20:40:00 +02:00
b7a543f8cb Do not export PageProps in the index 2024-06-11 20:30:39 +02:00
04b4e19563 Release candidate 2024-06-11 20:27:53 +02:00
ffb27fc66d Extract Props from UserProfileFormFields so it's ejectable 2024-06-11 20:27:03 +02:00
8b5f7eefda Release candidate 2024-06-11 19:14:19 +02:00
c750bf4ee8 Export PageProps 2024-06-11 19:14:04 +02:00
aa74019ef6 Fix build 2024-06-11 19:08:36 +02:00
9be6d9f75f Release candidate 2024-06-11 17:27:40 +02:00
81ebb9b552 Prevent the jar to be corrupted when rebuild 2024-06-11 17:19:36 +02:00
5e13b8c41f Exclude Keycloak 22 from test panel 2024-06-11 17:12:12 +02:00
dd1ed948ec Update Keycloak 25 default realm config 2024-06-11 16:26:03 +02:00
8b93f701cf Add realms configurations for Keycloak majors 2024-06-11 16:19:54 +02:00
2f0084de5b Pass the input options translation to the kcContext 2024-06-11 16:10:54 +02:00
2ef9828625 Start with keycloak 18 for local container 2024-06-11 11:39:03 +02:00
89db8983a7 Fix exception in terms.ftl 2024-06-11 11:37:45 +02:00
287dd9bd31 Refactor + attributes with options rendered by default as select inputs 2024-06-11 09:22:50 +02:00
9a92054c1a Remove unused dependency 2024-06-10 21:06:02 +02:00
4189036213 Fix storybook 2024-06-10 21:05:17 +02:00
2c0a427ba5 Fix the script to export realm 2024-06-10 20:51:00 +02:00
77b488d624 Fix the formatNumber function 2024-06-10 20:14:14 +02:00
5249e05746 Release candidate 2024-06-10 19:36:11 +02:00
1e7a0dd7a6 Enable to add files to the jar with the post build options 2024-06-10 19:35:56 +02:00
fd67f2402a Release candidate 2024-06-10 17:30:20 +02:00
60a65ede2f Preserve ordering on user attributes 2024-06-10 17:30:00 +02:00
1fa659ce61 Release candidate 2024-06-10 16:01:56 +02:00
0ab903dbc7 Add new build target for Kc 25 https://github.com/p2-inc/keycloak-account-v1/pull/13 2024-06-10 15:29:08 +02:00
70b0a04793 Release candidate 2024-06-10 15:08:46 +02:00
c0df9aa939 Remove logs 2024-06-10 09:32:07 +02:00
60a1886942 Fix path error 2024-06-10 09:28:31 +02:00
1ebf97871b Fix logical error 2024-06-10 09:26:47 +02:00
72e321aa32 Fix update of the build process checkpoint 2024-06-10 09:24:16 +02:00
b0f602b565 Fix post build script 2024-06-10 09:12:24 +02:00
84c774503d Build rework checkpoint 2024-06-10 07:57:12 +02:00
9bbc7cc651 Release candidate 2024-06-09 15:04:47 +02:00
458083fb6d Prettier stable generated code 2024-06-09 15:04:31 +02:00
8dcfc840b4 Remove useless 'as const' 2024-06-09 14:34:41 +02:00
9d06a3a6ad Release candidate 2024-06-09 14:33:42 +02:00
86cd08b954 Add missing file to the NPM bundle 2024-06-09 14:33:29 +02:00
144c3cc082 Release candidate 2024-06-09 11:53:41 +02:00
802cef41a6 Rename KcApp to KcPage 2024-06-09 11:53:25 +02:00
e128e8f0a9 Release candidate 2024-06-09 11:25:05 +02:00
8a25b93ab2 Rename Fallback to DefaultPage 2024-06-09 11:24:50 +02:00
7a040935e9 i18n need to be passed as props if we want to be able to ovewrite 2024-06-09 11:20:45 +02:00
2015882688 Avoid loop rebuild in watch mode 2024-06-09 10:28:06 +02:00
379301eb9d Release candidate 2024-06-09 09:50:27 +02:00
5d86b05cdb Fix eject-page script 2024-06-09 09:50:02 +02:00
168 changed files with 13823 additions and 2346 deletions

View File

@ -16,8 +16,8 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again. - name: If this step fails run 'npm run format' then commit again.
run: yarn format:check run: npm run format:check
test: test:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: test_lint needs: test_lint
@ -32,13 +32,12 @@ jobs:
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build
- run: yarn test - run: npm run test
#- run: yarn test:keycloakify-starter
storybook: storybook:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' #if: github.event_name == 'push'
needs: test needs: test
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@ -46,11 +45,11 @@ jobs:
with: with:
node-version: '18' node-version: '18'
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook - run: npm run build-storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git - run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>" - run: npx -y -p gh-pages@3.1.0 gh-pages -d ./storybook-static -u "github-actions-bot <actions@github.com>"
check_if_version_upgraded: check_if_version_upgraded:
name: Check if version upgrade name: Check if version upgrade
@ -112,7 +111,7 @@ jobs:
with: with:
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: yarn build - run: npm run build
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path - run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
env: env:
DRY_RUN: "0" DRY_RUN: "0"

View File

@ -1,24 +1,24 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.39", "version": "10.0.0-rc.73",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git://github.com/keycloakify/keycloakify.git" "url": "git://github.com/keycloakify/keycloakify.git"
}, },
"scripts": { "scripts": {
"prepare": "patch-package && ts-node --skipProject scripts/generate-i18n-messages.ts", "prepare": "patch-package && tsx scripts/generate-i18n-messages.ts",
"build": "ts-node --skipProject scripts/build.ts", "build": "tsx scripts/build.ts",
"storybook": "ts-node --skipProject scripts/start-storybook.ts", "storybook": "tsx scripts/start-storybook.ts",
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts", "link-in-starter": "tsx scripts/link-in-starter.ts",
"test": "yarn test:types && vitest run", "test": "yarn test:types && vitest run",
"test:types": "tsc -p test/tsconfig.json --noEmit", "test:types": "tsc -p test/tsconfig.json --noEmit",
"_format": "prettier '**/*.{ts,tsx,json,md}'", "_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write", "format": "yarn _format --write",
"format:check": "yarn _format --list-different", "format:check": "yarn _format --list-different",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts", "link-in-app": "tsx scripts/link-in-app.ts",
"build-storybook": "ts-node --skipProject scripts/build-storybook.ts", "build-storybook": "tsx scripts/build-storybook.ts",
"dump-keycloak-realm": "ts-node --skipProject scripts/dump-keycloak-realm.ts" "dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
}, },
"bin": { "bin": {
"keycloakify": "dist/bin/main.js" "keycloakify": "dist/bin/main.js"
@ -41,9 +41,10 @@
"!dist/bin/", "!dist/bin/",
"dist/bin/main.js", "dist/bin/main.js",
"dist/bin/*.index.js", "dist/bin/*.index.js",
"!dist/bin/shared/*.js",
"dist/bin/shared/constants.js", "dist/bin/shared/constants.js",
"dist/bin/shared/constants.d.ts", "dist/bin/shared/*.d.ts",
"dist/bin/shared/constants.js.map", "dist/bin/shared/*.js.map",
"!dist/vite-plugin/", "!dist/vite-plugin/",
"dist/vite-plugin/index.d.ts", "dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts", "dist/vite-plugin/vite-plugin.d.ts",
@ -109,10 +110,8 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"recast": "^0.23.3", "recast": "^0.23.3",
"run-exclusive": "^2.2.19", "run-exclusive": "^2.2.19",
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2", "storybook-dark-mode": "^1.1.2",
"termost": "^0.12.0", "termost": "^0.12.0",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.10", "tsc-alias": "^1.8.10",
"tss-react": "^4.9.10", "tss-react": "^4.9.10",
"typescript": "^4.9.1-beta", "typescript": "^4.9.1-beta",
@ -120,6 +119,7 @@
"vitest": "^1.6.0", "vitest": "^1.6.0",
"yauzl": "^2.10.0", "yauzl": "^2.10.0",
"zod": "^3.17.10", "zod": "^3.17.10",
"evt": "^2.5.7" "evt": "^2.5.7",
"tsx": "^4.15.5"
} }
} }

View File

@ -3,40 +3,87 @@ import child_process from "child_process";
import { SemVer } from "../src/bin/tools/SemVer"; import { SemVer } from "../src/bin/tools/SemVer";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import chalk from "chalk"; import chalk from "chalk";
import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
run( (async () => {
[ {
`docker exec -it ${containerName}`, const dCompleted = new Deferred<void>();
`/opt/keycloak/bin/kc.sh export`,
`--dir /tmp`,
`--realm myrealm`,
`--users realm_file`
].join(" ")
);
const keycloakMajorVersionNumber = SemVer.parse( const child = child_process.spawn(
child_process "docker",
.execSync(`docker inspect --format '{{.Config.Image}}' ${containerName}`) [
.toString("utf8") ...["exec", containerName],
.trim() ...["/opt/keycloak/bin/kc.sh", "export"],
.split(":")[1] ...["--dir", "/tmp"],
).major; ...["--realm", "myrealm"],
...["--users", "realm_file"]
],
{ shell: true }
);
const targetFilePath = pathRelative( let output = "";
process.cwd(),
pathJoin(
__dirname,
"..",
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
)
);
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`); const onExit = (code: number | null) => {
dCompleted.reject(new Error(`Exited with code ${code}`));
};
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`); 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}}' ${containerName}`)
.toString("utf8")
.trim()
.split(":")[1]
).major;
const targetFilePath = pathRelative(
process.cwd(),
pathJoin(
__dirname,
"..",
"src",
"bin",
"start-keycloak",
`myrealm-realm-${keycloakMajorVersionNumber}.json`
)
);
run(`docker cp ${containerName}:/tmp/myrealm-realm.json ${targetFilePath}`);
console.log(`${chalk.green(`✓ Exported realm to`)} ${chalk.bold(targetFilePath)}`);
})();
function run(command: string) { function run(command: string) {
console.log(chalk.grey(`$ ${command}`)); console.log(chalk.grey(`$ ${command}`));

View File

@ -2,66 +2,53 @@ import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath"; import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import * as fs from "fs"; import * as fs from "fs";
import * as os from "os";
const singletonDependencies: string[] = ["react", "@types/react"]; const singletonDependencies: string[] = ["react", "@types/react"];
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
const rootDirPath = getThisCodebaseRootDirPath(); const rootDirPath = getThisCodebaseRootDirPath();
const commonThirdPartyDeps = [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58 //NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync( {
pathJoin(rootDirPath, "dist", "package.json"), let modifiedPackageJsonContent = fs
Buffer.from( .readFileSync(pathJoin(rootDirPath, "package.json"))
JSON.stringify( .toString("utf8");
(() => {
const packageJsonParsed = JSON.parse(
fs
.readFileSync(pathJoin(rootDirPath, "package.json"))
.toString("utf8")
);
return { modifiedPackageJsonContent = (() => {
...packageJsonParsed, const o = JSON.parse(modifiedPackageJsonContent);
main: packageJsonParsed["main"]?.replace(/^dist\//, ""),
types: packageJsonParsed["types"]?.replace(/^dist\//, ""),
module: packageJsonParsed["module"]?.replace(/^dist\//, ""),
exports: !("exports" in packageJsonParsed)
? undefined
: Object.fromEntries(
Object.entries(packageJsonParsed["exports"]).map(
([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
]
)
)
};
})(),
null,
2
),
"utf8"
)
);
const commonThirdPartyDeps = (() => { delete o.files;
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
return [ return JSON.stringify(o, null, 2);
...namespaceSingletonDependencies })();
.map(namespaceModuleName =>
fs modifiedPackageJsonContent = modifiedPackageJsonContent
.readdirSync( .replace(/"dist\//g, '"')
pathJoin(rootDirPath, "node_modules", namespaceModuleName) .replace(/"\.\/dist\//g, '"./')
) .replace(/"!dist\//g, '"!')
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`) .replace(/"!\.\/dist\//g, '"!./');
)
.reduce((prev, curr) => [...prev, ...curr], []), fs.writeFileSync(
...singletonDependencies pathJoin(rootDirPath, "dist", "package.json"),
]; Buffer.from(modifiedPackageJsonContent, "utf8")
})(); );
}
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home"); const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
@ -83,7 +70,9 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
cwd, cwd,
env: { env: {
...process.env, ...process.env,
HOME: yarnGlobalDirPath ...(os.platform() === "win32"
? { USERPROFILE: yarnGlobalDirPath }
: { HOME: yarnGlobalDirPath })
} }
}); });
}; };

View File

@ -17,7 +17,7 @@ fs.rmSync(join("..", "keycloakify-starter", "node_modules"), {
run("yarn install", { cwd: join("..", "keycloakify-starter") }); run("yarn install", { cwd: join("..", "keycloakify-starter") });
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`); run(`npx tsx ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
startRebuildOnSrcChange(); startRebuildOnSrcChange();

View File

@ -13,7 +13,9 @@ run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`,
}); });
{ {
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"]); const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
shell: true
});
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

View File

@ -13,7 +13,7 @@ export function startRebuildOnSrcChange() {
const dCompleted = new Deferred<void>(); const dCompleted = new Deferred<void>();
const child = child_process.spawn("yarn", ["build"]); const child = child_process.spawn("yarn", ["build"], { shell: true });
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

View File

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "keycloakify/bin/shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
/** /**
@ -9,7 +6,7 @@ import { assert } from "tsafe/assert";
* This works both in your main app and in your Keycloak theme. * This works both in your main app and in your Keycloak theme.
*/ */
export const PUBLIC_URL = (() => { export const PUBLIC_URL = (() => {
const kcContext = (window as any)[nameOfTheGlobal]; const kcContext = (window as any).kcContext;
if (kcContext === undefined || process.env.NODE_ENV === "development") { if (kcContext === undefined || process.env.NODE_ENV === "development") {
assert( assert(

View File

@ -1,7 +1,8 @@
import { lazy, Suspense } from "react"; import { lazy, Suspense } from "react";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "./KcContext"; import type { KcContext } from "keycloakify/account/KcContext";
import { I18n } from "keycloakify/account/i18n";
const Password = lazy(() => import("keycloakify/account/pages/Password")); const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account")); const Account = lazy(() => import("keycloakify/account/pages/Account"));
@ -11,7 +12,7 @@ const Applications = lazy(() => import("keycloakify/account/pages/Applications")
const Log = lazy(() => import("keycloakify/account/pages/Log")); const Log = lazy(() => import("keycloakify/account/pages/Log"));
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity")); const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
export default function Fallback(props: PageProps<KcContext>) { export default function DefaultPage(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props; const { kcContext, ...rest } = props;
return ( return (

View File

@ -166,6 +166,7 @@ export declare namespace KcContext {
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512"; algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number; digits: number;
lookAheadWindow: number; lookAheadWindow: number;
getAlgorithmKey: () => string;
} & ( } & (
| { | {
type: "totp"; type: "totp";

View File

@ -82,17 +82,7 @@ export const kcContextCommonMock: KcContext.Common = {
email: "john.doe@code.gouv.fr", email: "john.doe@code.gouv.fr",
username: "doe_j" username: "doe_j"
}, },
properties: { properties: {}
parent: "account-v1",
kcButtonLargeClass: "btn-lg",
locales:
"ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN",
kcButtonPrimaryClass: "btn-primary",
accountResourceProvider: "account-v1",
styles: "css/account.css img/icon-sidebar-active.png img/logo.png resources-common/node_modules/patternfly/dist/css/patternfly.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css resources-common/node_modules/patternfly/dist/css/patternfly-additions.min.css",
kcButtonClass: "btn",
kcButtonDefaultClass: "btn-default"
}
}; };
export const kcContextMocks: KcContext[] = [ export const kcContextMocks: KcContext[] = [
@ -158,7 +148,8 @@ export const kcContextMocks: KcContext[] = [
digits: 6, digits: 6,
lookAheadWindow: 1, lookAheadWindow: 1,
type: "totp", type: "totp",
period: 30 period: 30,
getAlgorithmKey: () => "SHA1"
} }
}, },
mode: "qr", mode: "qr",

View File

@ -5,15 +5,15 @@ import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags"; import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useSetClassName } from "keycloakify/tools/useSetClassName"; import { useSetClassName } from "keycloakify/tools/useSetClassName";
import type { TemplateProps } from "keycloakify/account/TemplateProps"; import type { TemplateProps } from "keycloakify/account/TemplateProps";
import type { I18n } from "./i18n";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
export default function Template(props: TemplateProps<KcContext>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcContext, doUseDefaultCss, active, classes, children } = props; const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes }); const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = useI18n({ kcContext }); const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext; const { locale, url, features, realm, message, referrer } = kcContext;

View File

@ -1,12 +1,13 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { KcContext } from "./KcContext";
export type TemplateProps<KcContext extends KcContext.Common> = { export type TemplateProps<KcContext, I18n> = {
kcContext: KcContext; kcContext: KcContext;
i18n: I18n;
doUseDefaultCss: boolean; doUseDefaultCss: boolean;
active: string;
classes?: Partial<Record<ClassKey, string>>; classes?: Partial<Record<ClassKey, string>>;
children: ReactNode; children: ReactNode;
active: string;
}; };
export type ClassKey = export type ClassKey =

View File

@ -130,7 +130,7 @@ function createGetI18n<ExtraMessageKey extends string = never>(extraMessages: {
extraMessages: extraMessages[partialI18n.currentLanguageTag] extraMessages: extraMessages[partialI18n.currentLanguageTag]
}); });
const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag !== fallbackLanguageTag; const isCurrentLanguageFallbackLanguage = partialI18n.currentLanguageTag === fallbackLanguageTag;
const result: Result = { const result: Result = {
i18n: { i18n: {
@ -175,7 +175,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
const { getI18n } = createGetI18n(extraMessages); const { getI18n } = createGetI18n(extraMessages);
function useI18n(params: { kcContext: KcContextLike }): I18n { function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params; const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
@ -198,7 +198,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
}; };
}, []); }, []);
return i18n_toReturn; return { i18n: i18n_toReturn };
} }
return { useI18n, ofTypeI18n: Reflect<I18n>() }; return { useI18n, ofTypeI18n: Reflect<I18n>() };

View File

@ -1,10 +1,5 @@
export type { MessageKey } from "./i18n"; import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
import { createUseI18n } from "./i18n"; export type { MessageKey, KcContextLike };
export { createUseI18n }; export type I18n = GenericI18n<MessageKey>;
export { createUseI18n } from "./i18n";
export { fallbackLanguageTag } from "./i18n"; export { fallbackLanguageTag } from "./i18n";
const { useI18n, ofTypeI18n } = createUseI18n({});
export type I18n = typeof ofTypeI18n;
export { useI18n };

View File

@ -2,10 +2,10 @@ import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>>) { export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template } = props; const { kcContext, i18n, doUseDefaultCss, Template } = props;
const classes = { const classes = {
...props.classes, ...props.classes,
@ -19,10 +19,10 @@ export default function Account(props: PageProps<Extract<KcContext, { pageId: "a
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext; const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="account"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="account">
<div className="row"> <div className="row">
<div className="col-md-10"> <div className="col-md-10">
<h2>{msg("editAccountHtmlTitle")}</h2> <h2>{msg("editAccountHtmlTitle")}</h2>

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>>) { export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, classes, Template } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -17,10 +17,10 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
stateChecker stateChecker
} = kcContext; } = kcContext;
const { msg, advancedMsg } = useI18n({ kcContext }); const { msg, advancedMsg } = i18n;
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="applications"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="applications">
<div className="row"> <div className="row">
<div className="col-md-10"> <div className="col-md-10">
<h2>{msg("applicationsHtmlTitle")}</h2> <h2>{msg("applicationsHtmlTitle")}</h2>

View File

@ -1,14 +1,14 @@
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>>) { export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, classes, Template } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, federatedIdentity, stateChecker } = kcContext; const { url, federatedIdentity, stateChecker } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="federatedIdentity"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity">
<div className="main-layout social"> <div className="main-layout social">
<div className="row"> <div className="row">
<div className="col-md-10"> <div className="col-md-10">

View File

@ -2,10 +2,10 @@ import type { Key } from "react";
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>>) { export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, classes, Template } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -14,10 +14,10 @@ export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.f
const { log } = kcContext; const { log } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="log"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="log">
<div className={kcClsx("kcContentWrapperClass")}> <div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10"> <div className="col-md-10">
<h2>{msg("accountLogHtmlTitle")}</h2> <h2>{msg("accountLogHtmlTitle")}</h2>

View File

@ -1,10 +1,10 @@
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps"; import { type TemplateProps, type ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { KcContext } from "../KcContext";
export type PageProps<NarrowedKcContext = KcContext> = { export type PageProps<NarrowedKcContext, I18n> = {
Template: LazyOrNot<(props: TemplateProps<any>) => JSX.Element | null>; Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: NarrowedKcContext; kcContext: NarrowedKcContext;
i18n: I18n;
doUseDefaultCss: boolean; doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>; classes?: Partial<Record<ClassKey, string>>;
}; };

View File

@ -3,10 +3,10 @@ import { clsx } from "keycloakify/tools/clsx";
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>>) { export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template } = props; const { kcContext, i18n, doUseDefaultCss, Template } = props;
const classes = { const classes = {
...props.classes, ...props.classes,
@ -20,7 +20,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
const { url, password, account, stateChecker } = kcContext; const { url, password, account, stateChecker } = kcContext;
const { msgStr, msg } = useI18n({ kcContext }); const { msgStr, msg } = i18n;
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
@ -77,6 +77,7 @@ export default function Password(props: PageProps<Extract<KcContext, { pageId: "
return kcContext.message; return kcContext.message;
})() })()
}, },
i18n,
doUseDefaultCss, doUseDefaultCss,
classes classes
}} }}

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>>) { export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,9 +13,9 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
const { url, stateChecker, sessions } = kcContext; const { url, stateChecker, sessions } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="sessions"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
<div className={kcClsx("kcContentWrapperClass")}> <div className={kcClsx("kcContentWrapperClass")}>
<div className="col-md-10"> <div className="col-md-10">
<h2>{msg("sessionsHtmlTitle")}</h2> <h2>{msg("sessionsHtmlTitle")}</h2>

View File

@ -2,10 +2,10 @@ import { clsx } from "keycloakify/tools/clsx";
import { getKcClsx } from "keycloakify/account/lib/kcClsx"; import { getKcClsx } from "keycloakify/account/lib/kcClsx";
import type { PageProps } from "keycloakify/account/pages/PageProps"; import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>>) { export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -14,16 +14,10 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
const { totp, mode, url, messagesPerField, stateChecker } = kcContext; const { totp, mode, url, messagesPerField, stateChecker } = kcContext;
const { msg, msgStr, advancedMsg } = useI18n({ kcContext }); const { msg, msgStr, advancedMsg } = i18n;
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1",
HmacSHA256: "SHA256",
HmacSHA512: "SHA512"
};
return ( return (
<Template {...{ kcContext, doUseDefaultCss, classes }} active="totp"> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="totp">
<> <>
<div className="row"> <div className="row">
<div className="col-md-10"> <div className="col-md-10">
@ -100,7 +94,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)} {msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li> </li>
<li id="kc-totp-algorithm"> <li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} {msg("totpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li> </li>
<li id="kc-totp-digits"> <li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits} {msg("totpDigits")}: {totp.policy.digits}

View File

@ -13,7 +13,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -53,17 +52,13 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageId}`); console.log(`${pageId}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace( const componentBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(
/ftl$/, /ftl$/,
"stories.tsx" "stories.tsx"
); );
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
"pages", "pages",
componentBasename componentBasename
@ -103,7 +98,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`${chalk.green("✓")} ${chalk.bold( `${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath)) pathJoin(".", pathRelative(process.cwd(), targetFilePath))
)} copy pasted from the Keycloakify source code into your project`, )} copy pasted from the Keycloakify source code into your project`,
`You can start storybook with ${chalk.bold("yarn storybook")}` `You can start storybook with ${chalk.bold("npm run storybook")}`
].join("\n") ].join("\n")
); );
} }

View File

@ -1,63 +0,0 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext";
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "./tools/transformCodebase";
import type { CliCommandOptions } from "./main";
import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildContext = getBuildContext({
cliCommandOptions
});
console.log(
chalk.cyan(
"Select the Keycloak version from which you want to download the builtins theme:"
)
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: undefined,
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
const destDirPath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
console.log(
[
`Downloading builtins theme of Keycloak ${keycloakVersion} here:`,
`- ${chalk.bold(
`.${pathSep}${pathJoin(pathRelative(process.cwd(), destDirPath), "base")}`
)}`,
`- ${chalk.bold(
`.${pathSep}${pathJoin(
pathRelative(process.cwd(), destDirPath),
"keycloak"
)}`
)}`
].join("\n")
);
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion,
buildContext
});
transformCodebase({
srcDirPath: defaultThemeDirPath,
destDirPath
});
console.log(chalk.green(`✓ done`));
}

View File

@ -15,7 +15,6 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase"; import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert"; import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import chalk from "chalk"; import chalk from "chalk";
@ -68,10 +67,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log(`${pageIdOrComponent}`); console.log(`${pageIdOrComponent}`);
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const componentBasename = (() => { const componentBasename = (() => {
if (pageIdOrComponent === templateValue) { if (pageIdOrComponent === templateValue) {
return "Template.tsx"; return "Template.tsx";
@ -96,7 +91,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
})(); })();
const targetFilePath = pathJoin( const targetFilePath = pathJoin(
themeSrcDirPath, buildContext.themeSrcDirPath,
themeType, themeType,
pagesOrDot, pagesOrDot,
componentBasename componentBasename
@ -149,7 +144,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
break edit_KcApp; break edit_KcApp;
} }
const kcAppTsxPath = pathJoin(themeSrcDirPath, themeType, "KcApp.tsx"); const kcAppTsxPath = pathJoin(
buildContext.themeSrcDirPath,
themeType,
"KcPage.tsx"
);
const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8"); const kcAppTsxCode = fs.readFileSync(kcAppTsxPath).toString("utf8");
@ -172,7 +171,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
if (kcAppTsxCode === modifiedKcAppTsxCode) { if (kcAppTsxCode === modifiedKcAppTsxCode) {
console.log( console.log(
chalk.red( chalk.red(
"Unable to automatically update KcApp.tsx, please update it manually" "Unable to automatically update KcPage.tsx, please update it manually"
) )
); );
return; return;
@ -199,9 +198,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
`${chalk.bold( `${chalk.bold(
pathJoin( pathJoin(
".", ".",
pathRelative(process.cwd(), themeSrcDirPath), pathRelative(process.cwd(), buildContext.themeSrcDirPath),
themeType, themeType,
"KcApp.tsx" "KcPage.tsx"
) )
)}:`, )}:`,
chalk.grey("```"), chalk.grey("```"),
@ -215,7 +214,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
), ),
...[ ...[
``, ``,
` export default function KcApp(props: { kcContext: KcContext; }) {`, ` export default function KcPage(props: { kcContext: KcContext; }) {`,
``, ``,
` // ...`, ` // ...`,
``, ``,
@ -225,15 +224,16 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
` switch (kcContext.pageId) {`, ` switch (kcContext.pageId) {`,
` // ...`, ` // ...`,
`+ case "${pageIdOrComponent}": return (`, `+ case "${pageIdOrComponent}": return (`,
`+ <Login`, `+ <${componentBasename}`,
`+ {...{ kcContext, i18n, classes }}`, `+ {...{ kcContext, i18n, classes }}`,
`+ Template={Template}`, `+ Template={Template}`,
`+ doUseDefaultCss={true}`,
...(!componentCode.includes(userProfileFormFieldComponentName) ...(!componentCode.includes(userProfileFormFieldComponentName)
? [] ? []
: [ : [
`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}` `+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`,
`+ doMakeUserConfirmPassword={doMakeUserConfirmPassword}`
]), ]),
`+ doUseDefaultCss={true}`,
`+ />`, `+ />`,
`+ );`, `+ );`,
` default: return <Fallback /* .. */ />;`, ` default: return <Fallback /* .. */ />;`,

View File

@ -4,7 +4,6 @@ import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { getBuildContext } from "./shared/buildContext"; import { getBuildContext } from "./shared/buildContext";
import * as fs from "fs"; import * as fs from "fs";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main"; import type { CliCommandOptions } from "./main";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
@ -12,11 +11,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { themeSrcDirPath } = getThemeSrcDirPath({ const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
projectDirPath: buildContext.projectDirPath
});
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) { if (fs.existsSync(emailThemeSrcDirPath)) {
console.warn( console.warn(
@ -34,6 +29,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const { keycloakVersion } = await promptKeycloakVersion({ const { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary // NOTE: This is arbitrary
startingFromMajor: 17, startingFromMajor: 17,
excludeMajorVersions: [],
cacheDirPath: buildContext.cacheDirPath cacheDirPath: buildContext.cacheDirPath
}); });

View File

@ -16,7 +16,7 @@ import { readFileSync } from "fs";
import { isInside } from "../../tools/isInside"; import { isInside } from "../../tools/isInside";
import child_process from "child_process"; import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { getMetaInfKeycloakThemesJsonFilePath } from "../../shared/metaInfKeycloakThemes"; import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
export type BuildContextLike = BuildContextLike_generatePom & { export type BuildContextLike = BuildContextLike_generatePom & {
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
@ -32,12 +32,14 @@ export async function buildJar(params: {
jarFileBasename: string; jarFileBasename: string;
keycloakAccountV1Version: KeycloakAccountV1Version; keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion; keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
resourcesDirPath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { const {
jarFileBasename, jarFileBasename,
keycloakAccountV1Version, keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
resourcesDirPath,
buildContext buildContext
} = params; } = params;
@ -48,35 +50,17 @@ export async function buildJar(params: {
rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true }); rmSync(keycloakifyBuildTmpDirPath, { recursive: true, force: true });
{ const tmpResourcesDirPath = pathJoin(
const transformCodebase_common = (params: { keycloakifyBuildTmpDirPath,
fileRelativePath: string; "src",
sourceCode: Buffer; "main",
}): { modifiedSourceCode: Buffer } | undefined => { "resources"
const { fileRelativePath, sourceCode } = params; );
if ( transformCodebase({
fileRelativePath === srcDirPath: resourcesDirPath,
getMetaInfKeycloakThemesJsonFilePath({ keycloakifyBuildDirPath: "." }) destDirPath: tmpResourcesDirPath,
) { transformSourceCode:
return { modifiedSourceCode: sourceCode };
}
for (const themeName of [...buildContext.themeNames, accountV1ThemeName]) {
if (
isInside({
dirPath: pathJoin("src", "main", "resources", "theme", themeName),
filePath: fileRelativePath
})
) {
return { modifiedSourceCode: sourceCode };
}
}
return undefined;
};
const transformCodebase_patchForUsingBuiltinAccountV1 =
keycloakAccountV1Version !== null keycloakAccountV1Version !== null
? undefined ? undefined
: (params: { : (params: {
@ -87,56 +71,17 @@ export async function buildJar(params: {
if ( if (
isInside({ isInside({
dirPath: pathJoin( dirPath: pathJoin("theme", accountV1ThemeName),
"src",
"main",
"resources",
"theme",
accountV1ThemeName
),
filePath: fileRelativePath filePath: fileRelativePath
}) })
) { ) {
return undefined; return undefined;
} }
if (
fileRelativePath ===
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath: "."
})
) {
const keycloakThemesJsonParsed = JSON.parse(
sourceCode.toString("utf8")
) as {
themes: { name: string; types: string[] }[];
};
keycloakThemesJsonParsed.themes =
keycloakThemesJsonParsed.themes.filter(
({ name }) => name !== accountV1ThemeName
);
return {
modifiedSourceCode: Buffer.from(
JSON.stringify(keycloakThemesJsonParsed, null, 2),
"utf8"
)
};
}
for (const themeName of buildContext.themeNames) { for (const themeName of buildContext.themeNames) {
if ( if (
fileRelativePath === fileRelativePath ===
pathJoin( pathJoin("theme", themeName, "account", "theme.properties")
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
)
) { ) {
const modifiedSourceCode = Buffer.from( const modifiedSourceCode = Buffer.from(
sourceCode sourceCode
@ -157,28 +102,20 @@ export async function buildJar(params: {
} }
return { modifiedSourceCode: sourceCode }; return { modifiedSourceCode: sourceCode };
}; }
});
transformCodebase({ if (keycloakAccountV1Version === null) {
srcDirPath: buildContext.keycloakifyBuildDirPath, writeMetaInfKeycloakThemes({
destDirPath: keycloakifyBuildTmpDirPath, resourcesDirPath: tmpResourcesDirPath,
transformSourceCode: params => { getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
const resultCommon = transformCodebase_common(params); assert(metaInfKeycloakTheme !== undefined);
if (transformCodebase_patchForUsingBuiltinAccountV1 === undefined) { metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter(
return resultCommon; ({ name }) => name !== accountV1ThemeName
} );
if (resultCommon === undefined) { return metaInfKeycloakTheme;
return undefined;
}
const { modifiedSourceCode } = resultCommon;
return transformCodebase_patchForUsingBuiltinAccountV1({
...params,
sourceCode: modifiedSourceCode
});
} }
}); });
} }
@ -228,8 +165,8 @@ export async function buildJar(params: {
})(); })();
const modifiedFtlFileContent = ftlFileContent.replace( const modifiedFtlFileContent = ftlFileContent.replace(
`out["pageId"] = "\${pageId}";`, `kcContext.pageId = "\${pageId}";`,
`out["pageId"] = "${pageId}"; out["realPageId"] = "${realPageId}";` `kcContext.pageId = "${pageId}"; kcContext.realPageId = "${realPageId}";`
); );
assert(modifiedFtlFileContent !== ftlFileContent); assert(modifiedFtlFileContent !== ftlFileContent);

View File

@ -1,5 +1,4 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { exclude } from "tsafe/exclude";
import { import {
keycloakAccountV1Versions, keycloakAccountV1Versions,
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions
@ -7,30 +6,29 @@ import {
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar"; import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar"; import { buildJar, BuildContextLike as BuildContextLike_buildJar } from "./buildJar";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants";
export type BuildContextLike = BuildContextLike_buildJar & { export type BuildContextLike = BuildContextLike_buildJar & {
projectDirPath: string;
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
jarTargets: BuildContext["jarTargets"];
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function buildJars(params: { export async function buildJars(params: {
resourcesDirPath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { resourcesDirPath, buildContext } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes({ const doesImplementAccountTheme = buildContext.recordIsImplementedByThemeType.account;
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
await Promise.all( await Promise.all(
keycloakAccountV1Versions keycloakAccountV1Versions
.map(keycloakAccountV1Version => .map(keycloakAccountV1Version =>
keycloakThemeAdditionalInfoExtensionVersions keycloakThemeAdditionalInfoExtensionVersions.map(
.map(keycloakThemeAdditionalInfoExtensionVersion => { keycloakThemeAdditionalInfoExtensionVersion => {
const keycloakVersionRange = getKeycloakVersionRangeForJar({ const keycloakVersionRange = getKeycloakVersionRangeForJar({
doesImplementAccountTheme, doesImplementAccountTheme,
keycloakAccountV1Version, keycloakAccountV1Version,
@ -41,39 +39,26 @@ export async function buildJars(params: {
return undefined; return undefined;
} }
return { const jarTarget = buildContext.jarTargets.find(
keycloakThemeAdditionalInfoExtensionVersion, jarTarget =>
keycloakVersionRange jarTarget.keycloakVersionRange === keycloakVersionRange
}; );
})
.filter(exclude(undefined))
.map(
({
keycloakThemeAdditionalInfoExtensionVersion,
keycloakVersionRange
}) => {
const { jarFileBasename } = getJarFileBasename({
keycloakVersionRange
});
return { if (jarTarget === undefined) {
keycloakThemeAdditionalInfoExtensionVersion, return undefined;
jarFileBasename
};
} }
)
.map( const { jarFileBasename } = jarTarget;
({
return buildJar({
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion, keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename resourcesDirPath,
}) => buildContext
buildJar({ });
jarFileBasename, }
keycloakAccountV1Version, )
keycloakThemeAdditionalInfoExtensionVersion,
buildContext
})
)
) )
.flat() .flat()
); );

View File

@ -1,5 +1,5 @@
// NOTE: v0.5 is a dummy version. // NOTE: v0.5 is a dummy version.
export const keycloakAccountV1Versions = [null, "0.3", "0.4"] as const; export const keycloakAccountV1Versions = [null, "0.3", "0.4", "0.6"] as const;
/** /**
* https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1 * https://central.sonatype.com/artifact/io.phasetwo.keycloak/keycloak-account-v1

View File

@ -42,7 +42,7 @@ export function generatePom(params: {
` <properties>`, ` <properties>`,
` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`, ` <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>`,
` </properties>`, ` </properties>`,
...(keycloakAccountV1Version !== null && ...(keycloakAccountV1Version !== null ||
keycloakThemeAdditionalInfoExtensionVersion !== null keycloakThemeAdditionalInfoExtensionVersion !== null
? [ ? [
` <build>`, ` <build>`,

View File

@ -44,12 +44,20 @@ export function getKeycloakVersionRangeForJar(params: {
case null: case null:
return undefined; return undefined;
case "1.1.5": case "1.1.5":
return "24-and-above" as const; return "24" as const;
} }
assert< assert<
Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never> Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>
>(false); >(false);
case "0.6":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "25-and-above" as const;
}
} }
assert<Equals<typeof keycloakAccountV1Version, never>>(false);
})(); })();
assert< assert<
@ -65,7 +73,6 @@ export function getKeycloakVersionRangeForJar(params: {
if (keycloakAccountV1Version !== null) { if (keycloakAccountV1Version !== null) {
return undefined; return undefined;
} }
switch (keycloakThemeAdditionalInfoExtensionVersion) { switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null: case null:
return "21-and-below"; return "21-and-below";

View File

@ -8,7 +8,6 @@ import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { import {
type ThemeType, type ThemeType,
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir, basenameOfTheKeycloakifyResourcesDir,
resources_common, resources_common,
nameOfTheLocalizationRealmOverridesUserProfileProperty nameOfTheLocalizationRealmOverridesUserProfileProperty
@ -116,7 +115,7 @@ export function generateFtlFilesCodeFactory(params: {
} }
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later. //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlObjectToJsCodeDeclaringAnObject = fs const kcContextDeclarationTemplateFtl = fs
.readFileSync( .readFileSync(
pathJoin( pathJoin(
getThisCodebaseRootDirPath(), getThisCodebaseRootDirPath(),
@ -124,11 +123,10 @@ export function generateFtlFilesCodeFactory(params: {
"bin", "bin",
"keycloakify", "keycloakify",
"generateFtl", "generateFtl",
"ftl_object_to_js_code_declaring_an_object.ftl" "kcContextDeclarationTemplate.ftl"
) )
) )
.toString("utf8") .toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace( .replace(
"FIELD_NAMES_eKsIY4ZsZ4xeM", "FIELD_NAMES_eKsIY4ZsZ4xeM",
fieldNames.map(name => `"${name}"`).join(", ") fieldNames.map(name => `"${name}"`).join(", ")
@ -150,7 +148,7 @@ export function generateFtlFilesCodeFactory(params: {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }'; '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
$("head").prepend( $("head").prepend(
`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>` `<script>\n${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}\n</script>`
); );
// Remove part of the document marked as ignored. // Remove part of the document marked as ignored.
@ -189,7 +187,7 @@ export function generateFtlFilesCodeFactory(params: {
Object.entries({ Object.entries({
[ftlObjectToJsCodeDeclaringAnObjectPlaceholder]: [ftlObjectToJsCodeDeclaringAnObjectPlaceholder]:
ftlObjectToJsCodeDeclaringAnObject, kcContextDeclarationTemplateFtl,
PAGE_ID_xIgLsPgGId9D8e: pageId PAGE_ID_xIgLsPgGId9D8e: pageId
}).map( }).map(
([searchValue, replaceValue]) => ([searchValue, replaceValue]) =>

View File

@ -1,172 +1,40 @@
<script>const _=
(()=>{
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e"> <#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc}; const kcContext = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["messagesPerField"]= { if( kcContext.messagesPerField ){
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]> var existsError_singleFieldName = kcContext.messagesPerField.existsError;
<#attempt> kcContext.messagesPerField.existsError = function (){
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#recover>
</#attempt>
"printIfExists": function (fieldName, text) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>text<#else>undefined</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"existsError": function (){
function existsError_singleFieldName(fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
}
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
if( existsError_singleFieldName(arguments[i]) ){ if( existsError_singleFieldName(arguments[i]) ){
return true; return true;
} }
} }
return false; return false;
}, };
"get": function (fieldName) { kcContext.messagesPerField.exists = function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)> return kcContext.messagesPerField.get(fieldName) !== "";
throw new Error("You're not supposed to use messagesPerField.get in this page"); };
<#else> kcContext.messagesPerField.printIfExists = function (fieldName, text) {
<#list fieldNames as fieldName> return kcContext.messagesPerField.exists(fieldName) ? text : undefined;
if(fieldName === "${fieldName}" ){ };
<#-- https://github.com/keycloakify/keycloakify/pull/218 --> kcContext.messagesPerField.getFirstError = function () {
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return decodeHtmlEntities("${msg('invalidUserMessage')?js_string}");
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
return "";
</#if>
<#else>
<#attempt>
return decodeHtmlEntities("${messagesPerField.get('${fieldName}')?js_string}");
<#recover>
return "Invalid field";
</#attempt>
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.exists in this page");
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
}
</#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if>
},
"getFirstError": function () {
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
const fieldName = arguments[i]; const fieldName = arguments[i];
if( out.messagesPerField.existsError(fieldName) ){ if( kcContext.messagesPerField.existsError(fieldName) ){
return out.messagesPerField.get(fieldName); return kcContext.messagesPerField.get(fieldName);
} }
} }
} };
}; }
kcContext.keycloakifyVersion = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr"; kcContext.themeVersion = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx"; kcContext.themeType = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr"; kcContext.themeName = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer"; kcContext.pageId = "${pageId}";
out["pageId"] = "${pageId}"; if( kcContext.url && kcContext.url.resourcesPath ){
kcContext.url.resourcesCommonPath = kcContext.url.resourcesPath + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
try { }
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} catch(error) { }
<#if profile?? && profile.attributes??> <#if profile?? && profile.attributes??>
out["lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX"] = { kcContext.lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX = {
<#list profile.attributes as attribute> <#list profile.attributes as attribute>
<#if attribute.annotations?? && attribute.displayName??> <#if attribute.annotations?? && attribute.displayName??>
"${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"), "${attribute.displayName}": decodeHtmlEntities("${advancedMsg(attribute.displayName)?js_string}"),
@ -180,12 +48,34 @@ try {
<#if attribute.annotations.inputTypePlaceholder??> <#if attribute.annotations.inputTypePlaceholder??>
"${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"), "${attribute.annotations.inputTypePlaceholder}": decodeHtmlEntities("${advancedMsg(attribute.annotations.inputTypePlaceholder)?js_string}"),
</#if> </#if>
<!-- Loop through the options that are in attribute.validators.options.options -->
<#if (
attribute.annotations.inputOptionLabelsI18nPrefix?? &&
attribute.validators?? &&
attribute.validators.options??
)>
<#list attribute.validators.options.options as option>
"${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}": decodeHtmlEntities("${msg(attribute.annotations.inputOptionLabelsI18nPrefix + "." + option)?js_string}"),
</#list>
</#if>
</#list> </#list>
}; };
</#if> </#if>
attributes_to_attributesByName: {
return out; if( !kcContext.profile ){
break attributes_to_attributesByName;
}
if( !kcContext.profile.attributes ){
break attributes_to_attributesByName;
}
var attributes = kcContext.profile.attributes;
delete kcContext.profile.attributes;
kcContext.profile.attributesByName = {};
attributes.forEach(function(attribute){
kcContext.profile.attributesByName[attribute.name] = attribute;
});
}
window.kcContext = kcContext;
function decodeHtmlEntities(htmlStr){ function decodeHtmlEntities(htmlStr){
var element = decodeHtmlEntities.element; var element = decodeHtmlEntities.element;
if (!element) { if (!element) {
@ -196,7 +86,6 @@ function decodeHtmlEntities(htmlStr){
return element.value; return element.value;
} }
})();
<#function ftl_object_to_js_code_declaring_an_object object path> <#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = ""> <#local isHash = "">
@ -287,16 +176,31 @@ function decodeHtmlEntities(htmlStr){
key == "realmAttributes" && key == "realmAttributes" &&
are_same_path(path, []) are_same_path(path, [])
) || ( ) || (
<#-- attributesByName adds a lot of noise to the output and is not needed --> <#-- attributesByName adds a lot of noise to the output and is not needed, we already have profile.attributes -->
key == "attributes" && key == "attributesByName" &&
are_same_path(path, ["profile"]) are_same_path(path, ["profile"])
) || ( ) || (
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object --> <#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
(key == "attributes" || key == "attributesByName") && (key == "attributes" || key == "attributesByName") &&
are_same_path(path, ["register"]) are_same_path(path, ["register"])
) || (
are_same_path(path, ["properties"]) &&
(
key?starts_with("kc") ||
key == "locales" ||
key == "import" ||
key == "parent" ||
key == "meta" ||
key == "stylesCommon" ||
key == "styles" ||
key == "accountResourceProvider"
)
) || (
key == "execution" &&
are_same_path(path, [])
) )
> >
<#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]> <#-- <#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
<#continue> <#continue>
</#if> </#if>
@ -304,7 +208,7 @@ function decodeHtmlEntities(htmlStr){
<#-- https://github.com/keycloakify/keycloakify/discussions/406 --> <#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if ( <#if (
["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) && ["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
key == "attemptedUsername" && are_same_path(path, ["auth"]) key == "attemptedUsername" && are_same_path(path, ["auth"])
)> )>
<#attempt> <#attempt>
@ -401,15 +305,121 @@ function decodeHtmlEntities(htmlStr){
</#if> </#if>
<#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])> <#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])>
<#local returnValue = ""> <#local returnValue = "error">
<#attempt> <#if mode?? && mode = "manual">
<#local returnValue = totp.policy.getAlgorithmKey()> <#attempt>
<#recover> <#local returnValue = totp.policy.getAlgorithmKey()>
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()"> <#recover>
</#attempt> <#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
</#attempt>
</#if>
<#return 'function(){ return "' + returnValue + '"; }'> <#return 'function(){ return "' + returnValue + '"; }'>
</#if> </#if>
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
<#if profile?? && profile.attributes??>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#if are_same_path(path, ["messagesPerField", "get"])>
<#local jsFunctionCode = "function (fieldName) { ">
<#list fieldNames as fieldName>
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
<#if pageId == "login.ftl" >
<#if fieldName == "username">
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
<#if messagesPerField.exists('username') || messagesPerField.exists('password')>
<#local jsFunctionCode += "return kcContext.message && kcContext.message.summary ? kcContext.message.summary : 'error'; ">
<#else>
<#local jsFunctionCode += "return ''; ">
</#if>
<#local jsFunctionCode += "} ">
<#continue>
</#if>
<#if fieldName == "password">
<#continue>
</#if>
</#if>
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "'){ ">
<#if messagesPerField.exists('${fieldName}')>
<#local jsFunctionCode += 'return decodeHtmlEntities("' + messagesPerField.get('${fieldName}')?js_string + '"); '>
<#else>
<#local jsFunctionCode += "return ''; ">
</#if>
<#local jsFunctionCode += "} ">
</#list>
<#local jsFunctionCode += "}">
<#return jsFunctionCode>
</#if>
<#if are_same_path(path, ["messagesPerField", "existsError"])>
<#local jsFunctionCode = "function (fieldName) { ">
<#list fieldNames as fieldName>
<#-- See: https://github.com/keycloakify/keycloakify/issues/217 -->
<#if pageId == "login.ftl" >
<#if fieldName == "username">
<#local jsFunctionCode += "if(fieldName === 'username' || fieldName === 'password' ){ ">
<#if messagesPerField.existsError('username') || messagesPerField.existsError('password')>
<#local jsFunctionCode += "return true; ">
<#else>
<#local jsFunctionCode += "return false; ">
</#if>
<#local jsFunctionCode += "} ">
<#continue>
</#if>
<#if fieldName == "password">
<#continue>
</#if>
</#if>
<#local jsFunctionCode += "if(fieldName === '" + fieldName + "' ){ ">
<#if messagesPerField.existsError('${fieldName}')>
<#local jsFunctionCode += 'return true; '>
<#else>
<#local jsFunctionCode += "return false; ">
</#if>
<#local jsFunctionCode += "}">
</#list>
<#local jsFunctionCode += "}">
<#return jsFunctionCode>
</#if>
<#return "ABORT: It's a method"> <#return "ABORT: It's a method">
</#if> </#if>
@ -540,5 +550,4 @@ function decodeHtmlEntities(htmlStr){
<#function are_same_path path searchedPath> <#function are_same_path path searchedPath>
<#return path?size == searchedPath?size && is_subpath(path, searchedPath)> <#return path?size == searchedPath?size && is_subpath(path, searchedPath)>
</#function> </#function>
</script>

View File

@ -13,13 +13,15 @@ import { transformCodebase } from "../../tools/transformCodebase";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
npmWorkspaceRootDirPath: string; npmWorkspaceRootDirPath: string;
keycloakifyBuildDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function bringInAccountV1(params: { buildContext: BuildContextLike }) { export async function bringInAccountV1(params: {
const { buildContext } = params; resourcesDirPath: string;
buildContext: BuildContextLike;
}) {
const { resourcesDirPath, buildContext } = params;
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
keycloakVersion: lastKeycloakVersionWithAccountV1, keycloakVersion: lastKeycloakVersionWithAccountV1,
@ -27,10 +29,7 @@ export async function bringInAccountV1(params: { buildContext: BuildContextLike
}); });
const accountV1DirPath = pathJoin( const accountV1DirPath = pathJoin(
buildContext.keycloakifyBuildDirPath, resourcesDirPath,
"src",
"main",
"resources",
"theme", "theme",
accountV1ThemeName, accountV1ThemeName,
"account" "account"

View File

@ -0,0 +1,42 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import {
generateResourcesForMainTheme,
type BuildContextLike as BuildContextLike_generateResourcesForMainTheme
} from "./generateResourcesForMainTheme";
import { generateResourcesForThemeVariant } from "./generateResourcesForThemeVariant";
import fs from "fs";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildContextLike = BuildContextLike_generateResourcesForMainTheme & {
themeNames: string[];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateResources(params: {
buildContext: BuildContextLike;
resourcesDirPath: string;
}): Promise<void> {
const { resourcesDirPath, buildContext } = params;
const [themeName, ...themeVariantNames] = buildContext.themeNames;
if (fs.existsSync(resourcesDirPath)) {
rmSync(resourcesDirPath, { recursive: true });
}
await generateResourcesForMainTheme({
resourcesDirPath,
themeName,
buildContext
});
for (const themeVariantName of themeVariantNames) {
generateResourcesForThemeVariant({
resourcesDirPath,
themeName,
themeVariantName
});
}
}

View File

@ -1,6 +1,6 @@
import { transformCodebase } from "../../tools/transformCodebase"; import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path"; import { join as pathJoin, resolve as pathResolve, relative as pathRelative } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { import {
@ -16,7 +16,6 @@ import {
loginThemePageIds, loginThemePageIds,
accountThemePageIds accountThemePageIds
} from "../../shared/constants"; } from "../../shared/constants";
import { isInside } from "../../tools/isInside";
import type { BuildContext } from "../../shared/buildContext"; import type { BuildContext } from "../../shared/buildContext";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import { import {
@ -30,7 +29,6 @@ import {
bringInAccountV1, bringInAccountV1,
type BuildContextLike as BuildContextLike_bringInAccountV1 type BuildContextLike as BuildContextLike_bringInAccountV1
} from "./bringInAccountV1"; } from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync"; import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { import {
@ -43,57 +41,36 @@ import { escapeStringForPropertiesFile } from "../../tools/escapeStringForProper
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
BuildContextLike_downloadKeycloakStaticResources & BuildContextLike_downloadKeycloakStaticResources &
BuildContextLike_bringInAccountV1 & { BuildContextLike_bringInAccountV1 & {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string; loginThemeResourcesFromKeycloakVersion: string;
projectBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
projectDirPath: string; projectDirPath: string;
keycloakifyBuildDirPath: string; projectBuildDirPath: string;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateSrcMainResourcesForMainTheme(params: { export async function generateResourcesForMainTheme(params: {
themeName: string; themeName: string;
resourcesDirPath: string;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<void> { }): Promise<void> {
const { themeName, buildContext } = params; const { themeName, resourcesDirPath, buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({
projectDirPath: buildContext.projectDirPath
});
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => { const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params; const { themeType } = params;
return pathJoin( return pathJoin(resourcesDirPath, "theme", themeName, themeType);
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
}; };
const cssGlobalsToDefine: Record<string, string> = {}; const cssGlobalsToDefine: Record<string, string> = {};
const implementedThemeTypes: Record<ThemeType | "email", boolean> = {
login: false,
account: false,
email: false
};
for (const themeType of ["login", "account"] as const) { for (const themeType of ["login", "account"] as const) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) { if (!buildContext.recordIsImplementedByThemeType[themeType]) {
continue; continue;
} }
implementedThemeTypes[themeType] = true;
const themeTypeDirPath = getThemeTypeDirPath({ themeType }); const themeTypeDirPath = getThemeTypeDirPath({ themeType });
apply_replacers_and_move_to_theme_resources: { apply_replacers_and_move_to_theme_resources: {
@ -106,7 +83,10 @@ export async function generateSrcMainResourcesForMainTheme(params: {
// NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up. // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up.
rmSync(destDirPath, { recursive: true, force: true }); rmSync(destDirPath, { recursive: true, force: true });
if (themeType === "account" && implementedThemeTypes.login) { if (
themeType === "account" &&
buildContext.recordIsImplementedByThemeType.login
) {
// NOTE: We prevent doing it twice, it has been done for the login theme. // NOTE: We prevent doing it twice, it has been done for the login theme.
transformCodebase({ transformCodebase({
@ -123,25 +103,32 @@ export async function generateSrcMainResourcesForMainTheme(params: {
break apply_replacers_and_move_to_theme_resources; break apply_replacers_and_move_to_theme_resources;
} }
{
const dirPath = pathJoin(
buildContext.projectBuildDirPath,
keycloak_resources
);
if (fs.existsSync(dirPath)) {
assert(buildContext.bundler === "webpack");
throw new Error(
[
`Keycloakify build error: The ${keycloak_resources} directory shouldn't exist in your build directory.`,
`(${pathRelative(process.cwd(), dirPath)}).\n`,
`Theses assets are only required for local development with Storybook.",
"Please remove this directory as an additional step of your command.\n`,
`For example: \`"build": "... && rimraf ${pathRelative(buildContext.projectDirPath, dirPath)}"\``
].join(" ")
);
}
}
transformCodebase({ transformCodebase({
srcDirPath: buildContext.projectBuildDirPath, srcDirPath: buildContext.projectBuildDirPath,
destDirPath, destDirPath,
transformSourceCode: ({ filePath, sourceCode }) => { transformSourceCode: ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/ if (filePath.endsWith(".css")) {
// This should not happen if users follow the new instruction setup but we keep it for retrocompatibility.
if (
isInside({
dirPath: pathJoin(
buildContext.projectBuildDirPath,
keycloak_resources
),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
const { const {
cssGlobalsToDefine: cssGlobalsToDefineForThisFile, cssGlobalsToDefine: cssGlobalsToDefineForThisFile,
fixedCssCode fixedCssCode
@ -160,7 +147,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
}; };
} }
if (/\.js?$/i.test(filePath)) { if (filePath.endsWith(".js")) {
const { fixedJsCode } = replaceImportsInJsCode({ const { fixedJsCode } = replaceImportsInJsCode({
jsCode: sourceCode.toString("utf8"), jsCode: sourceCode.toString("utf8"),
buildContext buildContext
@ -186,7 +173,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
keycloakifyVersion: readThisNpmPackageVersion(), keycloakifyVersion: readThisNpmPackageVersion(),
themeType, themeType,
fieldNames: readFieldNameUsage({ fieldNames: readFieldNameUsage({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}) })
}); });
@ -202,13 +189,11 @@ export async function generateSrcMainResourcesForMainTheme(params: {
})(), })(),
...readExtraPagesNames({ ...readExtraPagesNames({
themeType, themeType,
themeSrcDirPath themeSrcDirPath: buildContext.themeSrcDirPath
}) })
].forEach(pageId => { ].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeTypeDirPath, { recursive: true });
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeTypeDirPath, pageId), pathJoin(themeTypeDirPath, pageId),
Buffer.from(ftlCode, "utf8") Buffer.from(ftlCode, "utf8")
@ -216,7 +201,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
}); });
generateMessageProperties({ generateMessageProperties({
themeSrcDirPath, themeSrcDirPath: buildContext.themeSrcDirPath,
themeType themeType
}).forEach(({ languageTag, propertiesFileSource }) => { }).forEach(({ languageTag, propertiesFileSource }) => {
const messagesDirPath = pathJoin(themeTypeDirPath, "messages"); const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
@ -264,7 +249,7 @@ export async function generateSrcMainResourcesForMainTheme(params: {
assert<Equals<typeof themeType, never>>(false); assert<Equals<typeof themeType, never>>(false);
})()}`, })()}`,
...(buildContext.extraThemeProperties ?? []), ...(buildContext.extraThemeProperties ?? []),
buildContext.environmentVariables.map( ...buildContext.environmentVariables.map(
({ name, default: defaultValue }) => ({ name, default: defaultValue }) =>
`${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}` `${name}=\${env.${name}:${escapeStringForPropertiesFile(defaultValue)}}`
) )
@ -275,13 +260,11 @@ export async function generateSrcMainResourcesForMainTheme(params: {
} }
email: { email: {
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email"); if (!buildContext.recordIsImplementedByThemeType.email) {
if (!fs.existsSync(emailThemeSrcDirPath)) {
break email; break email;
} }
implementedThemeTypes.email = true; const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
transformCodebase({ transformCodebase({
srcDirPath: emailThemeSrcDirPath, srcDirPath: emailThemeSrcDirPath,
@ -289,8 +272,9 @@ export async function generateSrcMainResourcesForMainTheme(params: {
}); });
} }
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
await bringInAccountV1({ await bringInAccountV1({
resourcesDirPath,
buildContext buildContext
}); });
} }
@ -300,12 +284,12 @@ export async function generateSrcMainResourcesForMainTheme(params: {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: themeName, name: themeName,
types: objectEntries(implementedThemeTypes) types: objectEntries(buildContext.recordIsImplementedByThemeType)
.filter(([, isImplemented]) => isImplemented) .filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType) .map(([themeType]) => themeType)
}); });
if (implementedThemeTypes.account) { if (buildContext.recordIsImplementedByThemeType.account) {
metaInfKeycloakThemes.themes.push({ metaInfKeycloakThemes.themes.push({
name: accountV1ThemeName, name: accountV1ThemeName,
types: ["account"] types: ["account"]
@ -313,8 +297,8 @@ export async function generateSrcMainResourcesForMainTheme(params: {
} }
writeMetaInfKeycloakThemes({ writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath, resourcesDirPath,
metaInfKeycloakThemes getNewMetaInfKeycloakTheme: () => metaInfKeycloakThemes
}); });
} }
} }

View File

@ -0,0 +1,70 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext";
import { writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
export type BuildContextLike = {
keycloakifyBuildDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateResourcesForThemeVariant(params: {
resourcesDirPath: string;
themeName: string;
themeVariantName: string;
}) {
const { resourcesDirPath, themeName, themeVariantName } = params;
const mainThemeDirPath = pathJoin(resourcesDirPath, "theme", themeName);
transformCodebase({
srcDirPath: mainThemeDirPath,
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (
pathExtname(fileRelativePath) === ".ftl" &&
fileRelativePath.split(pathSep).length === 2
) {
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
`kcContext.themeName = "${themeName}";`,
`kcContext.themeName = "${themeVariantName}";`
),
"utf8"
);
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
return { modifiedSourceCode };
}
return { modifiedSourceCode: sourceCode };
}
});
writeMetaInfKeycloakThemes({
resourcesDirPath,
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
assert(metaInfKeycloakTheme !== undefined);
const newMetaInfKeycloakTheme = metaInfKeycloakTheme;
newMetaInfKeycloakTheme.themes.push({
name: themeVariantName,
types: (() => {
const theme = newMetaInfKeycloakTheme.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
return newMetaInfKeycloakTheme;
}
});
}

View File

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

View File

@ -1,34 +0,0 @@
import type { BuildContext } from "../../shared/buildContext";
import { assert } from "tsafe/assert";
import {
generateSrcMainResourcesForMainTheme,
type BuildContextLike as BuildContextLike_generateSrcMainResourcesForMainTheme
} from "./generateSrcMainResourcesForMainTheme";
import { generateSrcMainResourcesForThemeVariant } from "./generateSrcMainResourcesForThemeVariant";
export type BuildContextLike = BuildContextLike_generateSrcMainResourcesForMainTheme & {
themeNames: string[];
};
assert<BuildContext extends BuildContextLike ? true : false>();
export async function generateSrcMainResources(params: {
buildContext: BuildContextLike;
}): Promise<void> {
const { buildContext } = params;
const [themeName, ...themeVariantNames] = buildContext.themeNames;
await generateSrcMainResourcesForMainTheme({
themeName,
buildContext
});
for (const themeVariantName of themeVariantNames) {
generateSrcMainResourcesForThemeVariant({
themeName,
themeVariantName,
buildContext
});
}
}

View File

@ -1,80 +0,0 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildContext } from "../../shared/buildContext";
import {
readMetaInfKeycloakThemes,
writeMetaInfKeycloakThemes
} from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
export type BuildContextLike = {
keycloakifyBuildDirPath: string;
};
assert<BuildContext extends BuildContextLike ? true : false>();
export function generateSrcMainResourcesForThemeVariant(params: {
themeName: string;
themeVariantName: string;
buildContext: BuildContextLike;
}) {
const { themeName, themeVariantName, buildContext } = params;
const mainThemeDirPath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName
);
transformCodebase({
srcDirPath: mainThemeDirPath,
destDirPath: pathJoin(mainThemeDirPath, "..", themeVariantName),
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
if (
pathExtname(fileRelativePath) === ".ftl" &&
fileRelativePath.split(pathSep).length === 2
) {
const modifiedSourceCode = Buffer.from(
Buffer.from(sourceCode)
.toString("utf-8")
.replace(
`out["themeName"] = "${themeName}";`,
`out["themeName"] = "${themeVariantName}";`
),
"utf8"
);
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
return { modifiedSourceCode };
}
return { modifiedSourceCode: sourceCode };
}
});
{
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath
});
updatedMetaInfKeycloakThemes.themes.push({
name: themeVariantName,
types: (() => {
const theme = updatedMetaInfKeycloakThemes.themes.find(
({ name }) => name === themeName
);
assert(theme !== undefined);
return theme.types;
})()
});
writeMetaInfKeycloakThemes({
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath,
metaInfKeycloakThemes: updatedMetaInfKeycloakThemes
});
}
}

View File

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

View File

@ -1,74 +0,0 @@
import * as fs from "fs";
import {
join as pathJoin,
relative as pathRelative,
basename as pathBasename
} from "path";
import { assert } from "tsafe/assert";
import type { BuildContext } from "../shared/buildContext";
import { accountV1ThemeName } from "../shared/constants";
export type BuildContextLike = {
keycloakifyBuildDirPath: string;
themeNames: string[];
};
assert<BuildContext extends BuildContextLike ? true : false>();
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container";
const keycloakVersion = "24.0.4";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: {
jarFilePath: string;
doesImplementAccountTheme: boolean;
buildContext: BuildContextLike;
}) {
const { jarFilePath, doesImplementAccountTheme, buildContext } = params;
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
fs.writeFileSync(
pathJoin(
buildContext.keycloakifyBuildDirPath,
generateStartKeycloakTestingContainer.basename
),
Buffer.from(
[
"#!/usr/bin/env bash",
"",
`docker rm ${containerName} || true`,
"",
`cd "${buildContext.keycloakifyBuildDirPath}"`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
` -v "${pathJoin(
"$(pwd)",
pathRelative(buildContext.keycloakifyBuildDirPath, jarFilePath)
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
[
...(doesImplementAccountTheme ? [accountV1ThemeName] : []),
...buildContext.themeNames
].map(
themeName =>
` -v "${pathJoin(
"$(pwd)",
themeRelativeDirPath,
themeName
).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
""
].join("\n"),
"utf8"
),
{ mode: 0o755 }
);
}

View File

@ -1,14 +1,15 @@
import { generateSrcMainResources } from "./generateSrcMainResources"; import { generateResources } from "./generateResources";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import * as fs from "fs"; import * as fs from "fs";
import { getBuildContext } from "../shared/buildContext"; import { getBuildContext } from "../shared/buildContext";
import { vitePluginSubScriptEnvNames, skipBuildJarsEnvName } from "../shared/constants"; import { vitePluginSubScriptEnvNames } from "../shared/constants";
import { buildJars } from "./buildJars"; import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import chalk from "chalk"; import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os"; import * as os from "os";
import { rmSync } from "../tools/fs.rmSync";
export async function command(params: { cliCommandOptions: CliCommandOptions }) { export async function command(params: { cliCommandOptions: CliCommandOptions }) {
exit_if_maven_not_installed: { exit_if_maven_not_installed: {
@ -76,7 +77,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
); );
} }
await generateSrcMainResources({ buildContext }); const resourcesDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "resources");
await generateResources({
resourcesDirPath,
buildContext
});
run_post_build_script: { run_post_build_script: {
if (buildContext.bundler !== "vite") { if (buildContext.bundler !== "vite") {
@ -87,21 +93,24 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
[vitePluginSubScriptEnvNames.runPostBuildScript]: [vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify({
JSON.stringify(buildContext) resourcesDirPath,
buildContext
})
} }
}); });
} }
build_jars: { await buildJars({
if (process.env[skipBuildJarsEnvName]) { resourcesDirPath,
break build_jars; buildContext
} });
await buildJars({ buildContext }); rmSync(resourcesDirPath, { recursive: true });
}
console.log( console.log(
chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`) chalk.green(
`✓ keycloak theme built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`
)
); );
} }

View File

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext"; import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path"; import * as nodePath from "path";
@ -88,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: {
fixedJsCode = replaceAll( fixedJsCode = replaceAll(
fixedJsCode, fixedJsCode,
`"${relativePathOfAssetFile}"`, `"${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")` `(window.kcContext.url.resourcesPath.substring(1) + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
); );
fixedJsCode = replaceAll( fixedJsCode = replaceAll(
fixedJsCode, fixedJsCode,
`"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`, `"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`,
`(window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")` `(window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${relativePathOfAssetFile}")`
); );
}); });
} }

View File

@ -1,7 +1,4 @@
import { import { basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir
} from "../../../shared/constants";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "../../../shared/buildContext"; import type { BuildContext } from "../../../shared/buildContext";
import * as nodePath from "path"; import * as nodePath from "path";
@ -86,7 +83,7 @@ export function replaceImportsInJsCode_webpack(params: {
var pd = Object.getOwnPropertyDescriptor(${n}, "p"); var pd = Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){ if( pd === undefined || pd.configurable ){
Object.defineProperty(${n}, "p", { Object.defineProperty(${n}, "p", {
get: function() { return window.${nameOfTheGlobal}.url.resourcesPath; }, get: function() { return window.kcContext.url.resourcesPath; },
set: function() {} set: function() {}
}); });
} }
@ -107,7 +104,7 @@ export function replaceImportsInJsCode_webpack(params: {
`[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, `[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`,
"g" "g"
), ),
`window.${nameOfTheGlobal}.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}` `window.kcContext.url.resourcesPath + "/${basenameOfTheKeycloakifyResourcesDir}/${staticDir}`
); );
return { fixedJsCode }; return { fixedJsCode };

View File

@ -134,20 +134,6 @@ program
} }
}); });
program
.command({
name: "download-keycloak-default-theme",
description: "Download the built-in Keycloak theme."
})
.task({
skip,
handler: async cliCommandOptions => {
const { command } = await import("./download-keycloak-default-theme");
await command({ cliCommandOptions });
}
});
program program
.command({ .command({
name: "eject-page", name: "eject-page",

View File

@ -5,5 +5,5 @@ export type KeycloakVersionRange =
export namespace KeycloakVersionRange { export namespace KeycloakVersionRange {
export type WithoutAccountTheme = "21-and-below" | "22-and-above"; export type WithoutAccountTheme = "21-and-below" | "22-and-above";
export type WithAccountTheme = "21-and-below" | "23" | "24-and-above"; export type WithAccountTheme = "21-and-below" | "23" | "24" | "25-and-above";
} }

View File

@ -5,14 +5,27 @@ import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath"
import type { CliCommandOptions } from "../main"; import type { CliCommandOptions } from "../main";
import { z } from "zod"; import { z } from "zod";
import * as fs from "fs"; import * as fs from "fs";
import { assert } from "tsafe"; import { assert, type Equals } from "tsafe/assert";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { vitePluginSubScriptEnvNames } from "./constants"; import {
vitePluginSubScriptEnvNames,
buildForKeycloakMajorVersionEnvName
} from "./constants";
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { themeTypes } from "./constants";
import { objectFromEntries } from "tsafe/objectFromEntries";
import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import { symToStr } from "tsafe/symToStr";
import chalk from "chalk";
export type BuildContext = { export type BuildContext = {
bundler: "vite" | "webpack"; bundler: "vite" | "webpack";
themeVersion: string; themeVersion: string;
themeNames: string[]; themeNames: [string, ...string[]];
extraThemeProperties: string[] | undefined; extraThemeProperties: string[] | undefined;
groupId: string; groupId: string;
artifactId: string; artifactId: string;
@ -30,10 +43,17 @@ export type BuildContext = {
npmWorkspaceRootDirPath: string; npmWorkspaceRootDirPath: string;
kcContextExclusionsFtlCode: string | undefined; kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
recordIsImplementedByThemeType: Readonly<Record<ThemeType | "email", boolean>>;
jarTargets: {
keycloakVersionRange: KeycloakVersionRange;
jarFileBasename: string;
}[];
}; };
export type BuildOptions = { export type BuildOptions = {
themeName?: string | string[]; themeName?: string | string[];
themeVersion?: string;
environmentVariables?: { name: string; default: string }[]; environmentVariables?: { name: string; default: string }[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
artifactId?: string; artifactId?: string;
@ -41,8 +61,22 @@ export type BuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string; loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
kcContextExclusionsFtl?: string; kcContextExclusionsFtl?: string;
/** https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions */
keycloakVersionTargets?: BuildOptions.KeycloakVersionTargets;
}; };
export namespace BuildOptions {
export type KeycloakVersionTargets =
| ({ hasAccountTheme: true } & Record<
KeycloakVersionRange.WithAccountTheme,
string | boolean
>)
| ({ hasAccountTheme: false } & Record<
KeycloakVersionRange.WithoutAccountTheme,
string | boolean
>);
}
export type ResolvedViteConfig = { export type ResolvedViteConfig = {
buildDir: string; buildDir: string;
publicDir: string; publicDir: string;
@ -102,37 +136,96 @@ export function getBuildContext(params: {
})(); })();
const parsedPackageJson = (() => { const parsedPackageJson = (() => {
type BuildOptions_packageJson = BuildOptions & {
projectBuildDirPath?: string;
staticDirPathInProjectBuildDirPath?: string;
publicDirPath?: string;
};
type ParsedPackageJson = { type ParsedPackageJson = {
name: string; name: string;
version?: string; version?: string;
homepage?: string; homepage?: string;
keycloakify?: BuildOptions & { keycloakify?: BuildOptions_packageJson;
projectBuildDirPath?: string;
};
}; };
const zParsedPackageJson = z.object({ const zParsedPackageJson = z.object({
name: z.string(), name: z.string(),
version: z.string().optional(), version: z.string().optional(),
homepage: z.string().optional(), homepage: z.string().optional(),
keycloakify: z keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
.object({ (() => {
extraThemeProperties: z.array(z.string()).optional(), const zBuildOptions_packageJson = z.object({
artifactId: z.string().optional(), extraThemeProperties: z.array(z.string()).optional(),
groupId: z.string().optional(), artifactId: z.string().optional(),
loginThemeResourcesFromKeycloakVersion: z.string().optional(), groupId: z.string().optional(),
projectBuildDirPath: z.string().optional(), loginThemeResourcesFromKeycloakVersion: z.string().optional(),
keycloakifyBuildDirPath: z.string().optional(), projectBuildDirPath: z.string().optional(),
themeName: z.union([z.string(), z.array(z.string())]).optional() keycloakifyBuildDirPath: z.string().optional(),
}) kcContextExclusionsFtl: z.string().optional(),
.optional() environmentVariables: z
.array(
z.object({
name: z.string(),
default: z.string()
})
)
.optional(),
themeName: z.union([z.string(), z.array(z.string())]).optional(),
themeVersion: z.string().optional(),
staticDirPathInProjectBuildDirPath: z.string().optional(),
publicDirPath: z.string().optional(),
keycloakVersionTargets: id<
z.ZodType<BuildOptions.KeycloakVersionTargets>
>(
(() => {
const zKeycloakVersionTargets = z.union([
z.object({
hasAccountTheme: z.literal(true),
"21-and-below": z.union([
z.boolean(),
z.string()
]),
"23": z.union([z.boolean(), z.string()]),
"24": z.union([z.boolean(), z.string()]),
"25-and-above": z.union([z.boolean(), z.string()])
}),
z.object({
hasAccountTheme: z.literal(false),
"21-and-below": z.union([
z.boolean(),
z.string()
]),
"22-and-above": z.union([z.boolean(), z.string()])
})
]);
{
type Got = z.infer<typeof zKeycloakVersionTargets>;
type Expected = BuildOptions.KeycloakVersionTargets;
assert<Equals<Got, Expected>>();
}
return zKeycloakVersionTargets;
})()
).optional()
});
{
type Got = z.infer<typeof zBuildOptions_packageJson>;
type Expected = BuildOptions_packageJson;
assert<Equals<Got, Expected>>();
}
return zBuildOptions_packageJson;
})()
).optional()
}); });
{ {
type Got = ReturnType<(typeof zParsedPackageJson)["parse"]>; type Got = z.infer<typeof zParsedPackageJson>;
type Expected = ParsedPackageJson; type Expected = ParsedPackageJson;
assert<Got extends Expected ? true : false>(); assert<Equals<Got, Expected>>();
assert<Expected extends Got ? true : false>();
} }
return zParsedPackageJson.parse( return zParsedPackageJson.parse(
@ -142,12 +235,60 @@ export function getBuildContext(params: {
); );
})(); })();
const buildOptions: BuildOptions = { const buildOptions = {
...parsedPackageJson.keycloakify, ...parsedPackageJson.keycloakify,
...resolvedViteConfig?.buildOptions ...resolvedViteConfig?.buildOptions
}; };
const themeNames = (() => { const { themeSrcDirPath } = (() => {
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { themeSrcDirPath: srcDirPath };
}
console.log(
chalk.red(
[
"Can't locate your keycloak theme source directory.",
"See: https://docs.keycloakify.dev/v/v10/keycloakify-in-my-app/collocation"
].join("\n")
)
);
process.exit(1);
})();
const recordIsImplementedByThemeType = objectFromEntries(
(["login", "account", "email"] as const).map(themeType => [
themeType,
fs.existsSync(pathJoin(themeSrcDirPath, themeType))
])
);
const themeNames = ((): [string, ...string[]] => {
if (buildOptions.themeName === undefined) { if (buildOptions.themeName === undefined) {
return [ return [
parsedPackageJson.name parsedPackageJson.name
@ -161,7 +302,11 @@ export function getBuildContext(params: {
return [buildOptions.themeName]; return [buildOptions.themeName];
} }
return buildOptions.themeName; const [mainThemeName, ...themeVariantNames] = buildOptions.themeName;
assert(mainThemeName !== undefined);
return [mainThemeName, ...themeVariantNames];
})(); })();
const projectBuildDirPath = (() => { const projectBuildDirPath = (() => {
@ -170,9 +315,9 @@ export function getBuildContext(params: {
break webpack; break webpack;
} }
if (parsedPackageJson.keycloakify?.projectBuildDirPath !== undefined) { if (buildOptions.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath, pathIsh: buildOptions.projectBuildDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -188,10 +333,11 @@ export function getBuildContext(params: {
dependencyExpected: "keycloakify" dependencyExpected: "keycloakify"
}); });
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
return { return {
bundler: resolvedViteConfig !== undefined ? "vite" : "webpack", bundler,
themeVersion: themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames, themeNames,
extraThemeProperties: buildOptions.extraThemeProperties, extraThemeProperties: buildOptions.extraThemeProperties,
groupId: (() => { groupId: (() => {
@ -233,14 +379,21 @@ export function getBuildContext(params: {
); );
})(), })(),
publicDirPath: (() => { publicDirPath: (() => {
if (process.env.PUBLIC_DIR_PATH !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: process.env.PUBLIC_DIR_PATH,
cwd: projectDirPath
});
}
webpack: { webpack: {
if (resolvedViteConfig !== undefined) { if (resolvedViteConfig !== undefined) {
break webpack; break webpack;
} }
if (process.env.PUBLIC_DIR_PATH !== undefined) { if (buildOptions.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({ return getAbsoluteAndInOsFormatPath({
pathIsh: process.env.PUBLIC_DIR_PATH, pathIsh: buildOptions.publicDirPath,
cwd: projectDirPath cwd: projectDirPath
}); });
} }
@ -297,6 +450,13 @@ export function getBuildContext(params: {
break webpack; break webpack;
} }
if (buildOptions.staticDirPathInProjectBuildDirPath !== undefined) {
getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.staticDirPathInProjectBuildDirPath,
cwd: projectBuildDirPath
});
}
return pathJoin(projectBuildDirPath, "static"); return pathJoin(projectBuildDirPath, "static");
} }
@ -319,6 +479,290 @@ export function getBuildContext(params: {
return buildOptions.kcContextExclusionsFtl; return buildOptions.kcContextExclusionsFtl;
})(), })(),
environmentVariables: buildOptions.environmentVariables ?? [] environmentVariables: buildOptions.environmentVariables ?? [],
recordIsImplementedByThemeType,
themeSrcDirPath,
jarTargets: (() => {
const getDefaultJarFileBasename = (range: string) =>
`keycloak-theme-for-kc-${range}.jar`;
build_for_specific_keycloak_major_version: {
const buildForKeycloakMajorVersionNumber = (() => {
const envValue = process.env[buildForKeycloakMajorVersionEnvName];
if (envValue === undefined) {
return undefined;
}
const major = parseInt(envValue);
assert(!isNaN(major));
return major;
})();
if (buildForKeycloakMajorVersionNumber === undefined) {
break build_for_specific_keycloak_major_version;
}
const keycloakVersionRange: KeycloakVersionRange = (() => {
const doesImplementAccountTheme =
recordIsImplementedByThemeType.account;
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
if (buildForKeycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
assert(buildForKeycloakMajorVersionNumber !== 22);
if (buildForKeycloakMajorVersionNumber === 23) {
return "23" as const;
}
if (buildForKeycloakMajorVersionNumber === 24) {
return "24" as const;
}
return "25-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountTheme
>
>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (buildForKeycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
return "22-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
return keycloakVersionRange;
}
})();
const jarFileBasename = (() => {
use_custom_jar_basename: {
const { keycloakVersionTargets } = buildOptions;
if (keycloakVersionTargets === undefined) {
break use_custom_jar_basename;
}
const entry = objectEntries(keycloakVersionTargets).find(
([keycloakVersionRange_entry]) =>
keycloakVersionRange_entry === keycloakVersionRange
);
if (entry === undefined) {
break use_custom_jar_basename;
}
const maybeJarFileBasename = entry[1];
if (typeof maybeJarFileBasename !== "string") {
break use_custom_jar_basename;
}
return maybeJarFileBasename;
}
return getDefaultJarFileBasename(keycloakVersionRange);
})();
return [
{
keycloakVersionRange,
jarFileBasename
}
];
}
const jarTargets_default = (() => {
const jarTargets: BuildContext["jarTargets"] = [];
if (recordIsImplementedByThemeType.account) {
for (const keycloakVersionRange of [
"21-and-below",
"23",
"24",
"25-and-above"
] as const) {
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithAccountTheme
>
>(true);
jarTargets.push({
keycloakVersionRange,
jarFileBasename:
getDefaultJarFileBasename(keycloakVersionRange)
});
}
} else {
for (const keycloakVersionRange of [
"21-and-below",
"22-and-above"
] as const) {
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>(true);
jarTargets.push({
keycloakVersionRange,
jarFileBasename:
getDefaultJarFileBasename(keycloakVersionRange)
});
}
}
return jarTargets;
})();
if (buildOptions.keycloakVersionTargets === undefined) {
return jarTargets_default;
}
if (
buildOptions.keycloakVersionTargets.hasAccountTheme !==
recordIsImplementedByThemeType.account
) {
console.log(
chalk.red(
(() => {
const { keycloakVersionTargets } = buildOptions;
let message = `Bad ${symToStr({ keycloakVersionTargets })} configuration.\n`;
if (keycloakVersionTargets.hasAccountTheme) {
message +=
"Your codebase does not seem to implement an account theme ";
} else {
message += "Your codebase implements an account theme ";
}
const { hasAccountTheme } = keycloakVersionTargets;
message += `but you have set ${symToStr({ keycloakVersionTargets })}.${symToStr({ hasAccountTheme })}`;
message += ` to ${hasAccountTheme} in your `;
message += (() => {
switch (bundler) {
case "vite":
return "vite.config.ts";
case "webpack":
return "package.json";
}
assert<Equals<typeof bundler, never>>(false);
})();
message += `. Please set it to ${!hasAccountTheme} `;
message +=
"and fill up the relevant keycloak version ranges.\n";
message += "Example:\n";
message += JSON.stringify(
id<Pick<BuildOptions, "keycloakVersionTargets">>({
keycloakVersionTargets: {
hasAccountTheme:
recordIsImplementedByThemeType.account,
...objectFromEntries(
jarTargets_default.map(
({
keycloakVersionRange,
jarFileBasename
}) => [
keycloakVersionRange,
jarFileBasename
]
)
)
}
}),
null,
2
);
message +=
"\nSee: https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions";
return message;
})()
)
);
process.exit(1);
}
const jarTargets: BuildContext["jarTargets"] = [];
const { hasAccountTheme, ...rest } = buildOptions.keycloakVersionTargets;
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(rest)) {
if (jarNameOrBoolean === false) {
continue;
}
if (jarNameOrBoolean === true) {
jarTargets.push({
keycloakVersionRange: keycloakVersionRange,
jarFileBasename: getDefaultJarFileBasename(keycloakVersionRange)
});
continue;
}
const jarFileBasename = jarNameOrBoolean;
if (!jarFileBasename.endsWith(".jar")) {
console.log(
chalk.red(`Bad ${jarFileBasename} should end with '.jar'\n`)
);
process.exit(1);
}
if (jarFileBasename.includes("/") || jarFileBasename.includes("\\")) {
console.log(
chalk.red(
[
`Invalid ${jarFileBasename}. It's not supposed to be a path,`,
`Only the basename of the jar file is expected.`,
`Example: keycloak-theme.jar`
].join(" ")
)
);
process.exit(1);
}
jarTargets.push({
keycloakVersionRange: keycloakVersionRange,
jarFileBasename: jarNameOrBoolean
});
}
if (jarTargets.length === 0) {
console.log(
chalk.red(
"All jar targets are disabled. Please enable at least one jar target."
)
);
process.exit(1);
}
return jarTargets;
})()
}; };
} }

View File

@ -1,10 +1,9 @@
export const nameOfTheGlobal = "kcContext";
export const nameOfTheLocalizationRealmOverridesUserProfileProperty = export const nameOfTheLocalizationRealmOverridesUserProfileProperty =
"__localizationRealmOverridesUserProfile"; "__localizationRealmOverridesUserProfile";
export const keycloak_resources = "keycloak-resources"; export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common"; export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2"; export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const basenameOfTheKeycloakifyResourcesDir = "build"; export const basenameOfTheKeycloakifyResourcesDir = "dist";
export const themeTypes = ["login", "account"] as const; export const themeTypes = ["login", "account"] as const;
export const accountV1ThemeName = "account-v1"; export const accountV1ThemeName = "account-v1";
@ -16,7 +15,8 @@ export const vitePluginSubScriptEnvNames = {
resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG" resolveViteConfig: "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const; } as const;
export const skipBuildJarsEnvName = "KEYCLOAKIFY_SKIP_BUILD_JAR"; export const buildForKeycloakMajorVersionEnvName =
"KEYCLOAKIFY_BUILD_FOR_KEYCLOAK_MAJOR_VERSION";
export const loginThemePageIds = [ export const loginThemePageIds = [
"login.ftl", "login.ftl",

View File

@ -3,7 +3,6 @@ import { type BuildContext } from "./buildContext";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { lastKeycloakVersionWithAccountV1 } from "./constants"; import { lastKeycloakVersionWithAccountV1 } from "./constants";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
import { isInside } from "../tools/isInside";
export type BuildContextLike = { export type BuildContextLike = {
cacheDirPath: string; cacheDirPath: string;
@ -18,27 +17,25 @@ export async function downloadKeycloakDefaultTheme(params: {
}): Promise<{ defaultThemeDirPath: string }> { }): Promise<{ defaultThemeDirPath: string }> {
const { keycloakVersion, buildContext } = params; const { keycloakVersion, buildContext } = params;
let kcNodeModulesKeepFilePaths: string[] | undefined = undefined;
let kcNodeModulesKeepFilePaths_lastAccountV1: string[] | undefined = undefined;
const { extractedDirPath } = await downloadAndExtractArchive({ const { extractedDirPath } = await downloadAndExtractArchive({
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
cacheDirPath: buildContext.cacheDirPath, cacheDirPath: buildContext.cacheDirPath,
npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath, npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath,
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme", uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
onArchiveFile: async params => { onArchiveFile: async params => {
if (!isInside({ dirPath: "theme", filePath: params.fileRelativePath })) { const fileRelativePath = pathRelative("theme", params.fileRelativePath);
if (fileRelativePath.startsWith("..")) {
return; return;
} }
const { readFile, writeFile } = params; const { readFile, writeFile } = params;
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
skip_keycloak_v2: { skip_keycloak_v2: {
if ( if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
!isInside({
dirPath: pathJoin("keycloak.v2"),
filePath: fileRelativePath
})
) {
break skip_keycloak_v2; break skip_keycloak_v2;
} }
@ -50,6 +47,96 @@ export async function downloadKeycloakDefaultTheme(params: {
break last_account_v1_transformations; break last_account_v1_transformations;
} }
skip_web_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "web_modules")
)
) {
break skip_web_modules;
}
return;
}
skip_lib: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "lib")
)
) {
break skip_lib;
}
return;
}
skip_node_modules: {
if (
!fileRelativePath.startsWith(
pathJoin("keycloak", "common", "resources", "node_modules")
)
) {
break skip_node_modules;
}
if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) {
kcNodeModulesKeepFilePaths_lastAccountV1 = [
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Semibold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
)
];
}
for (const keepPath of kcNodeModulesKeepFilePaths_lastAccountV1) {
if (fileRelativePath.endsWith(keepPath)) {
break skip_node_modules;
}
}
return;
}
patch_account_css: { patch_account_css: {
if ( if (
fileRelativePath !== fileRelativePath !==
@ -70,69 +157,6 @@ export async function downloadKeycloakDefaultTheme(params: {
return; return;
} }
skip_web_modules: {
if (
!isInside({
dirPath: pathJoin(
"keycloak",
"common",
"resources",
"web_modules"
),
filePath: fileRelativePath
})
) {
break skip_web_modules;
}
return;
}
skip_unused_node_modules: {
const nodeModulesDirPath = pathJoin(
"keycloak",
"common",
"resources",
"node_modules"
);
if (
!isInside({
dirPath: nodeModulesDirPath,
filePath: fileRelativePath
})
) {
break skip_unused_node_modules;
}
const toKeepPrefixes = [
...[
"patternfly.min.css",
"patternfly-additions.min.css",
"patternfly-additions.min.css"
].map(fileBasename =>
pathJoin(
nodeModulesDirPath,
"patternfly",
"dist",
"css",
fileBasename
)
),
pathJoin(nodeModulesDirPath, "patternfly", "dist", "fonts")
];
if (
toKeepPrefixes.find(prefix =>
fileRelativePath.startsWith(prefix)
) !== undefined
) {
break skip_unused_node_modules;
}
return;
}
} }
skip_unused_resources: { skip_unused_resources: {
@ -140,61 +164,106 @@ export async function downloadKeycloakDefaultTheme(params: {
break skip_unused_resources; break skip_unused_resources;
} }
for (const dirBasename of [ skip_node_modules: {
"@patternfly-v5",
"@rollup",
"rollup",
"react",
"react-dom",
"shx",
".pnpm"
]) {
if ( if (
isInside({ !fileRelativePath.startsWith(
dirPath: pathJoin( pathJoin("keycloak", "common", "resources", "node_modules")
"keycloak", )
"common",
"resources",
"node_modules",
dirBasename
),
filePath: fileRelativePath
})
) { ) {
return; break skip_node_modules;
} }
if (kcNodeModulesKeepFilePaths === undefined) {
kcNodeModulesKeepFilePaths = [
pathJoin("@patternfly", "patternfly", "patternfly.min.css"),
pathJoin("patternfly", "dist", "css", "patternfly.min.css"),
pathJoin(
"patternfly",
"dist",
"css",
"patternfly-additions.min.css"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Regular-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Light-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.woff"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"OpenSans-Bold-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"fontawesome-webfont.woff2"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.ttf"
),
pathJoin(
"patternfly",
"dist",
"fonts",
"PatternFlyIcons-webfont.woff"
),
pathJoin("jquery", "dist", "jquery.min.js")
];
}
for (const keepPath of kcNodeModulesKeepFilePaths) {
if (fileRelativePath.endsWith(keepPath)) {
break skip_node_modules;
}
}
return;
} }
for (const dirBasename of ["react", "react-dom"]) { skip_vendor: {
if ( if (
isInside({ !fileRelativePath.startsWith(
dirPath: pathJoin( pathJoin("keycloak", "common", "resources", "vendor")
"keycloak", )
"common",
"resources",
"vendor",
dirBasename
),
filePath: fileRelativePath
})
) { ) {
return; break skip_vendor;
} }
return;
} }
if ( skip_rollup_config: {
isInside({ if (
dirPath: pathJoin( fileRelativePath !==
"keycloak", pathJoin("keycloak", "common", "resources", "rollup.config.js")
"common", ) {
"resources", break skip_rollup_config;
"node_modules", }
"@patternfly",
"react-core"
),
filePath: fileRelativePath
})
) {
return; return;
} }
} }

View File

@ -1,13 +1,14 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext"; import type { BuildContext } from "./buildContext";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import * as fs from "fs/promises"; import * as fs from "fs/promises";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
export type BuildContextLike = { export type BuildContextLike = {
projectDirPath: string; projectDirPath: string;
themeNames: string[]; themeNames: string[];
environmentVariables: { name: string; default: string }[]; environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
}; };
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
@ -17,45 +18,51 @@ export async function generateKcGenTs(params: {
}): Promise<void> { }): Promise<void> {
const { buildContext } = params; const { buildContext } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
projectDirPath: buildContext.projectDirPath
});
await fs.writeFile( const currentContent = (await existsAsync(filePath))
pathJoin(themeSrcDirPath, "kc.gen.ts"), ? await fs.readFile(filePath)
Buffer.from( : undefined;
[
`/* prettier-ignore-start */`, const newContent = Buffer.from(
``, [
`/* eslint-disable */`, `/* prettier-ignore-start */`,
``, ``,
`// @ts-nocheck`, `/* eslint-disable */`,
``, ``,
`// noinspection JSUnusedGlobalSymbols`, `// @ts-nocheck`,
``, ``,
`// This file is auto-generated by Keycloakify`, `// noinspection JSUnusedGlobalSymbols`,
``, ``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`, `// This file is auto-generated by Keycloakify`,
``, ``,
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`, `export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``, ``,
`export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`, `export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
``, ``,
`export const KcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`, `export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
``, ``,
`export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify( `export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
Object.fromEntries( ``,
buildContext.environmentVariables.map( `export const kcEnvDefaults: Record<KcEnvName, string> = ${JSON.stringify(
({ name, default: defaultValue }) => [name, defaultValue] Object.fromEntries(
) buildContext.environmentVariables.map(
), ({ name, default: defaultValue }) => [name, defaultValue]
null, )
2 ),
)};`, null,
``, 2
`/* prettier-ignore-end */` )};`,
].join("\n"), ``,
"utf8" `/* prettier-ignore-end */`,
) ``
].join("\n"),
"utf8"
); );
if (currentContent !== undefined && currentContent.equals(newContent)) {
return;
}
await fs.writeFile(filePath, newContent);
} }

View File

@ -1,11 +0,0 @@
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
export function getJarFileBasename(params: {
keycloakVersionRange: KeycloakVersionRange;
}) {
const { keycloakVersionRange } = params;
const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`;
return { jarFileBasename };
}

View File

@ -1,50 +0,0 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./constants";
const themeSrcDirBasenames = ["keycloak-theme", "keycloak_theme"];
/** Can't catch error, if the directory isn't found, this function will just exit the process with an error message. */
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const srcDirPath = pathJoin(projectDirPath, "src");
const themeSrcDirPath: string | undefined = crawl({
dirPath: srcDirPath,
returnedPathsType: "relative to dirPath"
})
.map(fileRelativePath => {
for (const themeSrcDirBasename of themeSrcDirBasenames) {
const split = fileRelativePath.split(themeSrcDirBasename);
if (split.length === 2) {
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
}
}
return undefined;
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath !== undefined) {
return { themeSrcDirPath };
}
for (const themeType of [...themeTypes, "email"]) {
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
continue;
}
return { themeSrcDirPath: srcDirPath };
}
console.error(
[
"Can't locate your theme source directory. It should be either: ",
"src/ or src/keycloak-theme or src/keycloak_theme.",
"Example in the starter: https://github.com/keycloakify/keycloakify-starter/tree/main/src/keycloak-theme"
].join("\n")
);
process.exit(-1);
}

View File

@ -6,56 +6,35 @@ export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[]; themes: { name: string; types: (ThemeType | "email")[] }[];
}; };
export function getMetaInfKeycloakThemesJsonFilePath(params: {
keycloakifyBuildDirPath: string;
}) {
const { keycloakifyBuildDirPath } = params;
return pathJoin(
keycloakifyBuildDirPath === "." ? "" : keycloakifyBuildDirPath,
"src",
"main",
"resources",
"META-INF",
"keycloak-themes.json"
);
}
export function readMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string;
}): MetaInfKeycloakTheme {
const { keycloakifyBuildDirPath } = params;
return JSON.parse(
fs
.readFileSync(
getMetaInfKeycloakThemesJsonFilePath({
keycloakifyBuildDirPath
})
)
.toString("utf8")
) as MetaInfKeycloakTheme;
}
export function writeMetaInfKeycloakThemes(params: { export function writeMetaInfKeycloakThemes(params: {
keycloakifyBuildDirPath: string; resourcesDirPath: string;
metaInfKeycloakThemes: MetaInfKeycloakTheme; getNewMetaInfKeycloakTheme: (params: {
metaInfKeycloakTheme: MetaInfKeycloakTheme | undefined;
}) => MetaInfKeycloakTheme;
}) { }) {
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params; const { resourcesDirPath, getNewMetaInfKeycloakTheme } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonFilePath({ const filePath = pathJoin(resourcesDirPath, "META-INF", "keycloak-themes.json");
keycloakifyBuildDirPath
const currentMetaInfKeycloakTheme = !fs.existsSync(filePath)
? undefined
: (JSON.parse(
fs.readFileSync(filePath).toString("utf8")
) as MetaInfKeycloakTheme);
const newMetaInfKeycloakThemes = getNewMetaInfKeycloakTheme({
metaInfKeycloakTheme: currentMetaInfKeycloakTheme
}); });
{ {
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath); const dirPath = pathDirname(filePath);
if (!fs.existsSync(dirPath)) { if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true }); fs.mkdirSync(dirPath, { recursive: true });
} }
} }
fs.writeFileSync( fs.writeFileSync(
metaInfKeycloakThemesJsonPath, filePath,
Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8") Buffer.from(JSON.stringify(newMetaInfKeycloakThemes, null, 2), "utf8")
); );
} }

View File

@ -9,9 +9,10 @@ import { id } from "tsafe/id";
export async function promptKeycloakVersion(params: { export async function promptKeycloakVersion(params: {
startingFromMajor: number | undefined; startingFromMajor: number | undefined;
excludeMajorVersions: number[];
cacheDirPath: string; cacheDirPath: string;
}) { }) {
const { startingFromMajor, cacheDirPath } = params; const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params;
const { getLatestsSemVersionedTag } = (() => { const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => { const { octokit } = (() => {
@ -95,6 +96,10 @@ export async function promptKeycloakVersion(params: {
return; return;
} }
if (excludeMajorVersions.includes(semVersionedTag.version.major)) {
return;
}
const currentSemVersionedTag = semVersionedTagByMajor.get( const currentSemVersionedTag = semVersionedTagByMajor.get(
semVersionedTag.version.major semVersionedTag.version.major
); );

View File

@ -109,7 +109,7 @@ export async function appBuild(params: {
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
const child = child_process.spawn(command, args, { cwd }); const child = child_process.spawn(command, args, { cwd, shell: true });
child.stdout.on("data", data => { child.stdout.on("data", data => {
if (data.toString("utf8").includes("gzip:")) { if (data.toString("utf8").includes("gzip:")) {

View File

@ -1,4 +1,4 @@
import { skipBuildJarsEnvName } from "../shared/constants"; import { buildForKeycloakMajorVersionEnvName } from "../shared/constants";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -14,10 +14,10 @@ export type BuildContextLike = {
assert<BuildContext extends BuildContextLike ? true : false>(); assert<BuildContext extends BuildContextLike ? true : false>();
export async function keycloakifyBuild(params: { export async function keycloakifyBuild(params: {
doSkipBuildJars: boolean; buildForKeycloakMajorVersionNumber: number;
buildContext: BuildContextLike; buildContext: BuildContextLike;
}): Promise<{ isKeycloakifyBuildSuccess: boolean }> { }): Promise<{ isKeycloakifyBuildSuccess: boolean }> {
const { buildContext, doSkipBuildJars } = params; const { buildForKeycloakMajorVersionNumber, buildContext } = params;
const dResult = new Deferred<{ isSuccess: boolean }>(); const dResult = new Deferred<{ isSuccess: boolean }>();
@ -25,8 +25,9 @@ export async function keycloakifyBuild(params: {
cwd: buildContext.projectDirPath, cwd: buildContext.projectDirPath,
env: { env: {
...process.env, ...process.env,
...(doSkipBuildJars ? { [skipBuildJarsEnvName]: "true" } : {}) [buildForKeycloakMajorVersionEnvName]: `${buildForKeycloakMajorVersionNumber}`
} },
shell: true
}); });
child.stdout.on("data", data => process.stdout.write(data)); child.stdout.on("data", data => process.stdout.write(data));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -587,7 +587,9 @@
"publicClient": true, "publicClient": true,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -619,7 +621,9 @@
"publicClient": false, "publicClient": false,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -695,7 +699,9 @@
"publicClient": false, "publicClient": false,
"frontchannelLogout": false, "frontchannelLogout": false,
"protocol": "openid-connect", "protocol": "openid-connect",
"attributes": {}, "attributes": {
"post.logout.redirect.uris": "+"
},
"authenticationFlowBindingOverrides": {}, "authenticationFlowBindingOverrides": {},
"fullScopeAllowed": false, "fullScopeAllowed": false,
"nodeReRegistrationTimeout": 0, "nodeReRegistrationTimeout": 0,
@ -783,6 +789,7 @@
"config": { "config": {
"introspection.token.claim": "true", "introspection.token.claim": "true",
"multivalued": "true", "multivalued": "true",
"userinfo.token.claim": "true",
"user.attribute": "foo", "user.attribute": "foo",
"id.token.claim": "true", "id.token.claim": "true",
"access.token.claim": "true", "access.token.claim": "true",
@ -827,7 +834,8 @@
"config": { "config": {
"id.token.claim": "true", "id.token.claim": "true",
"introspection.token.claim": "true", "introspection.token.claim": "true",
"access.token.claim": "true" "access.token.claim": "true",
"userinfo.token.claim": "true"
} }
} }
] ]
@ -1348,10 +1356,10 @@
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-address-mapper",
"saml-user-property-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"oidc-usermodel-attribute-mapper", "oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"oidc-address-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
@ -1423,13 +1431,13 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"saml-user-property-mapper",
"oidc-full-name-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-full-name-mapper", "oidc-address-mapper",
"saml-user-property-mapper", "oidc-usermodel-attribute-mapper",
"oidc-usermodel-property-mapper" "oidc-usermodel-property-mapper"
] ]
} }
@ -2043,7 +2051,7 @@
"name": "Terms and Conditions", "name": "Terms and Conditions",
"providerId": "TERMS_AND_CONDITIONS", "providerId": "TERMS_AND_CONDITIONS",
"enabled": true, "enabled": true,
"defaultAction": false, "defaultAction": true,
"priority": 20, "priority": 20,
"config": {} "config": {}
}, },
@ -2122,8 +2130,8 @@
"cibaExpiresIn": "120", "cibaExpiresIn": "120",
"cibaAuthRequestedUserHint": "login_hint", "cibaAuthRequestedUserHint": "login_hint",
"oauth2DeviceCodeLifespan": "600", "oauth2DeviceCodeLifespan": "600",
"oauth2DevicePollingInterval": "5",
"clientOfflineSessionMaxLifespan": "0", "clientOfflineSessionMaxLifespan": "0",
"oauth2DevicePollingInterval": "5",
"clientSessionIdleTimeout": "0", "clientSessionIdleTimeout": "0",
"parRequestUriLifespan": "60", "parRequestUriLifespan": "60",
"clientSessionMaxLifespan": "0", "clientSessionMaxLifespan": "0",

View File

@ -1501,14 +1501,14 @@
"subComponents": {}, "subComponents": {},
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"oidc-address-mapper", "oidc-address-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"oidc-sha256-pairwise-sub-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-full-name-mapper", "oidc-full-name-mapper",
"saml-user-property-mapper", "saml-user-property-mapper"
"oidc-usermodel-attribute-mapper"
] ]
} }
}, },
@ -1541,13 +1541,13 @@
"config": { "config": {
"allowed-protocol-mapper-types": [ "allowed-protocol-mapper-types": [
"oidc-sha256-pairwise-sub-mapper", "oidc-sha256-pairwise-sub-mapper",
"saml-user-property-mapper",
"oidc-usermodel-attribute-mapper",
"oidc-address-mapper",
"oidc-usermodel-property-mapper", "oidc-usermodel-property-mapper",
"oidc-full-name-mapper",
"oidc-address-mapper",
"oidc-usermodel-attribute-mapper",
"saml-user-property-mapper",
"saml-role-list-mapper", "saml-role-list-mapper",
"saml-user-attribute-mapper", "saml-user-attribute-mapper"
"oidc-full-name-mapper"
] ]
} }
}, },
@ -2200,7 +2200,7 @@
"name": "Terms and Conditions", "name": "Terms and Conditions",
"providerId": "TERMS_AND_CONDITIONS", "providerId": "TERMS_AND_CONDITIONS",
"enabled": true, "enabled": true,
"defaultAction": false, "defaultAction": true,
"priority": 20, "priority": 20,
"config": {} "config": {}
}, },
@ -2307,7 +2307,7 @@
"cibaInterval": "5", "cibaInterval": "5",
"realmReusableOtpCode": "false" "realmReusableOtpCode": "false"
}, },
"keycloakVersion": "24.0.4", "keycloakVersion": "24.0.5",
"userManagedAccessAllowed": false, "userManagedAccessAllowed": false,
"clientProfiles": { "clientProfiles": {
"profiles": [] "profiles": []

File diff suppressed because it is too large Load Diff

View File

@ -2,14 +2,16 @@ import { getBuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude"; import { exclude } from "tsafe/exclude";
import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "../shared/metaInfKeycloakThemes";
import { accountV1ThemeName, containerName } from "../shared/constants"; import { accountV1ThemeName, containerName } from "../shared/constants";
import { SemVer } from "../tools/SemVer"; import { SemVer } from "../tools/SemVer";
import type { KeycloakVersionRange } from "../shared/KeycloakVersionRange"; import { assert } from "tsafe/assert";
import { getJarFileBasename } from "../shared/getJarFileBasename";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import {
join as pathJoin,
relative as pathRelative,
sep as pathSep,
basename as pathBasename
} from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import chalk from "chalk"; import chalk from "chalk";
import chokidar from "chokidar"; import chokidar from "chokidar";
@ -21,6 +23,9 @@ import * as runExclusive from "run-exclusive";
import { extractArchive } from "../tools/extractArchive"; import { extractArchive } from "../tools/extractArchive";
import { appBuild } from "./appBuild"; import { appBuild } from "./appBuild";
import { keycloakifyBuild } from "./keycloakifyBuild"; import { keycloakifyBuild } from "./keycloakifyBuild";
import { isInside } from "../tools/isInside";
import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
export type CliCommandOptions = CliCommandOptions_common & { export type CliCommandOptions = CliCommandOptions_common & {
port: number; port: number;
@ -83,6 +88,31 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildContext = getBuildContext({ cliCommandOptions }); const buildContext = getBuildContext({ cliCommandOptions });
const { keycloakVersion } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
};
}
console.log(
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 18,
excludeMajorVersions: [22],
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
return { keycloakVersion };
})();
const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major;
{ {
const { isAppBuildSuccess } = await appBuild({ const { isAppBuildSuccess } = await appBuild({
buildContext buildContext
@ -91,121 +121,43 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
if (!isAppBuildSuccess) { if (!isAppBuildSuccess) {
console.log( console.log(
chalk.red( chalk.red(
`App build failed, exiting. Try running 'yarn build-keycloak-theme' and see what's wrong.` `App build failed, exiting. Try running 'npm run build' and see what's wrong.`
) )
); );
process.exit(1); process.exit(1);
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: false, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });
if (!isKeycloakifyBuildSuccess) { if (!isKeycloakifyBuildSuccess) {
console.log( console.log(
chalk.red( chalk.red(
`Keycloakify build failed, exiting. Try running 'yarn build-keycloak-theme' and see what's wrong.` `Keycloakify build failed, exiting. Try running 'npx keycloakify build' and see what's wrong.`
) )
); );
process.exit(1); process.exit(1);
} }
} }
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({ const jarFilePath = fs
keycloakifyBuildDirPath: buildContext.keycloakifyBuildDirPath .readdirSync(buildContext.keycloakifyBuildDirPath)
}); .filter(fileBasename => fileBasename.endsWith(".jar"))
.map(fileBasename => pathJoin(buildContext.keycloakifyBuildDirPath, fileBasename))
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0];
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some( assert(jarFilePath !== undefined);
({ name }) => name === accountV1ThemeName
);
const { keycloakVersion, keycloakMajorNumber: keycloakMajorVersionNumber } = console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`);
await (async function getKeycloakMajor(): Promise<{
keycloakVersion: string;
keycloakMajorNumber: number;
}> {
if (cliCommandOptions.keycloakVersion !== undefined) {
return {
keycloakVersion: cliCommandOptions.keycloakVersion,
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion)
.major
};
}
console.log(
chalk.cyan("On which version of Keycloak do you want to test your theme?")
);
const { keycloakVersion } = await promptKeycloakVersion({
startingFromMajor: 17,
cacheDirPath: buildContext.cacheDirPath
});
console.log(`${keycloakVersion}`);
const keycloakMajorNumber = SemVer.parse(keycloakVersion).major;
if (doesImplementAccountTheme && keycloakMajorNumber === 22) {
console.log(
[
"Unfortunately, Keycloakify themes that implements an account theme do not work on Keycloak 22",
"Please select any other Keycloak version"
].join(" ")
);
return getKeycloakMajor();
}
return { keycloakVersion, keycloakMajorNumber };
})();
const keycloakVersionRange: KeycloakVersionRange = (() => {
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
if (keycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
assert(keycloakMajorVersionNumber !== 22);
if (keycloakMajorVersionNumber === 23) {
return "23" as const;
}
return "24-and-above" as const;
})();
assert<
Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>
>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (keycloakMajorVersionNumber <= 21) {
return "21-and-below" as const;
}
return "22-and-above" as const;
})();
assert<
Equals<
typeof keycloakVersionRange,
KeycloakVersionRange.WithoutAccountTheme
>
>();
return keycloakVersionRange;
}
})();
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
console.log(`Using Keycloak ${chalk.bold(jarFileBasename)}`);
const realmJsonFilePath = await (async () => { const realmJsonFilePath = await (async () => {
if (cliCommandOptions.realmJsonFilePath !== undefined) { if (cliCommandOptions.realmJsonFilePath !== undefined) {
if (cliCommandOptions.realmJsonFilePath === "none") {
return undefined;
}
console.log( console.log(
chalk.green( chalk.green(
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}` `Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
@ -218,109 +170,103 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
}); });
} }
const dirPath = pathJoin( const internalFilePath = await (async () => {
getThisCodebaseRootDirPath(), const dirPath = pathJoin(
"src", getThisCodebaseRootDirPath(),
"bin", "src",
"start-keycloak" "bin",
); "start-keycloak"
);
const filePath = pathJoin( const filePath = pathJoin(
dirPath, dirPath,
`myrealm-realm-${keycloakMajorVersionNumber}.json` `myrealm-realm-${keycloakMajorVersionNumber}.json`
); );
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
return filePath; return filePath;
} }
console.log( console.log(
`${chalk.yellow( `${chalk.yellow(
`Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.` `Keycloakify do not have a realm configuration for Keycloak ${keycloakMajorVersionNumber} yet.`
)}` )}`
); );
console.log(chalk.cyan("Select what configuration to use:")); console.log(chalk.cyan("Select what configuration to use:"));
const { value } = await cliSelect<string>({ const { value } = await cliSelect<string>({
values: [ values: [
...fs ...fs
.readdirSync(dirPath) .readdirSync(dirPath)
.filter(fileBasename => fileBasename.endsWith(".json")), .filter(fileBasename => fileBasename.endsWith(".json")),
"none" "none"
] ]
}).catch(() => { }).catch(() => {
process.exit(-1); process.exit(-1);
}); });
if (value === "none") { if (value === "none") {
return undefined;
}
return pathJoin(dirPath, value);
})();
if (internalFilePath === undefined) {
return undefined; return undefined;
} }
return pathJoin(dirPath, value); 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;
})(); })();
const jarFilePath = pathJoin(buildContext.keycloakifyBuildDirPath, jarFileBasename); async function extractThemeResourcesFromJar() {
const { doUseBuiltInAccountV1Theme } = await (async () => {
let doUseBuiltInAccountV1Theme = false;
await extractArchive({ await extractArchive({
archiveFilePath: jarFilePath, archiveFilePath: jarFilePath,
onArchiveFile: async ({ relativeFilePathInArchive, readFile, earlyExit }) => { onArchiveFile: async ({ relativeFilePathInArchive, writeFile }) => {
for (const themeName of buildContext.themeNames) { if (isInside({ dirPath: "theme", filePath: relativeFilePathInArchive })) {
if ( await writeFile({
relativeFilePathInArchive === filePath: pathJoin(
pathJoin("theme", themeName, "account", "theme.properties") buildContext.keycloakifyBuildDirPath,
) { relativeFilePathInArchive
if ( )
(await readFile()) });
.toString("utf8")
.includes("parent=keycloak")
) {
doUseBuiltInAccountV1Theme = true;
}
earlyExit();
}
} }
} }
}); });
}
return { doUseBuiltInAccountV1Theme }; {
})(); const destDirPath = pathJoin(buildContext.keycloakifyBuildDirPath, "theme");
if (await existsAsync(destDirPath)) {
await rm(destDirPath, { recursive: true });
}
}
const accountThemePropertyPatch = !doUseBuiltInAccountV1Theme await extractThemeResourcesFromJar();
? undefined
: () => {
for (const themeName of buildContext.themeNames) {
const filePath = pathJoin(
buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
"account",
"theme.properties"
);
const sourceCode = fs.readFileSync(filePath); const jarFilePath_cacheDir = pathJoin(
buildContext.cacheDirPath,
pathBasename(jarFilePath)
);
const modifiedSourceCode = Buffer.from( fs.copyFileSync(jarFilePath, jarFilePath_cacheDir);
sourceCode
.toString("utf8")
.replace(`parent=${accountV1ThemeName}`, "parent=keycloak"),
"utf8"
);
assert(Buffer.compare(modifiedSourceCode, sourceCode) !== 0);
fs.writeFileSync(filePath, modifiedSourceCode);
}
};
accountThemePropertyPatch?.();
try { try {
child_process.execSync(`docker rm --force ${containerName}`, { child_process.execSync(`docker rm --force ${containerName}`, {
@ -342,20 +288,28 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
"-v", "-v",
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json` `${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
]), ]),
...["-v", `${jarFilePath}:/opt/keycloak/providers/keycloak-theme.jar`], ...[
"-v",
`${jarFilePath_cacheDir}:/opt/keycloak/providers/keycloak-theme.jar`
],
...(keycloakMajorVersionNumber <= 20 ...(keycloakMajorVersionNumber <= 20
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
: []), : []),
...[ ...[
...buildContext.themeNames, ...buildContext.themeNames,
...(doUseBuiltInAccountV1Theme ? [] : [accountV1ThemeName]) ...(fs.existsSync(
pathJoin(
buildContext.keycloakifyBuildDirPath,
"theme",
accountV1ThemeName
)
)
? [accountV1ThemeName]
: [])
] ]
.map(themeName => ({ .map(themeName => ({
localDirPath: pathJoin( localDirPath: pathJoin(
buildContext.keycloakifyBuildDirPath, buildContext.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme", "theme",
themeName themeName
), ),
@ -385,7 +339,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]) ...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
], ],
{ {
cwd: buildContext.keycloakifyBuildDirPath cwd: buildContext.keycloakifyBuildDirPath,
shell: true
} }
] as const; ] as const;
@ -411,6 +366,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
console.log( console.log(
[ [
"",
`The ftl files from ${chalk.bold(
`.${pathSep}${pathRelative(process.cwd(), pathJoin(buildContext.keycloakifyBuildDirPath, "theme"))}`
)} are mounted in the Keycloak container.`,
"", "",
`Keycloak Admin console: ${chalk.cyan.bold( `Keycloak Admin console: ${chalk.cyan.bold(
`http://localhost:${cliCommandOptions.port}` `http://localhost:${cliCommandOptions.port}`
@ -451,7 +410,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
} }
const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({ const { isKeycloakifyBuildSuccess } = await keycloakifyBuild({
doSkipBuildJars: true, buildForKeycloakMajorVersionNumber: keycloakMajorVersionNumber,
buildContext buildContext
}); });
@ -459,7 +418,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
return; return;
} }
accountThemePropertyPatch?.(); await extractThemeResourcesFromJar();
console.log(chalk.green("Theme rebuilt and updated in Keycloak.")); console.log(chalk.green("Theme rebuilt and updated in Keycloak."));
}); });

View File

@ -79,8 +79,16 @@ export async function getProxyFetchOptions(params: {
} }
const cafileContent = await readFile(cafile, "utf-8"); const cafileContent = await readFile(cafile, "utf-8");
const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt";
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map( return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n") ca =>
ca
.join("")
.replace(/\r?\n/g, newLinePlaceholder)
.replace(new RegExp(`^${newLinePlaceholder}`), "")
.replace(new RegExp(newLinePlaceholder, "g"), "\\n")
); );
})()) })())
); );

View File

@ -109,7 +109,7 @@ export async function extractArchive(params: {
zipFile.on("entry", async (entry: yauzl.Entry) => { zipFile.on("entry", async (entry: yauzl.Entry) => {
handle_file: { handle_file: {
// NOTE: Skip directories // NOTE: Skip directories
if (entry.fileName.endsWith(pathSep)) { if (entry.fileName.endsWith("/")) {
break handle_file; break handle_file;
} }

View File

@ -1,96 +0,0 @@
import { exec as execCallback } from "child_process";
import { readFile } from "fs/promises";
import { type FetchOptions } from "make-fetch-happen";
import { promisify } from "util";
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
function ensureSingleOrNone<T>(arg0: T | T[]) {
if (!Array.isArray(arg0)) return arg0;
if (arg0.length === 0) return undefined;
if (arg0.length === 1) return arg0[0];
throw new Error(
"Illegal configuration, expected a single value but found multiple: " +
arg0.map(String).join(", ")
);
}
type NPMConfig = Record<string, string | string[]>;
/**
* Get npm configuration as map
*/
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
const { npmWorkspaceRootDirPath } = params;
const exec = promisify(execCallback);
const stdout = await exec("npm config get", {
encoding: "utf8",
cwd: npmWorkspaceRootDirPath
}).then(({ stdout }) => stdout);
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
key in cfg
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
: { ...cfg, [key]: value };
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2) as [string, string])
.reduce(npmConfigReducer, {} as NPMConfig);
}
export type ProxyFetchOptions = Pick<
FetchOptions,
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
>;
export async function getProxyFetchOptions(params: {
npmWorkspaceRootDirPath: string;
}): Promise<ProxyFetchOptions> {
const { npmWorkspaceRootDirPath } = params;
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
function maybeBoolean(arg0: string | undefined) {
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
}
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
const cert = cfg["cert"];
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
const cafile = ensureSingleOrNone(cfg["cafile"]);
if (typeof cafile !== "undefined" && cafile !== "null") {
ca.push(
...(await (async () => {
function chunks<T>(arr: T[], size: number = 2) {
return arr
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
.filter(Boolean) as T[][];
}
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n")
);
})())
);
}
return {
proxy,
noProxy,
strictSSL,
cert,
ca: ca.length === 0 ? undefined : ca
};
}

View File

@ -2,8 +2,9 @@ import { lazy, Suspense } from "react";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "./KcContext"; import type { I18n } from "keycloakify/login/i18n";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields"; import type { KcContext } from "keycloakify/login/KcContext";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
const Login = lazy(() => import("keycloakify/login/pages/Login")); const Login = lazy(() => import("keycloakify/login/pages/Login"));
const Register = lazy(() => import("keycloakify/login/pages/Register")); const Register = lazy(() => import("keycloakify/login/pages/Register"));
@ -40,12 +41,12 @@ const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp")
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info")); const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError")); const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
type FallbackProps = PageProps<KcContext> & { type DefaultPageProps = PageProps<KcContext, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
doMakeUserConfirmPassword: boolean; doMakeUserConfirmPassword: boolean;
}; };
export default function Fallback(props: FallbackProps) { export default function DefaultPage(props: DefaultPageProps) {
const { kcContext, ...rest } = props; const { kcContext, ...rest } = props;
return ( return (

View File

@ -209,17 +209,13 @@ export declare namespace KcContext {
export type Register = Common & { export type Register = Common & {
pageId: "register.ftl"; pageId: "register.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
url: { url: {
registrationAction: string; registrationAction: string;
}; };
passwordRequired: boolean; passwordRequired: boolean;
recaptchaRequired: boolean; recaptchaRequired: boolean;
recaptchaSiteKey?: string; recaptchaSiteKey?: string;
/**
* Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension
* A Keycloak Java extension used as dependency in Keycloakify.
*/
passwordPolicies?: PasswordPolicies;
termsAcceptanceRequired?: boolean; termsAcceptanceRequired?: boolean;
}; };
@ -233,6 +229,7 @@ export declare namespace KcContext {
client: { client: {
baseUrl?: string; baseUrl?: string;
}; };
message: NonNullable<Common["message"]>;
}; };
export type Error = Common & { export type Error = Common & {
@ -479,16 +476,19 @@ export declare namespace KcContext {
export type LoginUpdateProfile = Common & { export type LoginUpdateProfile = Common & {
pageId: "login-update-profile.ftl"; pageId: "login-update-profile.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type IdpReviewUserProfile = Common & { export type IdpReviewUserProfile = Common & {
pageId: "idp-review-user-profile.ftl"; pageId: "idp-review-user-profile.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type UpdateEmail = Common & { export type UpdateEmail = Common & {
pageId: "update-email.ftl"; pageId: "update-email.ftl";
profile: UserProfile; profile: UserProfile;
passwordPolicies?: PasswordPolicies;
}; };
export type SelectAuthenticator = Common & { export type SelectAuthenticator = Common & {
@ -752,6 +752,10 @@ export declare namespace Validators {
assert<Equals<OnlyInExpected, never>>(); assert<Equals<OnlyInExpected, never>>();
} }
/**
* Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension
* A Keycloak Java extension used as dependency in Keycloakify.
*/
export type PasswordPolicies = { export type PasswordPolicies = {
/** The minimum length of the password */ /** The minimum length of the password */
length?: number; length?: number;

View File

@ -99,13 +99,22 @@ export const kcContextCommonMock: KcContext.Common = {
registrationEmailAsUsername: false registrationEmailAsUsername: false
}, },
messagesPerField: { messagesPerField: {
printIfExists: () => { get: () => "",
return undefined;
},
existsError: () => false, existsError: () => false,
get: fieldName => `Fake error for ${fieldName}`, printIfExists: function <T>(fieldName: string, text: T) {
exists: () => false, return this.get(fieldName) !== "" ? text : undefined;
getFirstError: fieldName => `Fake error for ${fieldName}` },
exists: function (fieldName) {
return this.get(fieldName) !== "";
},
getFirstError: function (...fieldNames) {
for (const fieldName of fieldNames) {
if (this.existsError(fieldName)) {
return this.get(fieldName);
}
}
return "";
}
}, },
locale: { locale: {
supported: [ supported: [
@ -212,6 +221,11 @@ export const kcContextMocks = [
clientId: "myApp", clientId: "myApp",
baseUrl: "#", baseUrl: "#",
attributes: {} attributes: {}
},
message: {
type: "info",
summary:
"This is the info message from the Keycloak server (in real environment, this message is localized)"
} }
}), }),
id<KcContext.Error>({ id<KcContext.Error>({
@ -224,7 +238,8 @@ export const kcContextMocks = [
}, },
message: { message: {
type: "error", type: "error",
summary: "This is the error message" summary:
"This is the error message from the Keycloak server (in real environment, this message is localized)"
} }
}), }),
id<KcContext.LoginResetPassword>({ id<KcContext.LoginResetPassword>({

View File

@ -6,10 +6,10 @@ import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags"; import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useSetClassName } from "keycloakify/tools/useSetClassName"; import { useSetClassName } from "keycloakify/tools/useSetClassName";
import type { I18n } from "./i18n";
import type { KcContext } from "./KcContext"; import type { KcContext } from "./KcContext";
import { useI18n } from "./i18n";
export default function Template(props: TemplateProps<KcContext>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
const { const {
displayInfo = false, displayInfo = false,
displayMessage = true, displayMessage = true,
@ -21,6 +21,7 @@ export default function Template(props: TemplateProps<KcContext>) {
documentTitle, documentTitle,
bodyClassName, bodyClassName,
kcContext, kcContext,
i18n,
doUseDefaultCss, doUseDefaultCss,
classes, classes,
children children
@ -28,7 +29,7 @@ export default function Template(props: TemplateProps<KcContext>) {
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes }); const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = useI18n({ kcContext }); const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext;

View File

@ -1,10 +1,11 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import type { KcContext } from "./KcContext";
export type TemplateProps<KcContext extends KcContext.Common> = { export type TemplateProps<KcContext, I18n> = {
kcContext: KcContext; kcContext: KcContext;
i18n: I18n;
doUseDefaultCss: boolean; doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>; classes?: Partial<Record<ClassKey, string>>;
children: ReactNode;
displayInfo?: boolean; displayInfo?: boolean;
displayMessage?: boolean; displayMessage?: boolean;
@ -16,8 +17,6 @@ export type TemplateProps<KcContext extends KcContext.Common> = {
infoNode?: ReactNode; infoNode?: ReactNode;
documentTitle?: string; documentTitle?: string;
bodyClassName?: string; bodyClassName?: string;
children: ReactNode;
}; };
export type ClassKey = export type ClassKey =

View File

@ -4,41 +4,25 @@ import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import { import {
useUserProfileForm, useUserProfileForm,
getButtonToDisplayForMultivaluedAttributeField, getButtonToDisplayForMultivaluedAttributeField,
type KcContextLike,
type FormAction, type FormAction,
type FormFieldError type FormFieldError
} from "keycloakify/login/lib/useUserProfileForm"; } from "keycloakify/login/lib/useUserProfileForm";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
import type { Attribute } from "keycloakify/login/KcContext"; import type { Attribute } from "keycloakify/login/KcContext";
import { useI18n, type I18n } from "./i18n"; import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
export type UserProfileFormFieldsProps = { export default function UserProfileFormFields(props: UserProfileFormFieldsProps<KcContext, I18n>) {
kcContext: KcContextLike; const { kcContext, i18n, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props;
kcClsx: KcClsx;
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
doMakeUserConfirmPassword: boolean;
BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
};
type BeforeAfterFieldProps = { const { advancedMsg } = i18n;
attribute: Attribute;
dispatchFormAction: React.Dispatch<FormAction>;
displayableErrors: FormFieldError[];
valueOrValues: string | string[];
kcClsx: KcClsx;
i18n: I18n;
};
export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
const { kcContext, kcClsx, onIsFormSubmittableValueChange, doMakeUserConfirmPassword, BeforeField, AfterField } = props;
const { advancedMsg } = useI18n({ kcContext });
const { const {
formState: { formFieldStates, isFormSubmittable }, formState: { formFieldStates, isFormSubmittable },
dispatchFormAction dispatchFormAction
} = useUserProfileForm({ } = useUserProfileForm({
kcContext, kcContext,
i18n,
doMakeUserConfirmPassword doMakeUserConfirmPassword
}); });
@ -46,8 +30,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps)
onIsFormSubmittableValueChange(isFormSubmittable); onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]); }, [isFormSubmittable]);
const i18n = useI18n({ kcContext });
const groupNameRef = { current: "" }; const groupNameRef = { current: "" };
return ( return (

View File

@ -0,0 +1,22 @@
import { type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
import type { Attribute } from "keycloakify/login/KcContext";
export type UserProfileFormFieldsProps<KcContext = any, I18n = any> = {
kcContext: Extract<KcContext, { profile: unknown }>;
i18n: I18n;
kcClsx: KcClsx;
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
doMakeUserConfirmPassword: boolean;
BeforeField?: (props: BeforeAfterFieldProps<I18n>) => JSX.Element | null;
AfterField?: (props: BeforeAfterFieldProps<I18n>) => JSX.Element | null;
};
type BeforeAfterFieldProps<I18n> = {
attribute: Attribute;
dispatchFormAction: React.Dispatch<FormAction>;
displayableErrors: FormFieldError[];
valueOrValues: string | string[];
kcClsx: KcClsx;
i18n: I18n;
};

View File

@ -177,7 +177,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
const { getI18n } = createGetI18n(extraMessages); const { getI18n } = createGetI18n(extraMessages);
function useI18n(params: { kcContext: KcContextLike }): I18n { function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
const { kcContext } = params; const { kcContext } = params;
const { i18n, prI18n_currentLanguage } = getI18n({ kcContext }); const { i18n, prI18n_currentLanguage } = getI18n({ kcContext });
@ -200,7 +200,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
}; };
}, []); }, []);
return i18n_toReturn; return { i18n: i18n_toReturn };
} }
return { useI18n, ofTypeI18n: Reflect<I18n>() }; return { useI18n, ofTypeI18n: Reflect<I18n>() };

View File

@ -1,10 +1,5 @@
export type { MessageKey, KcContextLike } from "./i18n"; import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
import { createUseI18n } from "./i18n"; export type { MessageKey, KcContextLike };
export { createUseI18n }; export type I18n = GenericI18n<MessageKey>;
export { createUseI18n } from "./i18n";
export { fallbackLanguageTag } from "./i18n"; export { fallbackLanguageTag } from "./i18n";
const { useI18n, ofTypeI18n } = createUseI18n({});
export type I18n = typeof ofTypeI18n;
export { useI18n };

View File

@ -11,7 +11,7 @@ import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import type { MessageKey } from "keycloakify/login/i18n"; import type { MessageKey } from "keycloakify/login/i18n";
import { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n"; import { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export type FormFieldError = { export type FormFieldError = {
errorMessage: JSX.Element; errorMessage: JSX.Element;
@ -79,10 +79,11 @@ export type KcContextLike = KcContextLike_i18n &
}; };
}; };
assert<Extract<KcContext.Register, { pageId: "register.ftl" }> extends KcContextLike ? true : false>(); assert<Extract<Extract<KcContext, { profile: unknown }>, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
export type ParamsOfUseUserProfileForm = { export type UseUserProfileFormParams = {
kcContext: KcContextLike; kcContext: KcContextLike;
i18n: I18n;
doMakeUserConfirmPassword: boolean; doMakeUserConfirmPassword: boolean;
}; };
@ -104,8 +105,8 @@ namespace internal {
}; };
} }
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm { export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
const { kcContext, doMakeUserConfirmPassword } = params; const { kcContext, i18n, doMakeUserConfirmPassword } = params;
const { insertScriptTags } = useInsertScriptTags({ const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "useUserProfileForm", componentOrHookName: "useUserProfileForm",
@ -122,174 +123,144 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}, []); }, []);
const { getErrors } = useGetErrors({ const { getErrors } = useGetErrors({
kcContext kcContext,
i18n
}); });
const initialState = useMemo((): internal.State => { const initialState = useMemo((): internal.State => {
// NOTE: We don't use te kcContext.profile.attributes directly because // NOTE: We don't use te kcContext.profile.attributes directly because
// they don't includes the password and password confirm fields and we want to add them. // they don't includes the password and password confirm fields and we want to add them.
// Also, we want to polyfill the attributes for older Keycloak version before User Profile was introduced. // We also want to apply some retro-compatibility and consistency patches.
// Finally we want to patch the changes made by Keycloak on the attributes format so we have an homogeneous const attributes: Attribute[] = (() => {
// attributes format to work with. mock_user_profile_attributes_for_older_keycloak_versions: {
const syntheticAttributes = (() => { if (
const syntheticAttributes: Attribute[] = []; "profile" in kcContext &&
"attributesByName" in kcContext.profile &&
Object.keys(kcContext.profile.attributesByName).length !== 0
) {
break mock_user_profile_attributes_for_older_keycloak_versions;
}
const attributes = (() => { if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
retrocompat_patch: { //NOTE: Handle legacy register.ftl page
if ( return (["firstName", "lastName", "email", "username"] as const)
"profile" in kcContext && .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
"attributesByName" in kcContext.profile && .map(name =>
Object.keys(kcContext.profile.attributesByName).length !== 0
) {
break retrocompat_patch;
}
if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
//NOTE: Handle legacy register.ftl page
return (["firstName", "lastName", "email", "username"] as const)
.filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
.map(name =>
id<Attribute>({
name: name,
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true,
value: (kcContext.register as any).formData[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
if ("user" in kcContext && kcContext.user instanceof Object) {
//NOTE: Handle legacy login-update-profile.ftl
return (["username", "email", "firstName", "lastName"] as const)
.filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
.map(name =>
id<Attribute>({
name: name,
displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true,
value: (kcContext as any).user[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
if ("email" in kcContext && kcContext.email instanceof Object) {
//NOTE: Handle legacy update-email.ftl
return [
id<Attribute>({ id<Attribute>({
name: "email", name: name,
displayName: id<`\${${MessageKey}}`>(`\${email}`), displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
required: true, required: true,
value: (kcContext.email as any).value ?? "", value: (kcContext.register as any).formData[name] ?? "",
html5DataAnnotations: {}, html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, annotations: {},
autocomplete: "email" autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
}) })
]; );
}
assert(false, "Unable to mock user profile from the current kcContext");
} }
return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => { if ("user" in kcContext && kcContext.user instanceof Object) {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") { //NOTE: Handle legacy login-update-profile.ftl
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } = return (["username", "email", "firstName", "lastName"] as const)
attribute_pre_group_patch as Attribute & { .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
group: string; .map(name =>
groupDisplayHeader?: string; id<Attribute>({
groupDisplayDescription?: string; name: name,
groupAnnotations: Record<string, string>; displayName: id<`\${${MessageKey}}`>(`\${${name}}`),
}; required: true,
value: (kcContext as any).user[name] ?? "",
html5DataAnnotations: {},
readOnly: false,
validators: {},
annotations: {},
autocomplete: (() => {
switch (name) {
case "email":
return "email";
case "username":
return "username";
default:
return undefined;
}
})()
})
);
}
return id<Attribute>({ if ("email" in kcContext && kcContext.email instanceof Object) {
...rest, //NOTE: Handle legacy update-email.ftl
group: { return [
name: group, id<Attribute>({
displayHeader: groupDisplayHeader, name: "email",
displayDescription: groupDisplayDescription, displayName: id<`\${${MessageKey}}`>(`\${email}`),
html5DataAnnotations: {}
}
});
}
return attribute_pre_group_patch;
});
})();
for (const attribute of attributes) {
syntheticAttributes.push(structuredCloneButFunctions(attribute));
add_password_and_password_confirm: {
if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
}
if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
// NOTE: We want to add password and password-confirm after the field that identifies the user.
// It's either email or username.
break add_password_and_password_confirm;
}
syntheticAttributes.push(
{
name: "password",
displayName: id<`\${${MessageKey}}`>("${password}"),
required: true, required: true,
value: (kcContext.email as any).value ?? "",
html5DataAnnotations: {},
readOnly: false, readOnly: false,
validators: {}, validators: {},
annotations: {}, annotations: {},
autocomplete: "new-password", autocomplete: "email"
html5DataAnnotations: {}, })
// NOTE: Compat with Keycloak version prior to 24 ];
...({ groupAnnotations: {} } as {})
},
{
name: "password-confirm",
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
html5DataAnnotations: {},
autocomplete: "new-password",
// NOTE: Compat with Keycloak version prior to 24
...({ groupAnnotations: {} } as {})
}
);
} }
assert(false, "Unable to mock user profile from the current kcContext");
} }
// NOTE: Consistency patch return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
syntheticAttributes.forEach(attribute => { })();
// Retro-compatibility and consistency patches
attributes.forEach(attribute => {
patch_legacy_group: {
if (typeof attribute.group !== "string") {
break patch_legacy_group;
}
const { group, groupDisplayHeader, groupDisplayDescription /*, groupAnnotations*/ } = attribute as Attribute & {
group: string;
groupDisplayHeader?: string;
groupDisplayDescription?: string;
groupAnnotations: Record<string, string>;
};
delete attribute.group;
// @ts-expect-error
delete attribute.groupDisplayHeader;
// @ts-expect-error
delete attribute.groupDisplayDescription;
// @ts-expect-error
delete attribute.groupAnnotations;
if (group === "") {
break patch_legacy_group;
}
attribute.group = {
name: group,
displayHeader: groupDisplayHeader,
displayDescription: groupDisplayDescription,
html5DataAnnotations: {}
};
}
// Attributes with options rendered by default as select inputs
if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
attribute.annotations.inputType = "select";
}
// Consistency patch on values/value property
{
if (getIsMultivaluedSingleField({ attribute })) { if (getIsMultivaluedSingleField({ attribute })) {
attribute.multivalued = true; attribute.multivalued = true;
} }
@ -301,65 +272,98 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
attribute.value ??= attribute.values?.[0]; attribute.value ??= attribute.values?.[0];
delete attribute.values; delete attribute.values;
} }
}); }
});
return syntheticAttributes; add_password_and_password_confirm: {
})(); if (!kcContext.passwordRequired) {
break add_password_and_password_confirm;
const initialFormFieldState = (() => {
const out: {
attribute: Attribute;
valueOrValues: string | string[];
}[] = [];
for (const attribute of syntheticAttributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
const values = attribute.values?.length ? attribute.values : [""];
apply_validator_min_range: {
if (getIsMultivaluedSingleField({ attribute })) {
break apply_validator_min_range;
}
const validator = attribute.validators.multivalued;
if (validator === undefined) {
break apply_validator_min_range;
}
const { min: minStr } = validator;
if (!minStr) {
break apply_validator_min_range;
}
const min = parseInt(`${minStr}`);
for (let index = values.length; index < min; index++) {
values.push("");
}
}
out.push({
attribute,
valueOrValues: values
});
continue;
}
out.push({
attribute,
valueOrValues: attribute.value ?? ""
});
} }
return out; attributes.forEach((attribute, i) => {
})(); if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
// NOTE: We want to add password and password-confirm after the field that identifies the user.
// It's either email or username.
return;
}
attributes.splice(
i + 1,
0,
{
name: "password",
displayName: id<`\${${MessageKey}}`>("${password}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
autocomplete: "new-password",
html5DataAnnotations: {}
},
{
name: "password-confirm",
displayName: id<`\${${MessageKey}}`>("${passwordConfirm}"),
required: true,
readOnly: false,
validators: {},
annotations: {},
html5DataAnnotations: {},
autocomplete: "new-password"
}
);
});
}
const initialFormFieldState: {
attribute: Attribute;
valueOrValues: string | string[];
}[] = [];
for (const attribute of attributes) {
handle_multi_valued_attribute: {
if (!attribute.multivalued) {
break handle_multi_valued_attribute;
}
const values = attribute.values?.length ? attribute.values : [""];
apply_validator_min_range: {
if (getIsMultivaluedSingleField({ attribute })) {
break apply_validator_min_range;
}
const validator = attribute.validators.multivalued;
if (validator === undefined) {
break apply_validator_min_range;
}
const { min: minStr } = validator;
if (!minStr) {
break apply_validator_min_range;
}
const min = parseInt(`${minStr}`);
for (let index = values.length; index < min; index++) {
values.push("");
}
}
initialFormFieldState.push({
attribute,
valueOrValues: values
});
continue;
}
initialFormFieldState.push({
attribute,
valueOrValues: attribute.value ?? ""
});
}
const initialState: internal.State = { const initialState: internal.State = {
formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
@ -523,12 +527,12 @@ type KcContextLike_useGetErrors = KcContextLike_i18n & {
assert<KcContextLike extends KcContextLike_useGetErrors ? true : false>(); assert<KcContextLike extends KcContextLike_useGetErrors ? true : false>();
function useGetErrors(params: { kcContext: KcContextLike_useGetErrors }) { function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
const { kcContext } = params; const { kcContext, i18n } = params;
const { messagesPerField, passwordPolicies } = kcContext; const { messagesPerField, passwordPolicies } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = useI18n({ kcContext }); const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
const getErrors = useConstCallback( const getErrors = useConstCallback(
(params: { (params: {

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>>) { export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,11 +13,12 @@ export default function Code(props: PageProps<Extract<KcContext, { pageId: "code
const { code } = kcContext; const { code } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
headerNode={code.success ? msg("codeSuccessTitle") : msg("codeErrorTitle", code.error)} headerNode={code.success ? msg("codeSuccessTitle") : msg("codeErrorTitle", code.error)}

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>>) { export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,10 +13,10 @@ export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext,
const { url, triggered_from_aia } = kcContext; const { url, triggered_from_aia } = kcContext;
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("deleteAccountConfirm")}> <Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("deleteAccountConfirm")}>
<form action={url.loginAction} className="form-vertical" method="post"> <form action={url.loginAction} className="form-vertical" method="post">
<div className="alert alert-warning" style={{ marginTop: "0", marginBottom: "30px" }}> <div className="alert alert-warning" style={{ marginTop: "0", marginBottom: "30px" }}>
<span className="pficon pficon-warning-triangle-o"></span> <span className="pficon pficon-warning-triangle-o"></span>

View File

@ -1,12 +1,12 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>>) { export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msgStr, msg } = useI18n({ kcContext }); const { msgStr, msg } = i18n;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -18,6 +18,7 @@ export default function DeleteCredential(props: PageProps<Extract<KcContext, { p
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
displayMessage={false} displayMessage={false}

View File

@ -1,16 +1,23 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>>) { export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { message, client, skipLink } = kcContext; const { message, client, skipLink } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} displayMessage={false} headerNode={msg("errorTitle")}> <Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
displayMessage={false}
headerNode={msg("errorTitle")}
>
<div id="kc-error-message"> <div id="kc-error-message">
<p className="instruction">{message.summary}</p> <p className="instruction">{message.summary}</p>
{!skipLink && client !== undefined && client.baseUrl !== undefined && ( {!skipLink && client !== undefined && client.baseUrl !== undefined && (

View File

@ -1,14 +1,14 @@
import { useEffect } from "react"; import { useEffect } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>>) { export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { logout } = kcContext; const { logout } = kcContext;
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
useEffect(() => { useEffect(() => {
if (logout.logoutRedirectUri) { if (logout.logoutRedirectUri) {
@ -19,6 +19,7 @@ export default function FrontchannelLogout(props: PageProps<Extract<KcContext, {
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
documentTitle={msgStr("frontchannel-logout.title")} documentTitle={msgStr("frontchannel-logout.title")}

View File

@ -2,24 +2,24 @@ import { useState } from "react";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields"; import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>> & { type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
doMakeUserConfirmPassword: boolean; doMakeUserConfirmPassword: boolean;
}; };
export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) { export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
const { kcContext, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
classes classes
}); });
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
const { url, messagesPerField } = kcContext; const { url, messagesPerField } = kcContext;
@ -28,6 +28,7 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
@ -37,6 +38,7 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
<form id="kc-idp-review-profile-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post"> <form id="kc-idp-review-profile-form" className={kcClsx("kcFormClass")} action={url.loginAction} method="post">
<UserProfileFormFields <UserProfileFormFields
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
onIsFormSubmittableValueChange={setIsFomSubmittable} onIsFormSubmittableValueChange={setIsFomSubmittable}
kcClsx={kcClsx} kcClsx={kcClsx}
doMakeUserConfirmPassword={doMakeUserConfirmPassword} doMakeUserConfirmPassword={doMakeUserConfirmPassword}

View File

@ -1,23 +1,18 @@
import { assert } from "keycloakify/tools/assert";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>>) { export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msgStr, msg } = useI18n({ kcContext }); const { msgStr, msg } = i18n;
assert(
kcContext.message !== undefined,
"No message in kcContext.message, there will always be a message in production context, add it in your mock"
);
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext; const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
displayMessage={false} displayMessage={false}

View File

@ -4,10 +4,10 @@ import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n, type I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>>) { export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -16,7 +16,6 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext; const { social, realm, url, usernameHidden, login, auth, registrationDisabled, messagesPerField } = kcContext;
const i18n = useI18n({ kcContext });
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
@ -24,6 +23,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
displayMessage={!messagesPerField.existsError("username", "password")} displayMessage={!messagesPerField.existsError("username", "password")}

View File

@ -1,10 +1,10 @@
import { getKcClsx, KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n, type I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>>) { export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,12 +13,17 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext; const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const i18n = useI18n({ kcContext });
const { msg, msgStr, advancedMsg } = i18n; const { msg, msgStr, advancedMsg } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("loginTotpTitle")}> <Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
headerNode={msg("loginTotpTitle")}
displayMessage={!messagesPerField.existsError("totp", "userLabel")}
>
<> <>
<ol id="kc-totp-settings"> <ol id="kc-totp-settings">
<li> <li>

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>>) { export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,10 +13,10 @@ export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext,
const { url, idpAlias } = kcContext; const { url, idpAlias } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmLinkIdpTitle")}> <Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmLinkIdpTitle")}>
<form id="kc-register-form" action={url.loginAction} method="post"> <form id="kc-register-form" action={url.loginAction} method="post">
<div className={kcClsx("kcFormGroupClass")}> <div className={kcClsx("kcFormGroupClass")}>
<button <button

View File

@ -1,16 +1,22 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>>) { export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { url, realm, brokerContext, idpAlias } = kcContext; const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("emailLinkIdpTitle", idpAlias)}> <Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
headerNode={msg("emailLinkIdpTitle", idpAlias)}
>
<p id="instruction1" className="instruction"> <p id="instruction1" className="instruction">
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)} {msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
</p> </p>

View File

@ -1,15 +1,15 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { PageProps } from "keycloakify/login/pages/PageProps"; import { PageProps } from "keycloakify/login/pages/PageProps";
import { KcContext } from "../KcContext"; import { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginOauth2DeviceVerifyUserCode( export default function LoginOauth2DeviceVerifyUserCode(
props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>> props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>
) { ) {
const { kcContext, doUseDefaultCss, classes, Template } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url } = kcContext; const { url } = kcContext;
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -17,7 +17,13 @@ export default function LoginOauth2DeviceVerifyUserCode(
}); });
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("oauth2DeviceVerificationTitle")}> <Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
headerNode={msg("oauth2DeviceVerificationTitle")}
>
<form <form
id="kc-user-verify-device-user-code-form" id="kc-user-verify-device-user-code-form"
className={kcClsx("kcFormClass")} className={kcClsx("kcFormClass")}

View File

@ -1,13 +1,13 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import { PageProps } from "keycloakify/login/pages/PageProps"; import { PageProps } from "keycloakify/login/pages/PageProps";
import { KcContext } from "../KcContext"; import { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>>) { export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, classes, Template } = props; const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, oauth, client } = kcContext; const { url, oauth, client } = kcContext;
const { msg, msgStr, advancedMsg, advancedMsgStr } = useI18n({ kcContext }); const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -17,6 +17,7 @@ export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pa
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
bodyClassName="oauth" bodyClassName="oauth"

View File

@ -2,10 +2,10 @@ import { Fragment } from "react";
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>>) { export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -14,11 +14,12 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
const { otpLogin, url, messagesPerField } = kcContext; const { otpLogin, url, messagesPerField } = kcContext;
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
displayMessage={!messagesPerField.existsError("totp")} displayMessage={!messagesPerField.existsError("totp")}

View File

@ -1,16 +1,16 @@
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>>) { export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { url } = kcContext; const { url } = kcContext;
const { msg } = useI18n({ kcContext }); const { msg } = i18n;
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("pageExpiredTitle")}> <Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("pageExpiredTitle")}>
<p id="instruction1" className="instruction"> <p id="instruction1" className="instruction">
{msg("pageExpiredMsg1")} {msg("pageExpiredMsg1")}
<a id="loginRestartLink" href={url.loginRestartFlowUrl}> <a id="loginRestartLink" href={url.loginRestartFlowUrl}>

View File

@ -4,10 +4,10 @@ import { assert } from "tsafe/assert";
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n, type I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginPassword(props: PageProps<Extract<KcContext, { pageId: "login-password.ftl" }>>) { export default function LoginPassword(props: PageProps<Extract<KcContext, { pageId: "login-password.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -16,7 +16,6 @@ export default function LoginPassword(props: PageProps<Extract<KcContext, { page
const { realm, url, messagesPerField } = kcContext; const { realm, url, messagesPerField } = kcContext;
const i18n = useI18n({ kcContext });
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
@ -24,6 +23,7 @@ export default function LoginPassword(props: PageProps<Extract<KcContext, { page
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
headerNode={msg("doLogIn")} headerNode={msg("doLogIn")}

View File

@ -4,10 +4,10 @@ import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n, type I18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>>) { export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -16,7 +16,6 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
const { recoveryAuthnCodesConfigBean, isAppInitiatedAction } = kcContext; const { recoveryAuthnCodesConfigBean, isAppInitiatedAction } = kcContext;
const i18n = useI18n({ kcContext });
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({ const { insertScriptTags } = useInsertScriptTags({
@ -145,7 +144,13 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
}, []); }, []);
return ( return (
<Template kcContext={kcContext} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("recovery-code-config-header")}> <Template
kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss}
classes={classes}
headerNode={msg("recovery-code-config-header")}
>
<div className={clsx("pf-c-alert", "pf-m-warning", "pf-m-inline", kcClsx("kcRecoveryCodesWarning"))} aria-label="Warning alert"> <div className={clsx("pf-c-alert", "pf-m-warning", "pf-m-inline", kcClsx("kcRecoveryCodesWarning"))} aria-label="Warning alert">
<div className="pf-c-alert__icon"> <div className="pf-c-alert__icon">
<i className="pficon-warning-triangle-o" aria-hidden="true" /> <i className="pficon-warning-triangle-o" aria-hidden="true" />

View File

@ -1,10 +1,10 @@
import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps"; import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext"; import type { KcContext } from "../KcContext";
import { useI18n } from "../i18n"; import type { I18n } from "../i18n";
export default function LoginRecoveryAuthnCodeInput(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-input.ftl" }>>) { export default function LoginRecoveryAuthnCodeInput(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-input.ftl" }>, I18n>) {
const { kcContext, doUseDefaultCss, Template, classes } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { kcClsx } = getKcClsx({ const { kcClsx } = getKcClsx({
doUseDefaultCss, doUseDefaultCss,
@ -13,11 +13,12 @@ export default function LoginRecoveryAuthnCodeInput(props: PageProps<Extract<KcC
const { url, messagesPerField, recoveryAuthnCodesInputBean } = kcContext; const { url, messagesPerField, recoveryAuthnCodesInputBean } = kcContext;
const { msg, msgStr } = useI18n({ kcContext }); const { msg, msgStr } = i18n;
return ( return (
<Template <Template
kcContext={kcContext} kcContext={kcContext}
i18n={i18n}
doUseDefaultCss={doUseDefaultCss} doUseDefaultCss={doUseDefaultCss}
classes={classes} classes={classes}
headerNode={msg("auth-recovery-code-header")} headerNode={msg("auth-recovery-code-header")}

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