Compare commits

..

133 Commits

Author SHA1 Message Date
f59ee55be5 Release candidate 2024-05-14 05:53:46 +02:00
bc549af64c Less verbose js coments (for start) #542 2024-05-14 05:53:22 +02:00
39add772f7 Fix vim motion typo 2024-05-14 05:44:39 +02:00
f19e622d39 Improve a little bit the readability of the rendered template 2024-05-14 05:30:03 +02:00
7eb13db467 Fix error in ftl templat 2024-05-14 05:05:18 +02:00
ef503e271d #545 2024-05-14 04:57:38 +02:00
7d24e2716f Update build scripts 2024-05-14 04:56:06 +02:00
9bbcc21f9c Remove debug file 2024-05-14 04:55:42 +02:00
fcfabb0c3f Update build script 2024-05-14 03:55:41 +02:00
9ca3cadd10 Converts all functions without arguments at the same place in the ftl template 2024-05-14 03:30:27 +02:00
f156fec1c3 Forget to add displayRequiredFields on some pages 2024-05-14 02:39:43 +02:00
e962b37948 Fix language menu select in templates 2024-05-14 02:32:03 +02:00
3a8f1a0ed1 Remove --feature=declarative-user-profile from testing container launch script 2024-05-14 01:47:08 +02:00
e3a7bb13f5 Fix inputs using value instead of defaultValue 2024-05-14 01:33:31 +02:00
29b45497ba Add missing mock value 2024-05-14 00:03:48 +02:00
a748e8d8ec Relase candidate 2024-05-14 00:01:06 +02:00
f2fcb553a5 Pass totp.policy.getAlgorithmKey() to the freemarker template 2024-05-14 00:00:17 +02:00
a9dc11c60d Fix path error in generate theme variant 2024-05-13 23:47:28 +02:00
ee9df31b18 Release candidate 2024-05-13 23:39:39 +02:00
69d1e86a8a Fix build jar script 2024-05-13 23:39:18 +02:00
06761807a3 Fix non closed tag 2024-05-13 23:39:09 +02:00
a6c1e9bb61 Fix several logical errors 2024-05-13 23:21:27 +02:00
b70dfe96f6 Remove debug log 2024-05-13 23:20:58 +02:00
5f2b1484b5 Update the exceptions 2024-05-13 23:20:40 +02:00
d4595c999f Better portability 2024-05-13 22:32:17 +02:00
373850e32a Fix storybook build 2024-05-13 04:00:52 +02:00
6013781594 Merge branch 'main' into keycloak_24 2024-05-13 03:38:13 +02:00
1712d7e2f3 Release candidate 2024-05-13 03:37:11 +02:00
970572c441 route the pages removed in kc 24 at low level 2024-05-13 03:35:38 +02:00
38a9779a2f Done with multi target build 2024-05-13 00:40:16 +02:00
988b96825f Only buildJar function left to implement 2024-05-12 21:41:49 +02:00
10bbc8ae74 Implement generateThemeVariants 2024-05-12 21:23:20 +02:00
434b03e070 Refactor 2024-05-12 20:47:03 +02:00
9d4543a611 Checkpoint 2024-05-12 19:38:48 +02:00
b675ce7142 Checkpoint 2024-05-12 19:37:16 +02:00
04461cd660 Multi target build (checkpoint before futher refactor) 2024-05-12 19:17:38 +02:00
9b8952336b Remove retrocompatiblity before re-introducing it 2024-05-11 23:20:15 +02:00
d15ae225b1 Use Keycloak 24.0.4 assets (and use kc 24 in testing container) 2024-05-11 23:11:52 +02:00
87ed9884ff Update the getKcContext function 2024-05-11 22:48:15 +02:00
9bbfac0896 Update account theme template 2024-05-11 22:21:34 +02:00
6a487b11ce More sensible mock date for UpdateEmail 2024-05-11 22:15:28 +02:00
e00ab03c19 Provide mocks data for the new pages 2024-05-11 21:39:07 +02:00
6fff769636 Forgot to actually insert the script 2024-05-11 21:39:01 +02:00
7904627653 Add webauthn-error.ftl page 2024-05-11 19:18:52 +02:00
7a2d7e5a9f Update saml-post-form.ftl page 2024-05-11 19:04:51 +02:00
31940102f2 Update the logout-confirm.ftl page 2024-05-11 19:00:23 +02:00
2c36bfe3bb Add the login-x509-info.ftl page 2024-05-11 18:54:11 +02:00
3af2eae618 Update login-verify-email.ftl page 2024-05-11 18:27:19 +02:00
a362b0fe2c Update login-username.ftl page 2024-05-11 18:15:00 +02:00
891d190787 Update login-update-password.ftl page 2024-05-11 17:37:33 +02:00
f228e50443 Update login-reset-password.ftl page 2024-05-11 17:17:35 +02:00
898a82cae1 Add login-reset-otp.ftl page 2024-05-11 16:58:45 +02:00
9512e5dab1 Add login-recovery-authn-code-input.ftl page 2024-05-11 16:43:18 +02:00
dade9a7460 Fix type confusion 2024-05-11 16:32:21 +02:00
060b1ea8b8 Add login-recovery-authn-code-config.ftl page 2024-05-11 16:25:12 +02:00
834ae4e45b fmt 2024-05-11 16:24:13 +02:00
18c82a58a7 Fix ftl template not correctly parsing numbers 2024-05-11 16:06:42 +02:00
632641a067 Update page login-password.ftl 2024-05-11 01:30:23 +02:00
fa051e4665 Update the login-page-expired.ftl page 2024-05-11 01:15:25 +02:00
259dbbae36 Update the login-otp.ftl page 2024-05-11 01:13:09 +02:00
87d9eb34a2 Rename src/login/pages/LoginDeviceVerifyUserCode.tsx to src/login/pages/LoginOauth2DeviceVerifyUserCode.tsx to respect naming convention 2024-05-11 00:54:52 +02:00
f9c55f5a43 Update login-oauth-grant.ftl page 2024-05-11 00:47:18 +02:00
aa880219c1 Update login-config-totp.ftl 2024-05-11 00:27:47 +02:00
01899c034b Update info.ftl 2024-05-11 00:18:18 +02:00
4f930a9fba Update idp-review-user-profile.ftl page 2024-05-11 00:11:50 +02:00
4a0ca9ff3b add frontchannel-logout.ftl page 2024-05-11 00:05:58 +02:00
9cb6b73607 Remove the usePrepareTemplate hook 2024-05-10 22:15:33 +02:00
695cdd5c63 Update error.ftl 2024-05-10 21:51:46 +02:00
c2fd92f516 Add delete-account-confirm.ftl page 2024-05-10 21:48:47 +02:00
1911763fcb Add code.ftl page 2024-05-10 21:40:23 +02:00
784bc71416 Prevent multiple loading of the same script 2024-05-10 21:32:16 +02:00
d3e065591b Add webauthn-register.ftl page 2024-05-10 21:12:35 +02:00
1d87e8fe8b update webauthn-autenticate.ftl 2024-05-10 18:30:48 +02:00
f8bf54835d Effort toward reconsiliating the server templating and the react world 2024-05-10 02:45:01 +02:00
9e21b5cb93 New mechanism for dynamically loading css and js (checkpoint) 2024-05-09 18:04:31 +02:00
771d6328af Do not restrict to any perticular version of React 2024-05-08 19:50:23 +02:00
1a145a49ed update evt 2024-05-08 19:48:53 +02:00
de2d4ac497 Refactor terms 2024-05-08 19:48:16 +02:00
4baeca58de Remove Register_legacy 2024-05-08 19:24:52 +02:00
0d36ddd6d3 Update update-email.ftl page 2024-05-08 19:24:18 +02:00
ef3c190747 Update SelectAuthenticator.tsx 2024-05-08 17:11:58 +02:00
20565038f5 Remove misleading comment 2024-05-08 16:57:56 +02:00
d6a302f6a3 Add delete-credential.ftl page 2024-05-08 16:54:04 +02:00
70589b6442 Factorise LoginUserProfile and LoginUpdateProfile 2024-05-08 16:10:03 +02:00
027c8f38d8 Refactor and handle legacy login-update-profile.ftl 2024-05-08 16:04:12 +02:00
a6f7f8ff49 Remove comment 2024-05-07 20:56:56 +02:00
fa24fb41a1 Remove unused variable 2024-05-07 20:56:48 +02:00
b1bec4a343 Login page overhaul 2024-05-07 20:46:02 +02:00
03e728fe04 Load scripts after component rendered #470 2024-05-07 20:04:27 +02:00
58580555a4 Update readFieldNameUsage for new messagePerField methods 2024-05-07 18:10:22 +02:00
30362df078 Handle password field hide/reveal 2024-05-07 15:58:54 +02:00
a70c651a11 Remove dead file 2024-05-06 21:28:39 +02:00
d902859b00 Fully retrocompatible, factorized Register page 🚀 2024-05-06 21:27:36 +02:00
3abb32ec82 File structure update 2024-05-06 19:16:17 +02:00
081376cdd3 Done with the new Register page (not yet retrocompatible) 2024-05-06 18:16:32 +02:00
25621182c9 Download terms when kcContext.termsAcceptanceRequired is set to true 2024-05-06 17:44:04 +02:00
c2b990ac53 Do not inject password field when password isn't required 2024-05-06 17:41:08 +02:00
bcb70a1851 Add TermsAcceptance component 2024-05-06 17:23:24 +02:00
879e376bd4 Actually use the doUseDefaultCss param in useClassName 2024-05-06 17:00:29 +02:00
4793d6dd23 Complete UserProfileFormFields 2024-05-06 16:11:36 +02:00
4794e35989 Apply number unformat during validation if any 2024-05-06 16:07:49 +02:00
7f55bb5ce3 Load number unformat for pre form submission 2024-05-06 15:25:43 +02:00
aab1b7d490 Almost done with UserProfileFormField.tsx 2024-05-05 20:58:27 +02:00
7d8db7f48c Good progress on UserProfileFormFields component 2024-05-05 20:47:23 +02:00
12225c1265 Done with select tag 2024-05-05 18:51:33 +02:00
f81ef406fb Multivalued attributes that uses a single field have an inputType that starts with "multiselect" 2024-05-04 22:57:34 +02:00
3770ec5f0d If required multivalued single file must have at least one value 2024-05-04 22:40:51 +02:00
52a6edc9ca use valueOrValues to simplify type definitions 2024-05-04 21:27:08 +02:00
6846a683b0 We have a polyfill for Array.every 2024-05-04 20:40:45 +02:00
98a0055490 Register form hook finally completed 2024-05-04 20:36:54 +02:00
a612fce78b Checkpoint validation supporting various multi valued fields 2024-05-02 15:33:48 +02:00
fc040d8302 Progress checkpoint on useUserProfileForm 2024-05-01 18:18:34 +02:00
2b0febce19 Checkpoint before refactor again 2024-04-30 12:07:35 +02:00
d143e2d3ad Feature TextArea 2024-04-28 19:35:16 +02:00
0b72724cb4 Extract field errors into a separate component 2024-04-27 19:35:42 +02:00
9e0e8acda0 Extract form group label into a separate component 2024-04-27 19:31:28 +02:00
998f18a3ce Progress on form reactivity 2024-04-27 19:09:22 +02:00
500f558658 Simplify the API of useUserProfileForm 2024-04-22 06:53:08 +02:00
49f46c758a Start refactor of UserProfilesFormFields 2024-04-22 06:36:41 +02:00
83548afa0a Implement password policy validation 2024-04-22 06:34:50 +02:00
cf42f5b2a0 do not use custom validator to check if password confirmation matches password 2024-04-22 04:34:54 +02:00
dfdc90b686 Dot not create fake attribute field, hide password confirm at an higher level 2024-04-22 04:06:06 +02:00
2e9d2b8bd2 Big refactor of useFormValidator into useUserProfileForm 2024-04-22 04:00:39 +02:00
95c27dd97e Add multivalued field validator 2024-04-22 03:55:50 +02:00
b871c3ecc3 Update KcContext type def, use an ext to get password policies. 2024-04-21 20:29:18 +02:00
82ffa801d6 Refactor useFormValidation 2024-04-21 08:12:25 +02:00
b42bf24935 checkpoint update on useFormValidation 2024-04-20 22:12:39 +02:00
9909971316 Update Login template for Keycloak 24 2024-04-13 04:46:13 +02:00
05bd0885af Feat polifill for getFirstError and make existsError accept more than one field (kcContext.messagePerField) 2024-04-13 04:28:28 +02:00
b4abe5a22e Drop compat with Keycloak prior to v12 #359 2024-04-13 04:15:23 +02:00
c5c54cb807 Fully sync login template with Keycloak 24 2024-04-13 03:26:15 +02:00
51ec342f6f Update css classes keys to reflect Keycloak 24 2024-04-13 02:18:06 +02:00
8d1c19bf1c Update prepare template for Keycloak 24 2024-04-13 01:26:41 +02:00
96 changed files with 1844 additions and 3646 deletions

25
.github/release.yaml vendored Normal file
View File

@ -0,0 +1,25 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes 🛠
labels:
- breaking
- title: Exciting New Features 🎉
labels:
- feature
- title: Fixes 🔧
labels:
- fix
- title: Documentation 🔧
labels:
- docs
- title: CI 👷
labels:
- ci
- title: Other Changes
labels:
- '*'

View File

@ -13,8 +13,8 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again.
run: yarn format:check
@ -27,8 +27,8 @@ jobs:
os: [ ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
@ -41,8 +41,8 @@ jobs:
if: github.event_name == 'push'
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- uses: bahmutov/npm-install@v1
@ -70,7 +70,7 @@ jobs:
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
steps:
- uses: garronej/ts-ci@v2.1.2
- uses: garronej/ts-ci@v2.1.0
id: step1
with:
action_name: is_package_json_version_upgraded
@ -88,7 +88,7 @@ jobs:
needs:
- check_if_version_upgraded
steps:
- uses: softprops/action-gh-release@v2
- uses: softprops/action-gh-release@v1
with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
@ -105,18 +105,18 @@ jobs:
- create_github_release
- check_if_version_upgraded
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v4
- uses: actions/setup-node@v3
with:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: yarn build
- run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
env:
DRY_RUN: "0"
- uses: garronej/ts-ci@v2.1.2
- uses: garronej/ts-ci@v2.1.0
with:
action_name: remove_dark_mode_specific_images_from_readme
- name: Publishing on NPM

View File

@ -1,28 +1,38 @@
{
"name": "keycloakify",
"version": "10.0.0-rc.16",
"version": "10.0.0-rc.3",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
"url": "git://github.com/keycloakify/keycloakify.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "ts-node --skipProject scripts/generate-i18n-messages.ts && patch-package",
"build": "ts-node --skipProject scripts/build.ts",
"watch": "chokidar './src/**/*' -c 'yarn build'",
"prepare": "yarn generate-i18n-messages",
"build": "tsc -p src/bin && tsc -p src && tsc -p src/vite-plugin && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn run copy-files && cp -r src dist/",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "ts-node --skipProject scripts/grant-exec-perms.ts",
"copy-files": "copyfiles -u 1 'src/**/*.ftl' dist/",
"test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write",
"format:check": "yarn _format --list-different",
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/main.js copy-keycloak-resources-to-public",
"link-in-starter": "yarn link-in-app keycloakify-starter",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
},
"bin": {
"keycloakify": "dist/bin/main.js"
"copy-keycloak-resources-to-public": "dist/bin/copy-keycloak-resources-to-public.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js",
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
"keycloakify": "dist/bin/keycloakify/index.js"
},
"lint-staged": {
"*.{ts,tsx,json,md}": [
@ -40,44 +50,25 @@
"src/",
"dist/",
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/",
"dist/bin/main.js",
"dist/bin/*.index.js",
"dist/bin/shared/constants.js",
"dist/bin/shared/constants.d.ts",
"dist/bin/shared/constants.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.d.ts",
"dist/vite-plugin/index.js"
"!dist/bin/tsconfig.tsbuildinfo"
],
"keywords": [
"bluehats",
"keycloak",
"react",
"theme",
"FreeMarker",
"ftl",
"login",
"register",
"account",
"bluehats"
"register"
],
"homepage": "https://www.keycloakify.dev",
"peerDependencies": {
"react": "*"
},
"dependencies": {
"evt": "^2.5.7",
"minimal-polyfills": "^2.2.3",
"react-markdown": "^5.0.3",
"tsafe": "^1.6.6"
},
"devDependencies": {
"@babel/core": "^7.24.5",
"@babel/generator": "^7.24.5",
"@babel/parser": "^7.24.5",
"@babel/types": "^7.24.5",
"@emotion/react": "^11.11.4",
"@octokit/rest": "^20.1.1",
"@babel/core": "^7.0.0",
"@emotion/react": "^11.10.6",
"@storybook/addon-a11y": "^6.5.16",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
@ -93,36 +84,46 @@
"@types/node": "^18.15.3",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/yauzl": "^2.10.3",
"@types/yazl": "^2.4.5",
"@vercel/ncc": "^0.38.1",
"chalk": "^4.1.2",
"cheerio": "^1.0.0-rc.12",
"chokidar-cli": "^3.0.0",
"cli-select": "^1.1.2",
"@types/yauzl": "^2.10.0",
"@types/yazl": "^2.4.2",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"eslint-plugin-storybook": "^0.6.7",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"magic-string": "^0.30.7",
"make-fetch-happen": "^11.0.3",
"patch-package": "^8.0.0",
"powerhooks": "^1.0.10",
"powerhooks": "^0.26.7",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"recast": "^0.23.3",
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2",
"termost": "^0.12.0",
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.10",
"tss-react": "^4.9.10",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.3",
"tss-react": "^4.8.2",
"typescript": "^4.9.1-beta",
"vitest": "^0.29.8",
"zod-to-json-schema": "^3.20.4",
"vite": "^5.0.12"
},
"dependencies": {
"@babel/generator": "^7.22.9",
"@babel/parser": "^7.22.7",
"@babel/types": "^7.22.5",
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.5.7",
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"react-markdown": "^5.0.3",
"recast": "^0.23.3",
"rfc4648": "^1.5.2",
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
"zod": "^3.17.10"
"zod": "^3.17.10",
"magic-string": "^0.30.7"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["main"],
"baseBranches": ["main", "landingpage"],
"extends": ["config:base"],
"dependencyDashboard": false,
"bumpVersion": "patch",

View File

@ -1,98 +0,0 @@
import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
import { assert } from "tsafe/assert";
import { transformCodebase } from "../src/bin/tools/transformCodebase";
import chalk from "chalk";
console.log(chalk.cyan("Building Keycloakify..."));
const startTime = Date.now();
if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
fs.renameSync(join("dist", "bin", "main.original.js"), join("dist", "bin", "main.js"));
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
if (/[0-9]\.index.js/.test(fileBasename)) {
fs.rmSync(join("dist", "bin", fileBasename));
}
});
}
run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`);
fs.cpSync(join("dist", "bin", "main.js"), join("dist", "bin", "main.original.js"));
run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`);
transformCodebase({
"srcDirPath": join("dist", "ncc_out"),
"destDirPath": join("dist", "bin"),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
if (fileRelativePath === "index.js") {
return {
"newFileName": "main.js",
"modifiedSourceCode": sourceCode
};
}
return { "modifiedSourceCode": sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
patchDeprecatedBufferApiUsage(join("dist", "bin", "main.js"));
fs.chmodSync(
join("dist", "bin", "main.js"),
fs.statSync(join("dist", "bin", "main.js")).mode | fs.constants.S_IXUSR | fs.constants.S_IXGRP | fs.constants.S_IXOTH
);
run(`npx tsc -p ${join("src", "tsconfig.json")}`);
run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`);
if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) {
fs.renameSync(join("dist", "vite-plugin", "index.original.js"), join("dist", "vite-plugin", "index.js"));
}
run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`);
fs.cpSync(join("dist", "vite-plugin", "index.js"), join("dist", "vite-plugin", "index.original.js"));
run(`npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join("dist", "ncc_out")}`);
transformCodebase({
"srcDirPath": join("dist", "ncc_out"),
"destDirPath": join("dist", "vite-plugin"),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => {
assert(fileRelativePath === "index.js");
return { "modifiedSourceCode": sourceCode };
}
});
fs.rmSync(join("dist", "ncc_out"), { "recursive": true });
patchDeprecatedBufferApiUsage(join("dist", "vite-plugin", "index.js"));
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
function run(command: string) {
console.log(chalk.grey(`$ ${command}`));
child_process.execSync(command, { "stdio": "inherit" });
}
function patchDeprecatedBufferApiUsage(filePath: string) {
const before = fs.readFileSync(filePath).toString("utf8");
const after = before.replace(
`var buffer = new Buffer(toRead);`,
`var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);`
);
assert(after !== before);
fs.writeFileSync(filePath, Buffer.from(after, "utf8"));
}

View File

@ -2,9 +2,9 @@ import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } from "path";
import { crawl } from "../src/bin/tools/crawl";
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { rmSync } from "../src/bin/tools/fs.rmSync";
import { getLogger } from "../src/bin/tools/logger";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
@ -12,6 +12,10 @@ import { rmSync } from "../src/bin/tools/fs.rmSync";
//@ts-ignore
const propertiesParser = require("properties-parser");
const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "24.0.4";
@ -19,13 +23,13 @@ async function main() {
const tmpDirPath = pathJoin(thisCodebaseRootDirPath, "tmp_xImOef9dOd44");
rmSync(tmpDirPath, { "recursive": true, "force": true });
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
fs.mkdirSync(tmpDirPath);
fs.writeFileSync(pathJoin(tmpDirPath, ".gitignore"), Buffer.from("/*\n!.gitignore\n", "utf8"));
await downloadKeycloakDefaultTheme({
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
"buildOptions": {
@ -62,7 +66,7 @@ async function main() {
});
}
rmSync(tmpDirPath, { "recursive": true });
fs.rmSync(tmpDirPath, { recursive: true, force: true });
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
@ -101,7 +105,7 @@ async function main() {
)
);
//console.log(`${filePath} wrote`);
logger.log(`${filePath} wrote`);
});
fs.writeFileSync(

View File

@ -37,11 +37,7 @@ fs.writeFileSync(
)
);
const destSrcDirPath = pathJoin(rootDirPath, "dist", "src");
fs.rmSync(destSrcDirPath, { "recursive": true, "force": true });
fs.cpSync(pathJoin(rootDirPath, "src"), destSrcDirPath, { "recursive": true });
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than

View File

@ -1,24 +0,0 @@
import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
fs.rmSync("node_modules", { "recursive": true, "force": true });
fs.rmSync("dist", { "recursive": true, "force": true });
fs.rmSync(".yarn_home", { "recursive": true, "force": true });
run("yarn install");
run("yarn build");
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), { "recursive": true, "force": true });
run("yarn install", { "cwd": join("..", "keycloakify-starter") });
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
run(`npx chokidar '${join("src", "**", "*")}' -c 'yarn build'`);
function run(command: string, options?: { cwd: string }) {
console.log(`$ ${command}`);
child_process.execSync(command, { "stdio": "inherit", ...options });
}

View File

@ -0,0 +1,29 @@
import { execSync } from "child_process";
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
import path from "path";
const testDir = "keycloakify_starter_test";
if (existsSync(path.join(process.cwd(), testDir))) {
rmSync(path.join(process.cwd(), testDir), { recursive: true });
}
// Build and link package
execSync("yarn build");
const pkgJSON = JSON.parse(readFileSync(path.join(process.cwd(), "package.json")).toString("utf8"));
pkgJSON.main = "./index.js";
pkgJSON.types = "./index.d.ts";
pkgJSON.scripts.prepare = undefined;
writeFileSync(path.join(process.cwd(), "dist", "package.json"), JSON.stringify(pkgJSON));
// Wrapped in a try/catch because unlink errors if the package isn't linked
try {
execSync("yarn unlink");
} catch {}
execSync("yarn link", { "cwd": path.join(process.cwd(), "dist") });
// Clone latest keycloakify-starter and link to keycloakify output
execSync(`git clone https://github.com/keycloakify/keycloakify-starter.git ${testDir}`);
execSync("yarn install", { "cwd": path.join(process.cwd(), testDir) });
execSync("yarn link keycloakify", { "cwd": path.join(process.cwd(), testDir) });
//Ensure keycloak theme can be built
execSync("yarn build-keycloak-theme", { "cwd": path.join(process.cwd(), testDir) });

View File

@ -1,4 +1,4 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/shared/constants";
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert";
/**

View File

@ -3,7 +3,6 @@ import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { I18n } from "keycloakify/account/i18n";
import type { KcContext } from "./kcContext";
import { assert, type Equals } from "tsafe/assert";
import FederatedIdentity from "./pages/FederatedIdentity";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
@ -31,8 +30,6 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
return <Applications kcContext={kcContext} {...rest} />;
case "log.ftl":
return <Log kcContext={kcContext} {...rest} />;
case "federatedIdentity.ftl":
return <FederatedIdentity kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}

View File

@ -4,7 +4,7 @@ export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
export type { AccountThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -1,15 +1,9 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
import { type ThemeType } from "keycloakify/bin/constants";
export type KcContext =
| KcContext.Password
| KcContext.Account
| KcContext.Sessions
| KcContext.Totp
| KcContext.Applications
| KcContext.Log
| KcContext.FederatedIdentity;
export type KcContext = KcContext.Password | KcContext.Account | KcContext.Sessions | KcContext.Totp | KcContext.Applications | KcContext.Log;
export declare namespace KcContext {
export type Common = {
@ -33,7 +27,6 @@ export declare namespace KcContext {
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
logoutUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
@ -98,6 +91,15 @@ export declare namespace KcContext {
username?: string;
};
properties: Record<string, string | undefined>;
sessions: {
sessions: {
ipAddress: string;
started?: any;
lastAccess?: any;
expires?: any;
clients: string[];
}[];
};
};
export type Password = Common & {
@ -124,12 +126,11 @@ export declare namespace KcContext {
pageId: "sessions.ftl";
sessions: {
sessions: {
expires: string;
clients: string[];
ipAddress: string;
started: string;
lastAccess: string;
id: string;
started?: any;
lastAccess?: any;
expires?: any;
clients: string[];
}[];
};
stateChecker: string;
@ -177,21 +178,12 @@ export declare namespace KcContext {
stateChecker: string;
applications: {
applications: {
realmRolesAvailable: {
name: string;
description: string;
compositesStream?: Record<string, unknown>;
clientRole?: boolean;
composite?: boolean;
id?: string;
containerId?: string;
attributes?: Record<string, unknown>;
}[];
realmRolesAvailable: { name: string; description: string }[];
resourceRolesAvailable: Record<
string,
{
roleName: string;
roleDescription?: string;
roleDescription: string;
clientName: string;
clientId: string;
}[]
@ -200,44 +192,41 @@ export declare namespace KcContext {
clientScopesGranted: string[];
effectiveUrl?: string;
client: {
alwaysDisplayInConsole: boolean;
attributes: Record<string, unknown>;
authenticationFlowBindingOverrides: Record<string, unknown>;
baseUrl?: string;
bearerOnly: boolean;
clientAuthenticatorType: string;
clientId: string;
consentRequired: boolean;
consentScreenText: string;
description: string;
directAccessGrantsEnabled: boolean;
displayOnConsentScreen: boolean;
dynamicScope: boolean;
enabled: boolean;
frontchannelLogout: boolean;
fullScopeAllowed: boolean;
id: string;
implicitFlowEnabled: boolean;
includeInTokenScope: boolean;
managementUrl: string;
name?: string;
nodeReRegistrationTimeout: string;
notBefore: string;
protocol: string;
protocolMappersStream: Record<string, unknown>;
publicClient: boolean;
realm: Record<string, unknown>;
realmScopeMappingsStream: Record<string, unknown>;
redirectUris: string[];
registeredNodes: Record<string, unknown>;
rolesStream: Record<string, unknown>;
rootUrl?: string;
scopeMappingsStream: Record<string, unknown>;
secret: string;
serviceAccountsEnabled: boolean;
standardFlowEnabled: boolean;
surrogateAuthRequired: boolean;
bearerOnly: boolean;
id: string;
protocolMappersStream: Record<string, unknown>;
includeInTokenScope: boolean;
redirectUris: string[];
fullScopeAllowed: boolean;
registeredNodes: Record<string, unknown>;
enabled: boolean;
clientAuthenticatorType: string;
realmScopeMappingsStream: Record<string, unknown>;
scopeMappingsStream: Record<string, unknown>;
displayOnConsentScreen: boolean;
clientId: string;
rootUrl: string;
authenticationFlowBindingOverrides: Record<string, unknown>;
standardFlowEnabled: boolean;
attributes: Record<string, unknown>;
publicClient: boolean;
alwaysDisplayInConsole: boolean;
consentRequired: boolean;
notBefore: string;
rolesStream: Record<string, unknown>;
protocol: string;
dynamicScope: boolean;
directAccessGrantsEnabled: boolean;
name: string;
serviceAccountsEnabled: boolean;
frontchannelLogout: boolean;
nodeReRegistrationTimeout: string;
implicitFlowEnabled: boolean;
baseUrl: string;
webOrigins: string[];
realm: Record<string, unknown>;
};
}[];
};
@ -250,25 +239,11 @@ export declare namespace KcContext {
date: string | number | Date;
event: string;
ipAddress: string;
client: string;
details: { value: string; key: string }[];
client: any;
details: any[];
}[];
};
};
export type FederatedIdentity = Common & {
pageId: "federatedIdentity.ftl";
stateChecker: string;
federatedIdentity: {
identities: {
providerId: string;
displayName: string;
userName: string;
connected: boolean;
}[];
removeLinkPossible: boolean;
};
};
}
{

View File

@ -1,5 +1,5 @@
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
import { nameOfTheGlobal } from "keycloakify/bin/constants";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]

View File

@ -1,5 +1,5 @@
import "minimal-polyfills/Object.fromEntries";
import { resources_common, keycloak_resources } from "keycloakify/bin/shared/constants";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
@ -17,7 +17,6 @@ export const kcContextCommonMock: KcContext.Common = {
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
"logoutUrl": "#",
"getLogoutUrl": () => "#",
"logUrl": "#",
"passwordUrl": "#",
@ -157,6 +156,17 @@ export const kcContextCommonMock: KcContext.Common = {
"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"
},
"sessions": {
"sessions": [
{
"ipAddress": "127.0.0.1",
"started": new Date().toString(),
"lastAccess": new Date().toString(),
"expires": new Date().toString(),
"clients": ["Chrome", "Firefox"]
}
]
}
};
@ -190,12 +200,12 @@ export const kcContextMocks: KcContext[] = [
"sessions": {
"sessions": [
{
...kcContextCommonMock.sessions,
"ipAddress": "127.0.0.1",
"started": new Date().toString(),
"lastAccess": new Date().toString(),
"expires": new Date().toString(),
"clients": ["Chrome", "Firefox"],
"id": "f8951177-817d-4a70-9c02-86d3c170fe51"
"clients": ["Chrome", "Firefox"]
}
]
},
@ -236,25 +246,9 @@ export const kcContextMocks: KcContext[] = [
"event": "login",
"ipAddress": "172.17.0.1",
"client": "security-admin-console",
"details": [{ key: "openid-connect", value: "admin" }]
"details": ["auth_method = openid-connect, username = admin"]
}
]
}
}),
id<KcContext.FederatedIdentity>({
...kcContextCommonMock,
"stateChecker": "",
"pageId": "federatedIdentity.ftl",
"federatedIdentity": {
"identities": [
{
"providerId": "keycloak-oidc",
"displayName": "keycloak-oidc",
"userName": "John",
"connected": true
}
],
"removeLinkPossible": true
}
})
];

View File

@ -61,10 +61,10 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
<td>
{!isArrayWithEmptyObject(application.realmRolesAvailable) &&
application.realmRolesAvailable.map((role, index) => (
application.realmRolesAvailable.map(role => (
<span key={role.name}>
{role.description ? advancedMsg(role.description) : advancedMsg(role.name)}
{index < application.realmRolesAvailable.length - 1 && ", "}
{role !== application.realmRolesAvailable[application.realmRolesAvailable.length - 1] && ", "}
</span>
))}
{!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}

View File

@ -1,58 +0,0 @@
import { PageProps } from "keycloakify/account";
import { I18n } from "keycloakify/account/i18n";
import { KcContext } from "keycloakify/account/kcContext";
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
const { url, federatedIdentity, stateChecker } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity">
<div className="main-layout social">
<div className="row">
<div className="col-md-10">
<h2>{msg("federatedIdentitiesHtmlTitle")}</h2>
</div>
</div>
<div id="federated-identities">
{federatedIdentity.identities.map(identity => (
<div key={identity.providerId} className="row margin-bottom">
<div className="col-sm-2 col-md-2">
<label htmlFor={identity.providerId} className="control-label">
{identity.displayName}
</label>
</div>
<div className="col-sm-5 col-md-5">
<input disabled className="form-control" value={identity.userName} />
</div>
<div className="col-sm-5 col-md-5">
{identity.connected ? (
federatedIdentity.removeLinkPossible && (
<form action={url.socialUrl} method="post" className="form-inline">
<input type="hidden" name="stateChecker" value={stateChecker} />
<input type="hidden" name="action" value="remove" />
<input type="hidden" name="providerId" value={identity.providerId} />
<button id={`remove-link-${identity.providerId}`} className="btn btn-default">
{msg("doRemove")}
</button>
</form>
)
) : (
<form action={url.socialUrl} method="post" className="form-inline">
<input type="hidden" name="stateChecker" value={stateChecker} />
<input type="hidden" name="action" value="add" />
<input type="hidden" name="providerId" value={identity.providerId} />
<button id={`add-link-${identity.providerId}`} className="btn btn-default">
{msg("doAdd")}
</button>
</form>
)}
</div>
</div>
))}
</div>
</div>
</Template>
);
}

View File

@ -2,7 +2,7 @@ import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import { Key } from "react";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import { useGetClassName } from "../lib/useGetClassName";
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;

View File

@ -12,9 +12,12 @@ export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "
classes
});
console.log({ kcContext });
const { url, stateChecker, sessions } = kcContext;
const { msg } = i18n;
console.log({ sdf: kcContext.locale?.supported });
console.log({ asdf: "asdf" });
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="sessions">
<div className={getClassName("kcContentWrapperClass")}>

View File

@ -7,7 +7,6 @@ import { MessageKey } from "keycloakify/account/i18n/i18n";
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
@ -79,7 +78,7 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
<p>{msg("totpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.supportedApplications?.map(app => (
{totp.supportedApplications.map(app => (
<li key={app}>{msg(app as MessageKey)}</li>
))}
</ul>
@ -100,26 +99,28 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
</li>
<li>
<p>{msg("totpManualStep3")}</p>
<ul>
<li id="kc-totp-type">
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("totpInterval")}: {totp.policy.period}
<p>
<ul>
<li id="kc-totp-type">
{msg("totpType")}: {msg(`totp.${totp.policy.type}`)}
</li>
) : (
<li id="kc-totp-counter">
{msg("totpCounter")}: {totp.policy.initialCounter}
<li id="kc-totp-algorithm">
{msg("totpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
)}
</ul>
<li id="kc-totp-digits">
{msg("totpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("totpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("totpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (

13
src/bin/constants.ts Normal file
View File

@ -0,0 +1,13 @@
export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const resolvedViteConfigJsonBasename = "vite.json";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const keycloakifyBuildOptionsForPostPostBuildScriptEnvName = "KEYCLOAKIFY_BUILD_OPTIONS_POST_POST_BUILD_SCRIPT";

View File

@ -1,16 +1,112 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import { readBuildOptions } from "./shared/buildOptions";
import type { CliCommandOptions } from "./main";
#!/usr/bin/env node
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
import { downloadKeycloakStaticResources, type BuildOptionsLike } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { readBuildOptions } from "./keycloakify/buildOptions";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "./constants";
import { readThisNpmProjectVersion } from "./tools/readThisNpmProjectVersion";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "./tools/fs.rmSync";
const buildOptions = readBuildOptions({ cliCommandOptions });
export async function copyKeycloakResourcesToPublic(params: { processArgv: string[] }) {
const { processArgv } = params;
await copyKeycloakResourcesToPublic({
"buildOptions": {
...buildOptions,
"publicDirPath": buildOptions.reactAppRootDirPath
const buildOptions = readBuildOptions({ processArgv });
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const { keycloakifyBuildinfoRaw } = generateKeycloakifyBuildinfoRaw({
destDirPath,
"keycloakifyVersion": readThisNpmProjectVersion(),
buildOptions
});
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { "force": true, "recursive": true });
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"keycloakVersion": (() => {
switch (themeType) {
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
case "account":
return lastKeycloakVersionWithAccountV1;
}
})(),
themeType,
"themeDirPath": destDirPath,
buildOptions
});
}
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(buildOptions.publicDirPath, keycloak_resources, ".gitignore"), Buffer.from("*", "utf8"));
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
}
export function generateKeycloakifyBuildinfoRaw(params: {
destDirPath: string;
keycloakifyVersion: string;
buildOptions: BuildOptionsLike & {
loginThemeResourcesFromKeycloakVersion: string;
};
}) {
const { destDirPath, keycloakifyVersion, buildOptions } = params;
const { cacheDirPath, npmWorkspaceRootDirPath, loginThemeResourcesFromKeycloakVersion, ...rest } = buildOptions;
assert<Equals<typeof rest, {}>>(true);
const keycloakifyBuildinfoRaw = JSON.stringify(
{
keycloakifyVersion,
"buildOptions": {
loginThemeResourcesFromKeycloakVersion,
"cacheDirPath": pathRelative(destDirPath, cacheDirPath),
"npmWorkspaceRootDirPath": pathRelative(destDirPath, npmWorkspaceRootDirPath)
}
},
null,
2
);
return { keycloakifyBuildinfoRaw };
}
async function main() {
await copyKeycloakResourcesToPublic({
"processArgv": process.argv.slice(2)
});
}
if (require.main === module) {
main();
}

View File

@ -1,12 +1,15 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./downloadAndUnzip";
import { type BuildOptions } from "./buildOptions";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getLogger } from "./tools/logger";
import { readBuildOptions, type BuildOptions } from "./keycloakify/buildOptions";
import { assert } from "tsafe/assert";
import * as child_process from "child_process";
import * as fs from "fs";
import { rmSync } from "../tools/fs.rmSync";
import { rmSync } from "./tools/fs.rmSync";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
import { transformCodebase } from "../tools/transformCodebase";
import { transformCodebase } from "./tools/transformCodebase";
export type BuildOptionsLike = {
cacheDirPath: string;
@ -15,7 +18,7 @@ export type BuildOptionsLike = {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; buildOptions: BuildOptionsLike }) {
const { keycloakVersion, destDirPath, buildOptions } = params;
await downloadAndUnzip({
@ -262,3 +265,26 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st
}
});
}
async function main() {
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
buildOptions
});
}
if (require.main === module) {
main();
}

View File

@ -1,37 +0,0 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { readBuildOptions } from "./shared/buildOptions";
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
import type { CliCommandOptions } from "./main";
import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildOptions = readBuildOptions({
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": buildOptions.cacheDirPath
});
console.log(`${keycloakVersion}`);
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
console.log(
`Downloading builtins theme of Keycloak ${keycloakVersion} here ${chalk.bold(`.${pathSep}${pathRelative(process.cwd(), destDirPath)}`)}`
);
await downloadKeycloakDefaultTheme({
keycloakVersion,
destDirPath,
buildOptions
});
console.log(chalk.green(`✓ done`));
}

View File

@ -3,13 +3,13 @@ import { mkdir, writeFile, unlink } from "fs/promises";
import fetch from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import { transformCodebase } from "../tools/transformCodebase";
import { unzip, zip } from "../tools/unzip";
import { rm } from "../tools/fs.rm";
import { transformCodebase } from "./tools/transformCodebase";
import { unzip, zip } from "./tools/unzip";
import { rm } from "./tools/fs.rm";
import * as child_process from "child_process";
import { existsAsync } from "../tools/fs.existsAsync";
import type { BuildOptions } from "./buildOptions";
import { getProxyFetchOptions } from "../tools/fetchProxyOptions";
import { existsAsync } from "./tools/fs.existsAsync";
import type { BuildOptions } from "./keycloakify/buildOptions";
import { getProxyFetchOptions } from "./tools/fetchProxyOptions";
export type BuildOptionsLike = {
cacheDirPath: string;
@ -116,11 +116,11 @@ export async function downloadAndUnzip(params: {
await rm(extractDirPath, { "recursive": true });
upload_to_remote_cache_if_admin: {
upload_to_remot_cache_if_admin: {
const githubToken = process.env["KEYCLOAKIFY_ADMIN_GITHUB_PERSONAL_ACCESS_TOKEN"];
if (!githubToken) {
break upload_to_remote_cache_if_admin;
if (githubToken === undefined) {
break upload_to_remot_cache_if_admin;
}
console.log("uploading to remote cache");

View File

@ -0,0 +1,64 @@
#!/usr/bin/env node
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import { loginThemePageIds, accountThemePageIds, type LoginThemePageId, type AccountThemePageId } from "./keycloakify/generateFtl";
import { capitalize } from "tsafe/capitalize";
import { readFile, writeFile } from "fs/promises";
import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import { themeTypes, type ThemeType } from "./constants";
import { getReactAppRootDirPath } from "./keycloakify/buildOptions/getReactAppRootDirPath";
(async () => {
console.log("Select a theme type");
const { reactAppRootDirPath } = getReactAppRootDirPath({
"processArgv": process.argv.slice(2)
});
const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes]
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
console.log("Select a page you would like to eject");
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
"values": (() => {
switch (themeType) {
case "login":
return [...loginThemePageIds];
case "account":
return [...accountThemePageIds];
}
assert<Equals<typeof themeType, never>>(false);
})()
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
const { themeSrcDirPath } = getThemeSrcDirPath({ reactAppRootDirPath });
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
if (existsSync(targetFilePath)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
process.exit(-1);
}
await writeFile(targetFilePath, await readFile(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();

View File

@ -1,139 +0,0 @@
#!/usr/bin/env node
import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
import cliSelect from "cli-select";
import {
loginThemePageIds,
accountThemePageIds,
type LoginThemePageId,
type AccountThemePageId,
themeTypes,
type ThemeType
} from "./shared/constants";
import { capitalize } from "tsafe/capitalize";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import type { CliCommandOptions } from "./main";
import { readBuildOptions } from "./shared/buildOptions";
import chalk from "chalk";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
const buildOptions = readBuildOptions({
cliCommandOptions
});
console.log(chalk.cyan("Theme type:"));
const { value: themeType } = await cliSelect<ThemeType>({
"values": [...themeTypes]
}).catch(() => {
process.exit(-1);
});
console.log(`${themeType}`);
console.log(chalk.cyan("Select the page you want to customize:"));
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
"values": (() => {
switch (themeType) {
case "login":
return [...loginThemePageIds];
case "account":
return [...accountThemePageIds];
}
assert<Equals<typeof themeType, never>>(false);
})()
}).catch(() => {
process.exit(-1);
});
console.log(`${pageId}`);
const componentPageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", componentPageBasename);
if (fs.existsSync(targetFilePath)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
process.exit(-1);
}
{
const targetDirPath = pathDirname(targetFilePath);
if (!fs.existsSync(targetDirPath)) {
fs.mkdirSync(targetDirPath, { "recursive": true });
}
}
const componentPageContent = fs
.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "src", themeType, "pages", componentPageBasename))
.toString("utf8");
fs.writeFileSync(targetFilePath, Buffer.from(componentPageContent, "utf8"));
const userProfileFormFieldComponentName = "UserProfileFormFields";
console.log(
[
``,
`${chalk.green("✓")} ${chalk.bold(
pathJoin(".", pathRelative(process.cwd(), targetFilePath))
)} copy pasted from the Keycloakify source code into your project`,
``,
`You now need to update your page router:`,
``,
`${chalk.bold(pathJoin(".", pathRelative(process.cwd(), themeSrcDirPath), themeType, "KcApp.tsx"))}:`,
chalk.grey("```"),
`// ...`,
``,
chalk.green(`+const ${componentPageBasename.replace(/.tsx$/, "")} = lazy(() => import("./pages/${componentPageBasename}"));`),
...[
``,
` export default function KcApp(props: { kcContext: KcContext; }) {`,
``,
` // ...`,
``,
` return (`,
` <Suspense>`,
` {(() => {`,
` switch (kcContext.pageId) {`,
` // ...`,
`+ case "${pageId}": return (`,
`+ <Login`,
`+ {...{ kcContext, i18n, classes }}`,
`+ Template={Template}`,
...(!componentPageContent.includes(userProfileFormFieldComponentName)
? []
: [`+ ${userProfileFormFieldComponentName}={${userProfileFormFieldComponentName}}`]),
`+ doUseDefaultCss={true}`,
`+ />`,
`+ );`,
` default: return <Fallback /* .. */ />;`,
` }`,
` })()}`,
` </Suspense>`,
` );`,
` }`
].map(line => {
if (line.startsWith("+")) {
return chalk.green(line);
}
if (line.startsWith("-")) {
return chalk.red(line);
}
return chalk.grey(line);
}),
chalk.grey("```")
].join("\n")
);
}

View File

@ -1,6 +1,6 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { crawl } from "./tools/crawl";
import { join as pathJoin } from "path";
import { themeTypes } from "./constants";

View File

@ -1,17 +1,21 @@
import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme";
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { readBuildOptions } from "./shared/buildOptions";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/buildOptions";
import * as fs from "fs";
import { getThemeSrcDirPath } from "./shared/getThemeSrcDirPath";
import { getLogger } from "./tools/logger";
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
import { rmSync } from "./tools/fs.rmSync";
import type { CliCommandOptions } from "./main";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
const { cliCommandOptions } = params;
export async function main() {
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2)
});
const buildOptions = readBuildOptions({ cliCommandOptions });
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { themeSrcDirPath } = getThemeSrcDirPath({
"reactAppRootDirPath": buildOptions.reactAppRootDirPath
@ -20,24 +24,16 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) {
console.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
console.log("Initialize with the base email theme from which version of Keycloak?");
const { keycloakVersion } = await promptKeycloakVersion();
const { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary
"startingFromMajor": 17,
"cacheDirPath": buildOptions.cacheDirPath
});
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "initialize-email-theme_tmp");
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
await downloadKeycloakDefaultTheme({
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
buildOptions
@ -54,8 +50,11 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
}
console.log(`The \`${pathJoin(".", pathRelative(process.cwd(), emailThemeSrcDirPath))}\` directory have been created.`);
console.log("You can delete any file you don't modify.");
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true });
rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {
main();
}

View File

@ -2,22 +2,19 @@ import { assert, type Equals } from "tsafe/assert";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
import { join as pathJoin, dirname as pathDirname } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import * as fs from "fs/promises";
import { accountV1ThemeName } from "../../shared/constants";
import { accountV1ThemeName } from "../../constants";
import { generatePom, BuildOptionsLike as BuildOptionsLike_generatePom } from "./generatePom";
import { readFileSync } from "fs";
import { existsSync, readFileSync } from "fs";
import { isInside } from "../../tools/isInside";
import child_process from "child_process";
import { rmSync } from "../../tools/fs.rmSync";
import { getMetaInfKeycloakThemesJsonPath } from "../../shared/metaInfKeycloakThemes";
export type BuildOptionsLike = BuildOptionsLike_generatePom & {
keycloakifyBuildDirPath: string;
themeNames: string[];
artifactId: string;
themeVersion: string;
cacheDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -30,18 +27,25 @@ export async function buildJar(params: {
}): Promise<void> {
const { jarFileBasename, keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, buildOptions } = params;
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.cacheDirPath, jarFileBasename.replace(".jar", ""));
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", jarFileBasename.replace(".jar", ""));
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true, "force": true });
if (existsSync(keycloakifyBuildTmpDirPath)) {
await fs.rm(keycloakifyBuildTmpDirPath, { "recursive": true });
}
await fs.mkdir(keycloakifyBuildTmpDirPath, { "recursive": true });
await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, ".gitignore"), Buffer.from("*", "utf8"));
const srcMainResourcesRelativeDirPath = pathJoin("src", "main", "resources");
{
const keycloakThemesJsonFilePath = getMetaInfKeycloakThemesJsonPath({ "keycloakifyBuildDirPath": "" });
const keycloakThemesJsonFilePath = pathJoin(srcMainResourcesRelativeDirPath, "META-INF", "keycloak-themes.json");
const themePropertiesFilePathSet = new Set(
...buildOptions.themeNames.map(themeName => pathJoin("src", "main", "resources", "theme", themeName, "account", "theme.properties"))
...buildOptions.themeNames.map(themeName => pathJoin(srcMainResourcesRelativeDirPath, "theme", themeName, "account", "theme.properties"))
);
const accountV1RelativeDirPath = pathJoin("src", "main", "resources", "theme", accountV1ThemeName);
const accountV1RelativeDirPath = pathJoin(srcMainResourcesRelativeDirPath, "theme", accountV1ThemeName);
transformCodebase({
"srcDirPath": buildOptions.keycloakifyBuildDirPath,
@ -103,7 +107,7 @@ export async function buildJar(params: {
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
buildOptions.themeNames.map(themeName => {
const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, "src", "main", "resources", "theme", themeName, "login", pageId);
const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, srcMainResourcesRelativeDirPath, "theme", themeName, "login", pageId);
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
@ -166,5 +170,5 @@ export async function buildJar(params: {
pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)
);
rmSync(keycloakifyBuildTmpDirPath, { "recursive": true });
await fs.rm(keycloakifyBuildTmpDirPath, { "recursive": true });
}

View File

@ -3,10 +3,7 @@ import { exclude } from "tsafe/exclude";
import { keycloakAccountV1Versions, keycloakThemeAdditionalInfoExtensionVersions } from "./extensionVersions";
import { getKeycloakVersionRangeForJar } from "./getKeycloakVersionRangeForJar";
import { buildJar, BuildOptionsLike as BuildOptionsLike_buildJar } from "./buildJar";
import type { BuildOptions } from "../../shared/buildOptions";
import { getJarFileBasename } from "../../shared/getJarFileBasename";
import { readMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "../../shared/constants";
import type { BuildOptions } from "../buildOptions";
export type BuildOptionsLike = BuildOptionsLike_buildJar & {
keycloakifyBuildDirPath: string;
@ -14,12 +11,13 @@ export type BuildOptionsLike = BuildOptionsLike_buildJar & {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function buildJars(params: { buildOptions: BuildOptionsLike }): Promise<void> {
const { buildOptions } = params;
export async function buildJars(params: {
doesImplementAccountTheme: boolean;
buildOptions: BuildOptionsLike;
}): Promise<{ lastJarFileBasename: string }> {
const { doesImplementAccountTheme, buildOptions } = params;
const doesImplementAccountTheme = readMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
}).themes.some(({ name }) => name === accountV1ThemeName);
let lastJarFileBasename: string | undefined = undefined;
await Promise.all(
keycloakAccountV1Versions
@ -40,12 +38,11 @@ export async function buildJars(params: { buildOptions: BuildOptionsLike }): Pro
})
.filter(exclude(undefined))
.map(({ keycloakThemeAdditionalInfoExtensionVersion, keycloakVersionRange }) => {
const { jarFileBasename } = getJarFileBasename({ keycloakVersionRange });
const jarFileBasename = `keycloak-theme-for-kc-${keycloakVersionRange}.jar`;
return {
keycloakThemeAdditionalInfoExtensionVersion,
jarFileBasename
};
lastJarFileBasename = jarFileBasename;
return { keycloakThemeAdditionalInfoExtensionVersion, jarFileBasename };
})
.map(({ keycloakThemeAdditionalInfoExtensionVersion, jarFileBasename }) =>
buildJar({
@ -58,4 +55,8 @@ export async function buildJars(params: { buildOptions: BuildOptionsLike }): Pro
)
.flat()
);
assert(lastJarFileBasename !== undefined);
return { lastJarFileBasename };
}

View File

@ -1,5 +1,5 @@
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
export type BuildOptionsLike = {

View File

@ -1,64 +1,37 @@
import { assert, type Equals } from "tsafe/assert";
import type { KeycloakAccountV1Version, KeycloakThemeAdditionalInfoExtensionVersion } from "./extensionVersions";
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
export function getKeycloakVersionRangeForJar(params: {
doesImplementAccountTheme: boolean;
keycloakAccountV1Version: KeycloakAccountV1Version;
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
}): KeycloakVersionRange | undefined {
}): string | undefined {
const { keycloakAccountV1Version, keycloakThemeAdditionalInfoExtensionVersion, doesImplementAccountTheme } = params;
if (doesImplementAccountTheme) {
const keycloakVersionRange = (() => {
switch (keycloakAccountV1Version) {
case null:
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return "21-and-below" as const;
case "1.1.5":
return undefined;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
case "0.3":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "23" as const;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
case "0.4":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return undefined;
case "1.1.5":
return "24-and-above" as const;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
}
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme | undefined>>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (keycloakAccountV1Version !== null) {
return undefined;
}
switch (keycloakAccountV1Version) {
case null:
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return "21-and-below";
return doesImplementAccountTheme ? "21-and-below" : "21-and-below";
case "1.1.5":
return "22-and-above";
return doesImplementAccountTheme ? undefined : "22-and-above";
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
case "0.3":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return doesImplementAccountTheme ? undefined : undefined;
case "1.1.5":
return doesImplementAccountTheme ? "23" : undefined;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
case "0.4":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
return doesImplementAccountTheme ? undefined : undefined;
case "1.1.5":
return doesImplementAccountTheme ? "24-and-above" : undefined;
}
assert<Equals<typeof keycloakThemeAdditionalInfoExtensionVersion, never>>(false);
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithoutAccountTheme | undefined>>();
return keycloakVersionRange;
}
}

View File

@ -0,0 +1,21 @@
import { z } from "zod";
export type UserProvidedBuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
loginThemeResourcesFromKeycloakVersion?: string;
reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
};
export const zUserProvidedBuildOptions = z.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional()
});

View File

@ -1,17 +1,18 @@
import { parse as urlParse } from "url";
import { readParsedPackageJson } from "./parsedPackageJson";
import { join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath";
import type { CliCommandOptions } from "../main";
import { z } from "zod";
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { readResolvedViteConfig } from "./resolvedViteConfig";
import * as fs from "fs";
import { assert } from "tsafe";
import * as child_process from "child_process";
import { vitePluginSubScriptEnvNames } from "./constants";
import { getCacheDirPath } from "./getCacheDirPath";
import { getReactAppRootDirPath } from "./getReactAppRootDirPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = {
bundler: "vite" | "webpack";
isSilent: boolean;
themeVersion: string;
themeNames: string[];
extraThemeProperties: string[] | undefined;
@ -19,7 +20,6 @@ export type BuildOptions = {
artifactId: string;
loginThemeResourcesFromKeycloakVersion: string;
reactAppRootDirPath: string;
// TODO: Remove from vite type
reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string;
@ -32,98 +32,23 @@ export type BuildOptions = {
npmWorkspaceRootDirPath: string;
};
export type UserProvidedBuildOptions = {
extraThemeProperties?: string[];
artifactId?: string;
groupId?: string;
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
};
export function readBuildOptions(params: { processArgv: string[] }): BuildOptions {
const { processArgv } = params;
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
userProvidedBuildOptions: UserProvidedBuildOptions;
};
const { reactAppRootDirPath } = getReactAppRootDirPath({ processArgv });
export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions }): BuildOptions {
const { cliCommandOptions } = params;
const { cacheDirPath } = getCacheDirPath({ reactAppRootDirPath });
const reactAppRootDirPath = (() => {
if (cliCommandOptions.reactAppRootDirPath === undefined) {
return process.cwd();
}
const { resolvedViteConfig } = readResolvedViteConfig({ cacheDirPath });
return getAbsoluteAndInOsFormatPath({
"pathIsh": cliCommandOptions.reactAppRootDirPath,
"cwd": process.cwd()
});
})();
if (resolvedViteConfig === undefined && fs.existsSync(pathJoin(reactAppRootDirPath, "vite.config.ts"))) {
throw new Error("Keycloakify's Vite plugin output not found");
}
const { resolvedViteConfig } = (() => {
if (fs.readdirSync(reactAppRootDirPath).find(fileBasename => fileBasename.startsWith("vite.config")) === undefined) {
return { "resolvedViteConfig": undefined };
}
const { keycloakify: userProvidedBuildOptionsFromPackageJson, ...parsedPackageJson } = readParsedPackageJson({ reactAppRootDirPath });
const output = child_process
.execSync("npx vite", {
"cwd": reactAppRootDirPath,
"env": {
...process.env,
[vitePluginSubScriptEnvNames.resolveViteConfig]: "true"
}
})
.toString("utf8");
assert(output.includes(vitePluginSubScriptEnvNames.resolveViteConfig), "Seems like the Keycloakify's Vite plugin is not installed.");
const resolvedViteConfigStr = output.split(vitePluginSubScriptEnvNames.resolveViteConfig).reverse()[0];
const resolvedViteConfig: ResolvedViteConfig = JSON.parse(resolvedViteConfigStr);
return { resolvedViteConfig };
})();
const parsedPackageJson = (() => {
type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: UserProvidedBuildOptions & { reactAppBuildDirPath?: string };
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraThemeProperties": z.array(z.string()).optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"loginThemeResourcesFromKeycloakVersion": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(),
"themeName": z.union([z.string(), z.array(z.string())]).optional()
})
.optional()
});
{
type Got = ReturnType<(typeof zParsedPackageJson)["parse"]>;
type Expected = ParsedPackageJson;
assert<Got extends Expected ? true : false>();
assert<Expected extends Got ? true : false>();
}
return zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
})();
const userProvidedBuildOptions: UserProvidedBuildOptions = {
...parsedPackageJson.keycloakify,
const userProvidedBuildOptions = {
...userProvidedBuildOptionsFromPackageJson,
...resolvedViteConfig?.userProvidedBuildOptions
};
@ -150,9 +75,9 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
break webpack;
}
if (parsedPackageJson.keycloakify?.reactAppBuildDirPath !== undefined) {
if (userProvidedBuildOptions.reactAppBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": parsedPackageJson.keycloakify.reactAppBuildDirPath,
"pathIsh": userProvidedBuildOptions.reactAppBuildDirPath,
"cwd": reactAppRootDirPath
});
}
@ -163,10 +88,13 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
return pathJoin(reactAppRootDirPath, resolvedViteConfig.buildDir);
})();
const argv = parseArgv(processArgv);
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
return {
"bundler": resolvedViteConfig !== undefined ? "vite" : "webpack",
"isSilent": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? parsedPackageJson.version ?? "0.0.0",
themeNames,
"extraThemeProperties": userProvidedBuildOptions.extraThemeProperties,
@ -220,23 +148,7 @@ export function readBuildOptions(params: { cliCommandOptions: CliCommandOptions
return pathJoin(reactAppRootDirPath, resolvedViteConfig.publicDir);
})(),
"cacheDirPath": (() => {
const cacheDirPath = pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": process.cwd()
});
}
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
);
return cacheDirPath;
})(),
cacheDirPath,
"urlPathname": (() => {
webpack: {
if (resolvedViteConfig !== undefined) {

View File

@ -0,0 +1,25 @@
import { join as pathJoin } from "path";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
import { getNpmWorkspaceRootDirPath } from "./getNpmWorkspaceRootDirPath";
export function getCacheDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({ reactAppRootDirPath });
const cacheDirPath = pathJoin(
(() => {
if (process.env.XDG_CACHE_HOME !== undefined) {
return getAbsoluteAndInOsFormatPath({
"pathIsh": process.env.XDG_CACHE_HOME,
"cwd": reactAppRootDirPath
});
}
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
})(),
"keycloakify"
);
return { cacheDirPath };
}

View File

@ -2,14 +2,31 @@ import * as child_process from "child_process";
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
import { assert } from "tsafe/assert";
let cache:
| {
reactAppRootDirPath: string;
npmWorkspaceRootDirPath: string;
}
| undefined = undefined;
export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
use_cache: {
if (cache === undefined || cache.reactAppRootDirPath !== reactAppRootDirPath) {
break use_cache;
}
const { npmWorkspaceRootDirPath } = cache;
return { npmWorkspaceRootDirPath };
}
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
const cwd = pathResolve(pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")]));
try {
child_process.execSync("npm config get", { cwd, "stdio": "ignore" });
child_process.execSync("npm config get", { cwd, "stdio": ["pipe", "pipe", "pipe"] });
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep, "NPM workspace not found");
@ -23,5 +40,10 @@ export function getNpmWorkspaceRootDirPath(params: { reactAppRootDirPath: string
return cwd;
})(0);
cache = {
reactAppRootDirPath,
npmWorkspaceRootDirPath
};
return { npmWorkspaceRootDirPath };
}

View File

@ -0,0 +1,23 @@
import parseArgv from "minimist";
import { getAbsoluteAndInOsFormatPath } from "../../tools/getAbsoluteAndInOsFormatPath";
export function getReactAppRootDirPath(params: { processArgv: string[] }) {
const { processArgv } = params;
const argv = parseArgv(processArgv);
const reactAppRootDirPath = (() => {
const arg = argv["project"] ?? argv["p"];
if (typeof arg !== "string") {
return process.cwd();
}
return getAbsoluteAndInOsFormatPath({
"pathIsh": arg,
"cwd": process.cwd()
});
})();
return { reactAppRootDirPath };
}

View File

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

View File

@ -0,0 +1,32 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { type UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ParsedPackageJson = {
name: string;
version?: string;
homepage?: string;
keycloakify?: UserProvidedBuildOptions;
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string().optional(),
"homepage": z.string().optional(),
"keycloakify": zUserProvidedBuildOptions.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ParsedPackageJson;
export function readParsedPackageJson(params: { reactAppRootDirPath: string }) {
const { reactAppRootDirPath } = params;
if (parsedPackageJson) {
return parsedPackageJson;
}
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactAppRootDirPath, "package.json")).toString("utf8")));
return parsedPackageJson;
}

View File

@ -0,0 +1,74 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { join as pathJoin } from "path";
import { resolvedViteConfigJsonBasename } from "../../constants";
import type { OptionalIfCanBeUndefined } from "../../tools/OptionalIfCanBeUndefined";
import { UserProvidedBuildOptions, zUserProvidedBuildOptions } from "./UserProvidedBuildOptions";
export type ResolvedViteConfig = {
buildDir: string;
publicDir: string;
assetsDir: string;
urlPathname: string | undefined;
userProvidedBuildOptions: UserProvidedBuildOptions;
};
const zResolvedViteConfig = z.object({
"buildDir": z.string(),
"publicDir": z.string(),
"assetsDir": z.string(),
"urlPathname": z.string().optional(),
"userProvidedBuildOptions": zUserProvidedBuildOptions
});
{
type Got = ReturnType<(typeof zResolvedViteConfig)["parse"]>;
type Expected = OptionalIfCanBeUndefined<ResolvedViteConfig>;
assert<Equals<Got, Expected>>();
}
export function readResolvedViteConfig(params: { cacheDirPath: string }): {
resolvedViteConfig: ResolvedViteConfig | undefined;
} {
const { cacheDirPath } = params;
const resolvedViteConfigJsonFilePath = pathJoin(cacheDirPath, resolvedViteConfigJsonBasename);
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
return { "resolvedViteConfig": undefined };
}
const resolvedViteConfig = (() => {
if (!fs.existsSync(resolvedViteConfigJsonFilePath)) {
throw new Error("Missing Keycloakify Vite plugin output.");
}
let out: ResolvedViteConfig;
try {
out = JSON.parse(fs.readFileSync(resolvedViteConfigJsonFilePath).toString("utf8"));
} catch {
throw new Error("The output of the Keycloakify Vite plugin is not a valid JSON.");
}
try {
const zodParseReturn = zResolvedViteConfig.parse(out);
// So that objectKeys from tsafe return the expected result no matter what.
Object.keys(zodParseReturn)
.filter(key => !(key in out))
.forEach(key => {
delete (out as any)[key];
});
} catch {
throw new Error("The output of the Keycloakify Vite plugin do not match the expected schema.");
}
return out;
})();
return { resolvedViteConfig };
}

View File

@ -4,10 +4,9 @@ import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCss
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import { type ThemeType, nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, resources_common } from "../../constants";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
@ -97,9 +96,7 @@ export function generateFtlFilesCodeFactory(params: {
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlObjectToJsCodeDeclaringAnObject = fs
.readFileSync(
pathJoin(getThisCodebaseRootDirPath(), "src", "bin", "keycloakify", "generateFtl", "ftl_object_to_js_code_declaring_an_object.ftl")
)
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))

View File

@ -1 +1,2 @@
export * from "./generateFtl";
export * from "./pageId";

View File

@ -1,19 +1,3 @@
export const nameOfTheGlobal = "kcContext";
export const keycloak_resources = "keycloak-resources";
export const resources_common = "resources-common";
export const lastKeycloakVersionWithAccountV1 = "21.1.2";
export const basenameOfTheKeycloakifyResourcesDir = "build";
export const themeTypes = ["login", "account"] as const;
export const accountV1ThemeName = "account-v1";
export type ThemeType = (typeof themeTypes)[number];
export const vitePluginSubScriptEnvNames = {
"runPostBuildScript": "KEYCLOAKIFY_RUN_POST_BUILD_SCRIPT",
"resolveViteConfig": "KEYCLOAKIFY_RESOLVE_VITE_CONFIG"
} as const;
export const loginThemePageIds = [
"login.ftl",
"login-username.ftl",
@ -51,15 +35,7 @@ export const loginThemePageIds = [
"webauthn-error.ftl"
] as const;
export const accountThemePageIds = [
"password.ftl",
"account.ftl",
"sessions.ftl",
"totp.ftl",
"applications.ftl",
"log.ftl",
"federatedIdentity.ftl"
] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl", "sessions.ftl", "totp.ftl", "applications.ftl", "log.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../shared/buildOptions";
import { accountV1ThemeName } from "../shared/constants";
import type { BuildOptions } from "./buildOptions";
import { accountV1ThemeName } from "../constants";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;

View File

@ -1,32 +1,31 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../shared/buildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../shared/constants";
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
import type { BuildOptions } from "../buildOptions";
import { resources_common, lastKeycloakVersionWithAccountV1, accountV1ThemeName } from "../../constants";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { transformCodebase } from "../../tools/transformCodebase";
import { rmSync } from "../../tools/fs.rmSync";
type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
keycloakifyBuildDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
export async function bringInAccountV1(params: { buildOptions: BuildOptionsLike; srcMainResourcesDirPath: string }) {
const { buildOptions, srcMainResourcesDirPath } = params;
const builtinKeycloakThemeTmpDirPath = pathJoin(buildOptions.cacheDirPath, "bringInAccountV1_tmp");
const builtinKeycloakThemeTmpDirPath = pathJoin(srcMainResourcesDirPath, "..", "tmp_yxdE2_builtin_keycloak_theme");
await downloadKeycloakDefaultTheme({
await downloadBuiltinKeycloakTheme({
"destDirPath": builtinKeycloakThemeTmpDirPath,
"keycloakVersion": lastKeycloakVersionWithAccountV1,
buildOptions
});
const accountV1DirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", accountV1ThemeName, "account");
const accountV1DirPath = pathJoin(srcMainResourcesDirPath, "theme", accountV1ThemeName, "account");
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "account"),

View File

@ -1,11 +1,11 @@
import { transformCodebase } from "../tools/transformCodebase";
import { transformCodebase } from "../../tools/transformCodebase";
import { join as pathJoin } from "path";
import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme";
import { resources_common, type ThemeType } from "./constants";
import type { BuildOptions } from "./buildOptions";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import { resources_common, type ThemeType } from "../../constants";
import { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import * as crypto from "crypto";
import { rmSync } from "../tools/fs.rmSync";
import { rmSync } from "../../tools/fs.rmSync";
export type BuildOptionsLike = {
cacheDirPath: string;
@ -23,11 +23,11 @@ export async function downloadKeycloakStaticResources(params: {
const { themeType, themeDirPath, keycloakVersion, buildOptions } = params;
const tmpDirPath = pathJoin(
buildOptions.cacheDirPath,
`downloadKeycloakStaticResources_tmp_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
themeDirPath,
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
);
await downloadKeycloakDefaultTheme({
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
buildOptions
@ -45,5 +45,5 @@ export async function downloadKeycloakStaticResources(params: {
"destDirPath": pathJoin(resourcesPath, resources_common)
});
rmSync(tmpDirPath, { "recursive": true });
rmSync(tmpDirPath, { "recursive": true, "force": true });
}

View File

@ -1,4 +1,4 @@
import type { ThemeType } from "../../shared/constants";
import type { ThemeType } from "../../constants";
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import { readFileSync } from "fs";

View File

@ -1,31 +1,25 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path";
import { join as pathJoin, resolve as pathResolve, dirname as pathDirname } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory } from "../generateFtl";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds } from "../generateFtl";
import {
type ThemeType,
lastKeycloakVersionWithAccountV1,
keycloak_resources,
accountV1ThemeName,
basenameOfTheKeycloakifyResourcesDir,
loginThemePageIds,
accountThemePageIds
} from "../../shared/constants";
basenameOfTheKeycloakifyResourcesDir
} from "../../constants";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "../../shared/downloadKeycloakStaticResources";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import { bringInAccountV1 } from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
import { writeMetaInfKeycloakThemes, type MetaInfKeycloakTheme } from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
@ -37,20 +31,23 @@ export type BuildOptionsLike = {
assetsDirPath: string;
urlPathname: string | undefined;
npmWorkspaceRootDirPath: string;
reactAppRootDirPath: string;
keycloakifyBuildDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateSrcMainResourcesForMainTheme(params: { themeName: string; buildOptions: BuildOptionsLike }): Promise<void> {
const { themeName, buildOptions } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
export async function generateSrcMainResources(params: {
themeName: string;
themeSrcDirPath: string;
keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
srcMainResourcesDirPath: string;
}): Promise<{ doesImplementAccountTheme: boolean }> {
const { themeName, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion, srcMainResourcesDirPath } = params;
const getThemeTypeDirPath = (params: { themeType: ThemeType | "email" }) => {
const { themeType } = params;
return pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName, themeType);
return pathJoin(srcMainResourcesDirPath, "theme", themeName, themeType);
};
const cssGlobalsToDefine: Record<string, string> = {};
@ -77,7 +74,7 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
rmSync(destDirPath, { "recursive": true, "force": true });
if (themeType === "account" && implementedThemeTypes.login) {
// NOTE: We prevent doing it twice, it has been done for the login theme.
// NOTE: We prevend doing it twice, it has been done for the login theme.
transformCodebase({
"srcDirPath": pathJoin(
@ -139,9 +136,10 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
"indexHtmlCode": fs.readFileSync(pathJoin(buildOptions.reactAppBuildDirPath, "index.html")).toString("utf8"),
cssGlobalsToDefine,
buildOptions,
"keycloakifyVersion": readThisNpmPackageVersion(),
keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType
})
@ -230,32 +228,40 @@ export async function generateSrcMainResourcesForMainTheme(params: { themeName:
});
}
if (implementedThemeTypes.account) {
const parsedKeycloakThemeJson: { themes: { name: string; types: string[] }[] } = { "themes": [] };
parsedKeycloakThemeJson.themes.push({
"name": themeName,
"types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
});
account_specific_extra_work: {
if (!implementedThemeTypes.account) {
break account_specific_extra_work;
}
await bringInAccountV1({
srcMainResourcesDirPath,
buildOptions
});
parsedKeycloakThemeJson.themes.push({
"name": accountV1ThemeName,
"types": ["account"]
});
}
{
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { "themes": [] };
const keycloakThemeJsonFilePath = pathJoin(srcMainResourcesDirPath, "META-INF", "keycloak-themes.json");
metaInfKeycloakThemes.themes.push({
"name": themeName,
"types": objectEntries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
});
try {
fs.mkdirSync(pathDirname(keycloakThemeJsonFilePath));
} catch {}
if (implementedThemeTypes.account) {
metaInfKeycloakThemes.themes.push({
"name": accountV1ThemeName,
"types": ["account"]
});
}
writeMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
metaInfKeycloakThemes
});
fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(parsedKeycloakThemeJson, null, 2), "utf8"));
}
return { "doesImplementAccountTheme": implementedThemeTypes.account };
}

View File

@ -0,0 +1,44 @@
import { join as pathJoin } from "path";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { generateSrcMainResources, type BuildOptionsLike as BuildOptionsLike_generateSrcMainResources } from "./generateSrcMainResources";
import { generateThemeVariations } from "./generateThemeVariants";
export type BuildOptionsLike = BuildOptionsLike_generateSrcMainResources & {
keycloakifyBuildDirPath: string;
themeNames: string[];
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: {
themeSrcDirPath: string;
keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}): Promise<{ doesImplementAccountTheme: boolean }> {
const { themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const [themeName, ...themeVariantNames] = buildOptions.themeNames;
const srcMainResourcesDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources");
const { doesImplementAccountTheme } = await generateSrcMainResources({
themeName,
srcMainResourcesDirPath,
themeSrcDirPath,
keycloakifySrcDirPath,
keycloakifyVersion,
buildOptions
});
for (const themeVariantName of themeVariantNames) {
generateThemeVariations({
themeName,
themeVariantName,
srcMainResourcesDirPath
});
}
return { doesImplementAccountTheme };
}

View File

@ -1,19 +1,12 @@
import { join as pathJoin, extname as pathExtname, sep as pathSep } from "path";
import { transformCodebase } from "../../tools/transformCodebase";
import type { BuildOptions } from "../../shared/buildOptions";
import { readMetaInfKeycloakThemes, writeMetaInfKeycloakThemes } from "../../shared/metaInfKeycloakThemes";
import { assert } from "tsafe/assert";
import * as fs from "fs";
export type BuildOptionsLike = {
keycloakifyBuildDirPath: string;
};
export function generateThemeVariations(params: { themeName: string; themeVariantName: string; srcMainResourcesDirPath: string }) {
const { themeName, themeVariantName, srcMainResourcesDirPath } = params;
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateSrcMainResourcesForThemeVariant(params: { themeName: string; themeVariantName: string; buildOptions: BuildOptionsLike }) {
const { themeName, themeVariantName, buildOptions } = params;
const mainThemeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme", themeName);
const mainThemeDirPath = pathJoin(srcMainResourcesDirPath, "theme", themeName);
transformCodebase({
"srcDirPath": mainThemeDirPath,
@ -37,20 +30,21 @@ export function generateSrcMainResourcesForThemeVariant(params: { themeName: str
});
{
const updatedMetaInfKeycloakThemes = readMetaInfKeycloakThemes({ "keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath });
const keycloakThemeJsonFilePath = pathJoin(srcMainResourcesDirPath, "META-INF", "keycloak-themes.json");
updatedMetaInfKeycloakThemes.themes.push({
const modifiedParsedJson = JSON.parse(fs.readFileSync(keycloakThemeJsonFilePath).toString("utf8")) as {
themes: { name: string; types: string[] }[];
};
modifiedParsedJson.themes.push({
"name": themeVariantName,
"types": (() => {
const theme = updatedMetaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
const theme = modifiedParsedJson.themes.find(({ name }) => name === themeName);
assert(theme !== undefined);
return theme.types;
})()
});
writeMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath,
"metaInfKeycloakThemes": updatedMetaInfKeycloakThemes
});
fs.writeFileSync(keycloakThemeJsonFilePath, Buffer.from(JSON.stringify(modifiedParsedJson, null, 2), "utf8"));
}
}

View File

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

View File

@ -1,9 +1,10 @@
import { crawl } from "../../tools/crawl";
import { accountThemePageIds, loginThemePageIds } from "../generateFtl";
import { id } from "tsafe/id";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { type ThemeType, accountThemePageIds, loginThemePageIds } from "../../shared/constants";
import type { ThemeType } from "../../constants";
export function readExtraPagesNames(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;

View File

@ -1,16 +1,15 @@
import { crawl } from "../../tools/crawl";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../../shared/constants";
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
import type { ThemeType } from "../../constants";
/** Assumes the theme type exists */
export function readFieldNameUsage(params: { themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { themeSrcDirPath, themeType } = params;
export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; themeSrcDirPath: string; themeType: ThemeType }): string[] {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const fieldNames = new Set<string>();
for (const srcDirPath of [pathJoin(getThisCodebaseRootDirPath(), "src", themeType), pathJoin(themeSrcDirPath, themeType)]) {
for (const srcDirPath of [pathJoin(keycloakifySrcDirPath, themeType), pathJoin(themeSrcDirPath, themeType)]) {
const filePaths = crawl({ "dirPath": srcDirPath, "returnedPathsType": "absolute" }).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
for (const filePath of filePaths) {

View File

@ -1 +1,8 @@
#!/usr/bin/env node
export * from "./keycloakify";
import { main } from "./keycloakify";
if (require.main === module) {
main();
}

View File

@ -1,56 +1,25 @@
import { generateSrcMainResources } from "./generateSrcMainResources";
import { generateTheme } from "./generateTheme";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "../shared/buildOptions";
import { vitePluginSubScriptEnvNames } from "../shared/constants";
import { readBuildOptions } from "./buildOptions";
import { getLogger } from "../tools/logger";
import { getThemeSrcDirPath } from "../getThemeSrcDirPath";
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
import { readThisNpmProjectVersion } from "../tools/readThisNpmProjectVersion";
import { keycloakifyBuildOptionsForPostPostBuildScriptEnvName } from "../constants";
import { buildJars } from "./buildJars";
import type { CliCommandOptions } from "../main";
import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os";
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
exit_if_maven_not_installed: {
let commandOutput: Buffer | undefined = undefined;
export async function main() {
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2)
});
try {
commandOutput = child_process.execSync("mvn --version", { "stdio": ["ignore", "pipe", "ignore"] });
} catch {}
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
if (commandOutput?.toString("utf8").includes("Apache Maven")) {
break exit_if_maven_not_installed;
}
const installationCommand = (() => {
switch (os.platform()) {
case "darwin":
return "brew install mvn";
case "win32":
return "choco install mvn";
case "linux":
default:
return "sudo apt-get install mvn";
}
})();
console.log(`${chalk.red("Apache Maven required.")} Install it with \`${chalk.bold(installationCommand)}\` (for example)`);
process.exit(1);
}
const { cliCommandOptions } = params;
const buildOptions = readBuildOptions({ cliCommandOptions });
console.log(
[
chalk.cyan(`keycloakify v${readThisNpmPackageVersion()}`),
chalk.green(`Building the keycloak theme in .${pathSep}${pathRelative(process.cwd(), buildOptions.keycloakifyBuildDirPath)} ...`)
].join(" ")
);
const startTime = Date.now();
const { themeSrcDirPath } = getThemeSrcDirPath({ "reactAppRootDirPath": buildOptions.reactAppRootDirPath });
{
if (!fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
@ -60,7 +29,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
fs.writeFileSync(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8"));
}
await generateSrcMainResources({ buildOptions });
const { doesImplementAccountTheme } = await generateTheme({
themeSrcDirPath,
"keycloakifySrcDirPath": pathJoin(getThisCodebaseRootDirPath(), "src"),
"keycloakifyVersion": readThisNpmProjectVersion(),
buildOptions
});
run_post_build_script: {
if (buildOptions.bundler !== "vite") {
@ -71,12 +45,56 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
"cwd": buildOptions.reactAppRootDirPath,
"env": {
...process.env,
[vitePluginSubScriptEnvNames.runPostBuildScript]: JSON.stringify(buildOptions)
[keycloakifyBuildOptionsForPostPostBuildScriptEnvName]: JSON.stringify(buildOptions)
}
});
}
await buildJars({ buildOptions });
const { lastJarFileBasename } = await buildJars({
doesImplementAccountTheme,
buildOptions
});
console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`));
generateStartKeycloakTestingContainer({
"jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, lastJarFileBasename),
doesImplementAccountTheme,
buildOptions
});
logger.log(
[
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathJoin(
pathRelative(buildOptions.reactAppRootDirPath, buildOptions.keycloakifyBuildDirPath),
"keycloak-theme-for-kc-*.jar"
)}`,
"",
`To test your theme locally you can spin up a Keycloak container image with the theme pre loaded by running:`,
"",
`👉 $ .${pathSep}${pathRelative(
buildOptions.reactAppRootDirPath,
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename)
)} 👈`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
`- Create a realm: Master -> AddRealm -> Name: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeNames[0]}`,
` Clients -> account -> Login theme: ${buildOptions.themeNames[0]}`,
`- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeNames[0]} (option will appear only if you have ran npx initialize-email-theme)`,
`- Create a client Clients -> Create -> Client ID: myclient`,
` Root URL: https://www.keycloak.org/app/`,
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
` Web origins: *`,
` Login Theme: ${buildOptions.themeNames[0]}`,
` Save (button at the bottom of the page)`,
``,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,
``,
`Video tutorial: https://youtu.be/WMyGZNHQkjU`,
``
].join("\n")
);
}

View File

@ -1,7 +1,7 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;

View File

@ -1,6 +1,6 @@
import type { BuildOptions } from "../../shared/buildOptions";
import type { BuildOptions } from "../buildOptions";
import { assert } from "tsafe/assert";
import { basenameOfTheKeycloakifyResourcesDir } from "../../shared/constants";
import { basenameOfTheKeycloakifyResourcesDir } from "../../constants";
export type BuildOptionsLike = {
urlPathname: string | undefined;

View File

@ -1,5 +1,5 @@
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../../shared/buildOptions";
import type { BuildOptions } from "../../buildOptions";
import { replaceImportsInJsCode_vite } from "./vite";
import { replaceImportsInJsCode_webpack } from "./webpack";
import * as fs from "fs";

View File

@ -1,6 +1,6 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../../shared/buildOptions";
import type { BuildOptions } from "../../buildOptions";
import * as nodePath from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";

View File

@ -1,6 +1,6 @@
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../shared/constants";
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir } from "../../../constants";
import { assert } from "tsafe/assert";
import type { BuildOptions } from "../../../shared/buildOptions";
import type { BuildOptions } from "../../buildOptions";
import * as nodePath from "path";
import { replaceAll } from "../../../tools/String.prototype.replaceAll";

View File

@ -1,175 +0,0 @@
#!/usr/bin/env node
import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
export type CliCommandOptions = {
reactAppRootDirPath: string | undefined;
};
const program = termost<CliCommandOptions>(
{
"name": "keycloakify",
"description": "Keycloakify CLI",
"version": readThisNpmPackageVersion()
},
{
"onException": error => {
console.error(error);
process.exit(1);
}
}
);
const optionsKeys: string[] = [];
program.option({
"key": "reactAppRootDirPath",
"name": (() => {
const long = "project";
const short = "p";
optionsKeys.push(long, short);
return { long, short };
})(),
"description": [
`For monorepos, path to the keycloakify project.`,
"Example: `npx keycloakify build --project packages/keycloak-theme`",
"https://docs.keycloakify.dev/build-options#project-or-p-cli-option"
].join(" "),
"defaultValue": undefined
});
function skip(_context: any, argv: { options: Record<string, unknown> }) {
const unrecognizedOptionKey = Object.keys(argv.options).find(key => !optionsKeys.includes(key));
if (unrecognizedOptionKey !== undefined) {
console.error(`keycloakify: Unrecognized option: ${unrecognizedOptionKey.length === 1 ? "-" : "--"}${unrecognizedOptionKey}`);
process.exit(1);
}
return false;
}
program
.command({
"name": "build",
"description": "Build the theme (default subcommand)."
})
.task({
skip,
"handler": async cliCommandOptions => {
const { command } = await import("./keycloakify");
await command({ cliCommandOptions });
}
});
program
.command<{ port: number; keycloakVersion: string | undefined }>({
"name": "start-keycloak",
"description": "Spin up a pre configured Docker image of Keycloak to test your theme."
})
.option({
"key": "port",
"name": (() => {
const name = "port";
optionsKeys.push(name);
return name;
})(),
"description": "Keycloak server port.",
"defaultValue": 8080
})
.option({
"key": "keycloakVersion",
"name": (() => {
const name = "keycloak-version";
optionsKeys.push(name);
return name;
})(),
"description": "Use a specific version of Keycloak.",
"defaultValue": undefined
})
.task({
skip,
"handler": async cliCommandOptions => {
const { command } = await import("./start-keycloak");
await command({ cliCommandOptions });
}
});
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
.command({
"name": "eject-page",
"description": "Eject a Keycloak page."
})
.task({
skip,
"handler": async cliCommandOptions => {
const { command } = await import("./eject-page");
await command({ cliCommandOptions });
}
});
program
.command({
"name": "initialize-email-theme",
"description": "Initialize an email theme."
})
.task({
skip,
"handler": async cliCommandOptions => {
const { command } = await import("./initialize-email-theme");
await command({ cliCommandOptions });
}
});
program
.command({
"name": "copy-keycloak-resources-to-public",
"description": "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
})
.task({
skip,
"handler": async cliCommandOptions => {
const { command } = await import("./copy-keycloak-resources-to-public");
await command({ cliCommandOptions });
}
});
// Fallback to build command if no command is provided
{
const [, , ...rest] = process.argv;
if (rest.length === 0 || (rest[0].startsWith("-") && rest[0] !== "--help" && rest[0] !== "-h")) {
const { status } = child_process.spawnSync("npx", ["keycloakify", "build", ...rest], {
"stdio": "inherit"
});
process.exit(status ?? 1);
}
}

View File

@ -0,0 +1,49 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
import { lastKeycloakVersionWithAccountV1 } from "./constants";
export async function promptKeycloakVersion() {
const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
return { octokit };
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
return { getLatestsSemVersionedTag };
})();
console.log("Select Keycloak version?");
const tags = [
...(await getLatestsSemVersionedTag({
"count": 15,
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
lastKeycloakVersionWithAccountV1,
"19.0.1",
"11.0.3"
];
if (process.env["GITHUB_ACTIONS"] === "true") {
return { "keycloakVersion": tags[0] };
}
const { value: keycloakVersion } = await cliSelect<string>({
"values": tags
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
console.log(keycloakVersion);
return { keycloakVersion };
}

View File

@ -1,7 +0,0 @@
export type KeycloakVersionRange = KeycloakVersionRange.WithAccountTheme | KeycloakVersionRange.WithoutAccountTheme;
export namespace KeycloakVersionRange {
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
export type WithAccountTheme = "21-and-below" | "23" | "24-and-above";
}

View File

@ -1,90 +0,0 @@
import {
downloadKeycloakStaticResources,
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
} from "./downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { themeTypes, keycloak_resources, lastKeycloakVersionWithAccountV1 } from "../shared/constants";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import { assert } from "tsafe/assert";
import * as fs from "fs";
import { rmSync } from "../tools/fs.rmSync";
import type { BuildOptions } from "./buildOptions";
export type BuildOptionsLike = BuildOptionsLike_downloadKeycloakStaticResources & {
loginThemeResourcesFromKeycloakVersion: string;
publicDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function copyKeycloakResourcesToPublic(params: { buildOptions: BuildOptionsLike }) {
const { buildOptions } = params;
const destDirPath = pathJoin(buildOptions.publicDirPath, keycloak_resources);
const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
const keycloakifyBuildinfoRaw = JSON.stringify(
{
destDirPath,
"keycloakifyVersion": readThisNpmPackageVersion(),
"buildOptions": {
"loginThemeResourcesFromKeycloakVersion": readThisNpmPackageVersion(),
"cacheDirPath": pathRelative(destDirPath, buildOptions.cacheDirPath),
"npmWorkspaceRootDirPath": pathRelative(destDirPath, buildOptions.npmWorkspaceRootDirPath)
}
},
null,
2
);
skip_if_already_done: {
if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
break skip_if_already_done;
}
const keycloakifyBuildinfoRaw_previousRun = fs.readFileSync(keycloakifyBuildinfoFilePath).toString("utf8");
if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
break skip_if_already_done;
}
return;
}
rmSync(destDirPath, { "force": true, "recursive": true });
fs.mkdirSync(destDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"keycloakVersion": (() => {
switch (themeType) {
case "login":
return buildOptions.loginThemeResourcesFromKeycloakVersion;
case "account":
return lastKeycloakVersionWithAccountV1;
}
})(),
themeType,
"themeDirPath": destDirPath,
buildOptions
});
}
fs.writeFileSync(
pathJoin(destDirPath, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container\n",
"This directory will be automatically excluded from the final build."
].join(" ")
)
);
fs.writeFileSync(keycloakifyBuildinfoFilePath, Buffer.from(keycloakifyBuildinfoRaw, "utf8"));
}

View File

@ -1,9 +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,34 +0,0 @@
import { join as pathJoin, dirname as pathDirname } from "path";
import type { ThemeType } from "./constants";
import * as fs from "fs";
export type MetaInfKeycloakTheme = {
themes: { name: string; types: (ThemeType | "email")[] }[];
};
export function getMetaInfKeycloakThemesJsonPath(params: { keycloakifyBuildDirPath: string }) {
const { keycloakifyBuildDirPath } = params;
return pathJoin(keycloakifyBuildDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
}
export function readMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string }): MetaInfKeycloakTheme {
const { keycloakifyBuildDirPath } = params;
return JSON.parse(fs.readFileSync(getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath })).toString("utf8")) as MetaInfKeycloakTheme;
}
export function writeMetaInfKeycloakThemes(params: { keycloakifyBuildDirPath: string; metaInfKeycloakThemes: MetaInfKeycloakTheme }) {
const { keycloakifyBuildDirPath, metaInfKeycloakThemes } = params;
const metaInfKeycloakThemesJsonPath = getMetaInfKeycloakThemesJsonPath({ keycloakifyBuildDirPath });
{
const dirPath = pathDirname(metaInfKeycloakThemesJsonPath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { "recursive": true });
}
}
fs.writeFileSync(metaInfKeycloakThemesJsonPath, Buffer.from(JSON.stringify(metaInfKeycloakThemes, null, 2), "utf8"));
}

View File

@ -1,106 +0,0 @@
import { getLatestsSemVersionedTagFactory } from "../tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
import { SemVer } from "../tools/SemVer";
import { join as pathJoin, dirname as pathDirname } from "path";
import * as fs from "fs";
import type { ReturnType } from "tsafe";
import { id } from "tsafe/id";
export async function promptKeycloakVersion(params: { startingFromMajor: number | undefined; cacheDirPath: string }) {
const { startingFromMajor, cacheDirPath } = params;
const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
return { octokit };
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
return { getLatestsSemVersionedTag };
})();
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
const semVersionedTags = await (async () => {
const cacheFilePath = pathJoin(cacheDirPath, "keycloak-versions.json");
type Cache = {
time: number;
semVersionedTags: ReturnType<typeof getLatestsSemVersionedTag>;
};
use_cache: {
if (!fs.existsSync(cacheFilePath)) {
break use_cache;
}
const cache: Cache = JSON.parse(fs.readFileSync(cacheFilePath).toString("utf8"));
if (Date.now() - cache.time > 3_600_000) {
fs.unlinkSync(cacheFilePath);
break use_cache;
}
return cache.semVersionedTags;
}
const semVersionedTags = await getLatestsSemVersionedTag({
"count": 50,
"owner": "keycloak",
"repo": "keycloak"
});
{
const dirPath = pathDirname(cacheFilePath);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { "recursive": true });
}
}
fs.writeFileSync(
cacheFilePath,
JSON.stringify(
id<Cache>({
"time": Date.now(),
semVersionedTags
}),
null,
2
)
);
return semVersionedTags;
})();
semVersionedTags.forEach(semVersionedTag => {
if (startingFromMajor !== undefined && semVersionedTag.version.major < startingFromMajor) {
return;
}
const currentSemVersionedTag = semVersionedTagByMajor.get(semVersionedTag.version.major);
if (currentSemVersionedTag !== undefined && SemVer.compare(semVersionedTag.version, currentSemVersionedTag.version) === -1) {
return;
}
semVersionedTagByMajor.set(semVersionedTag.version.major, semVersionedTag);
});
const lastMajorVersions = Array.from(semVersionedTagByMajor.values()).map(({ tag }) => tag);
const { value } = await cliSelect<string>({
"values": lastMajorVersions
}).catch(() => {
process.exit(-1);
});
const keycloakVersion = value.split(" ")[0];
return { keycloakVersion };
}

View File

@ -1,236 +0,0 @@
import { readBuildOptions } from "./shared/buildOptions";
import type { CliCommandOptions as CliCommandOptions_common } from "./main";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import { readMetaInfKeycloakThemes } from "./shared/metaInfKeycloakThemes";
import { accountV1ThemeName } from "./shared/constants";
import { SemVer } from "./tools/SemVer";
import type { KeycloakVersionRange } from "./shared/KeycloakVersionRange";
import { getJarFileBasename } from "./shared/getJarFileBasename";
import { assert, type Equals } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin, posix as pathPosix } from "path";
import * as child_process from "child_process";
import chalk from "chalk";
export type CliCommandOptions = CliCommandOptions_common & {
port: number;
keycloakVersion: string | undefined;
};
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
exit_if_docker_not_installed: {
let commandOutput: Buffer | undefined = undefined;
try {
commandOutput = child_process.execSync("docker --version", { "stdio": ["ignore", "pipe", "ignore"] });
} catch {}
if (commandOutput?.toString("utf8").includes("Docker")) {
break exit_if_docker_not_installed;
}
console.log(
[
`${chalk.red("Docker required.")}`,
`Install it with Docker Desktop: ${chalk.bold.underline("https://www.docker.com/products/docker-desktop/")}`,
`(or any other way)`
].join(" ")
);
process.exit(1);
}
exit_if_docker_not_running: {
let isDockerRunning: boolean;
try {
child_process.execSync("docker info", { "stdio": "ignore" });
isDockerRunning = true;
} catch {
isDockerRunning = false;
}
if (isDockerRunning) {
break exit_if_docker_not_running;
}
console.log([`${chalk.red("Docker daemon is not running.")}`, `Please start Docker Desktop and try again.`].join(" "));
process.exit(1);
}
const { cliCommandOptions } = params;
const buildOptions = readBuildOptions({ cliCommandOptions });
exit_if_theme_not_built: {
if (fs.existsSync(buildOptions.keycloakifyBuildDirPath)) {
break exit_if_theme_not_built;
}
console.log(
[`${chalk.red("The theme has not been built.")}`, `Please run ${chalk.bold("npx vite && npx keycloakify build")} first.`].join(" ")
);
process.exit(1);
}
const metaInfKeycloakThemes = readMetaInfKeycloakThemes({
"keycloakifyBuildDirPath": buildOptions.keycloakifyBuildDirPath
});
const doesImplementAccountTheme = metaInfKeycloakThemes.themes.some(({ name }) => name === accountV1ThemeName);
const { keycloakVersion, keycloakMajorNumber } = 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("On which version of Keycloak do you want to test your theme?");
const { keycloakVersion } = await promptKeycloakVersion({
"startingFromMajor": 17,
"cacheDirPath": buildOptions.cacheDirPath
});
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 (keycloakMajorNumber <= 21) {
return "21-and-below" as const;
}
assert(keycloakMajorNumber !== 22);
if (keycloakMajorNumber === 23) {
return "23" as const;
}
return "24-and-above" as const;
})();
assert<Equals<typeof keycloakVersionRange, KeycloakVersionRange.WithAccountTheme>>();
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
if (keycloakMajorNumber <= 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 });
const mountTargets = buildOptions.themeNames
.map(themeName => {
const themeEntry = metaInfKeycloakThemes.themes.find(({ name }) => name === themeName);
assert(themeEntry !== undefined);
return themeEntry.types
.map(themeType => {
const localPathDirname = pathJoin(
buildOptions.keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme",
themeName,
themeType
);
return fs
.readdirSync(localPathDirname)
.filter(fileOrDirectoryBasename => !fileOrDirectoryBasename.endsWith(".properties"))
.map(fileOrDirectoryBasename => ({
"localPath": pathJoin(localPathDirname, fileOrDirectoryBasename),
"containerPath": pathPosix.join("/", "opt", "keycloak", "themes", themeName, themeType, fileOrDirectoryBasename)
}));
})
.flat();
})
.flat();
const containerName = "keycloak-keycloakify";
try {
child_process.execSync(`docker rm ${containerName}`, { "stdio": "ignore" });
} catch {}
const child = child_process.spawn(
"docker",
[
"run",
...["-p", `${cliCommandOptions.port}:8080`],
...["--name", containerName],
...["-e", "KEYCLOAK_ADMIN=admin"],
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
...["-v", `${pathJoin(buildOptions.keycloakifyBuildDirPath, jarFileBasename)}:/opt/keycloak/providers/keycloak-theme.jar`],
...(keycloakMajorNumber <= 20 ? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"] : []),
...mountTargets.map(({ localPath, containerPath }) => ["-v", `${localPath}:${containerPath}:rw`]).flat(),
`quay.io/keycloak/keycloak:${keycloakVersion}`,
"start-dev",
...(21 <= keycloakMajorNumber && keycloakMajorNumber < 24 ? ["--features=declarative-user-profile"] : [])
],
{
"cwd": buildOptions.keycloakifyBuildDirPath
}
);
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
{
const handler = async (data: Buffer) => {
if (!data.toString("utf8").includes("Listening on: http://0.0.0.0:8080")) {
return;
}
child.stdout.off("data", handler);
await new Promise(resolve => setTimeout(resolve, 1_000));
console.log(
[
"",
`${chalk.green("Your theme is accessible at:")}`,
`${chalk.green("➜")} ${chalk.cyan.bold("https://test.keycloakify.dev/")}`,
""
].join("\n")
);
};
child.stdout.on("data", handler);
}
child.on("exit", process.exit);
}

View File

@ -20,13 +20,13 @@ export function rmSync(dirPath: string, options: { recursive: true; force?: true
const removeDir_rec = (dirPath: string) =>
fs.readdirSync(dirPath).forEach(basename => {
const fileOrDirPath = pathJoin(dirPath, basename);
const fileOrDirpath = pathJoin(dirPath, basename);
if (fs.lstatSync(fileOrDirPath).isDirectory()) {
removeDir_rec(fileOrDirPath);
if (fs.lstatSync(fileOrDirpath).isDirectory()) {
removeDir_rec(fileOrDirpath);
return;
} else {
fs.unlinkSync(fileOrDirPath);
fs.unlinkSync(fileOrDirpath);
}
});

View File

@ -1,5 +1,4 @@
import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin, resolve as pathResolve } from "path";
import * as os from "os";
import { isAbsolute as pathIsAbsolute, sep as pathSep, join as pathJoin } from "path";
export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: string }): string {
const { pathIsh, cwd } = params;
@ -8,17 +7,11 @@ export function getAbsoluteAndInOsFormatPath(params: { pathIsh: string; cwd: str
pathOut = pathOut.replace(/\//g, pathSep);
if (pathOut.startsWith("~")) {
pathOut = pathOut.replace("~", os.homedir());
}
pathOut = pathOut.endsWith(pathSep) ? pathOut.slice(0, -1) : pathOut;
if (!pathIsAbsolute(pathOut)) {
pathOut = pathJoin(cwd, pathOut);
}
pathOut = pathResolve(pathOut);
return pathOut;
}

27
src/bin/tools/logger.ts Normal file
View File

@ -0,0 +1,27 @@
type LoggerOpts = {
force?: boolean;
};
type Logger = {
log: (message: string, opts?: LoggerOpts) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
return {
log: (message, { force } = {}) => {
if (isSilent && !force) {
return;
}
console.log(message);
},
warn: message => {
console.warn(message);
},
error: message => {
console.error(message);
}
};
};

View File

@ -3,7 +3,7 @@ import { assert } from "tsafe/assert";
import * as fs from "fs";
import { join as pathJoin } from "path";
export function readThisNpmPackageVersion(): string {
export function readThisNpmProjectVersion(): string {
const version = JSON.parse(fs.readFileSync(pathJoin(getThisCodebaseRootDirPath(), "package.json")).toString("utf8"))["version"];
assert(typeof version === "string");

1
src/index.ts Normal file
View File

@ -0,0 +1 @@
export { createKeycloakAdapter } from "keycloakify/lib/keycloakJsAdapter";

View File

@ -0,0 +1,118 @@
export declare namespace keycloak_js {
export type KeycloakPromiseCallback<T> = (result: T) => void;
export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> {
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>;
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>;
}
export interface KeycloakAdapter {
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>;
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
accountManagement(): KeycloakPromise<void, void>;
redirectUri(options: { redirectUri: string }, encodeHash: boolean): string;
}
export interface KeycloakLogoutOptions {
redirectUri?: string;
}
export interface KeycloakLoginOptions {
scope?: string;
redirectUri?: string;
prompt?: "none" | "login";
action?: string;
maxAge?: number;
loginHint?: string;
idpHint?: string;
locale?: string;
cordovaOptions?: { [optionName: string]: string };
}
export type KeycloakInstance = Record<
"createLoginUrl" | "createLogoutUrl" | "createRegisterUrl",
(options: KeycloakLoginOptions | undefined) => string
> & {
createAccountUrl(): string;
redirectUri?: string;
};
}
/**
* @deprecated: This will be removed in the next major version.
* If you use this, please copy paste the code into your project.
* Better yet migrate away from keycloak-js and use https://docs.oidc-spa.dev instead.
*
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js
* The goal here is just to be able to inject search param in url before keycloak redirect.
* Our use case for it is to pass over the login screen the states of useGlobalState
* namely isDarkModeEnabled, lgn...
*/
export function createKeycloakAdapter(params: {
keycloakInstance: keycloak_js.KeycloakInstance;
transformUrlBeforeRedirect: (url: string) => string;
getRedirectMethod?: () => "overwrite location.href" | "location.replace";
}): keycloak_js.KeycloakAdapter {
const { keycloakInstance, transformUrlBeforeRedirect, getRedirectMethod = () => "overwrite location.href" } = params;
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties(new Promise(() => {}), {
"success": { "value": () => {} },
"error": { "value": () => {} }
}) as any;
return {
"login": options => {
const newHref = transformUrlBeforeRedirect(keycloakInstance.createLoginUrl(options));
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(newHref);
break;
case "overwrite location.href":
window.location.href = newHref;
break;
}
return neverResolvingPromise;
},
"register": options => {
const newHref = transformUrlBeforeRedirect(keycloakInstance.createRegisterUrl(options));
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(newHref);
break;
case "overwrite location.href":
window.location.href = newHref;
break;
}
return neverResolvingPromise;
},
"logout": options => {
window.location.replace(transformUrlBeforeRedirect(keycloakInstance.createLogoutUrl(options)));
return neverResolvingPromise;
},
"accountManagement": () => {
const accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl());
if (accountUrl === "undefined") {
throw new Error("Not supported by the OIDC server");
}
switch (getRedirectMethod()) {
case "location.replace":
window.location.replace(accountUrl);
break;
case "overwrite location.href":
window.location.href = accountUrl;
break;
}
return neverResolvingPromise;
},
"redirectUri": options => {
if (options && options.redirectUri) {
return options.redirectUri;
} else if (keycloakInstance.redirectUri) {
return keycloakInstance.redirectUri;
} else {
return window.location.href;
}
}
};
}

View File

@ -201,15 +201,18 @@ function FieldErrors(props: {
<span
id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`}
className={getClassName("kcInputErrorMessageClass")}
style={{
"position": displayableErrors.length === 1 ? "absolute" : undefined
}}
aria-live="polite"
>
{displayableErrors
.filter(error => error.fieldIndex === fieldIndex)
.map(({ errorMessage }, i, arr) => (
<Fragment key={i}>
<>
<span key={i}>{errorMessage}</span>
{arr.length - 1 !== i && <br />}
</Fragment>
</>
))}
</span>
);

View File

@ -5,7 +5,7 @@ export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
export type { LoginThemePageId as PageId } from "keycloakify/bin/shared/constants";
export type { LoginThemePageId as PageId } from "keycloakify/bin/keycloakify/generateFtl";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -1,4 +1,5 @@
import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constants";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { type ThemeType } from "keycloakify/bin/constants";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";

View File

@ -1,6 +1,6 @@
import type { KcContext } from "./KcContext";
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
import { nameOfTheGlobal } from "keycloakify/bin/constants";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
? KcContext

View File

@ -1,8 +1,9 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { resources_common, keycloak_resources, type LoginThemePageId } from "keycloakify/bin/shared/constants";
import { resources_common, keycloak_resources } from "keycloakify/bin/constants";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [

View File

@ -354,15 +354,15 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
return initialState;
}, []);
const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, formAction: FormAction): internal.State {
const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === formAction.name);
const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, params: FormAction): internal.State {
const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === params.name);
assert(formFieldState !== undefined);
(() => {
switch (formAction.action) {
switch (params.action) {
case "update":
formFieldState.valueOrValues = formAction.valueOrValues;
formFieldState.valueOrValues = params.valueOrValues;
apply_formatters: {
const { attribute } = formFieldState;
@ -381,7 +381,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
formFieldState.errors = getErrors({
"attributeName": formAction.name,
"attributeName": params.name,
"formFieldStates": state.formFieldStates
});
@ -390,21 +390,21 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break update_password_confirm;
}
if (formAction.name !== "password") {
if (params.name !== "password") {
break update_password_confirm;
}
state = reducer(state, {
"action": "update",
"name": "password-confirm",
"valueOrValues": formAction.valueOrValues
"valueOrValues": params.valueOrValues
});
}
return;
case "focus lost":
if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
const { fieldIndex } = formAction;
const { fieldIndex } = params;
assert(fieldIndex !== undefined);
formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
return;
@ -413,10 +413,10 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
formFieldState.hasLostFocusAtLeastOnce = true;
return;
}
assert<Equals<typeof formAction, never>>(false);
assert<Equals<typeof params, never>>(false);
})();
return { ...state };
return state;
}, initialState);
const formState: FormState = useMemo(

View File

@ -8,8 +8,7 @@
"outDir": "../../dist/vite-plugin",
"rootDir": ".",
// https://github.com/vitejs/vite/issues/15112#issuecomment-1823908010
"skipLibCheck": true,
"sourceMap": false
"skipLibCheck": true
},
"references": [
{

View File

@ -1,11 +1,21 @@
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import type { Plugin } from "vite";
import { nameOfTheGlobal, basenameOfTheKeycloakifyResourcesDir, keycloak_resources, vitePluginSubScriptEnvNames } from "../bin/shared/constants";
import * as fs from "fs";
import {
resolvedViteConfigJsonBasename,
nameOfTheGlobal,
basenameOfTheKeycloakifyResourcesDir,
keycloak_resources,
keycloakifyBuildOptionsForPostPostBuildScriptEnvName
} from "../bin/constants";
import type { ResolvedViteConfig } from "../bin/keycloakify/buildOptions/resolvedViteConfig";
import { getCacheDirPath } from "../bin/keycloakify/buildOptions/getCacheDirPath";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
import { copyKeycloakResourcesToPublic } from "../bin/shared/copyKeycloakResourcesToPublic";
import { copyKeycloakResourcesToPublic } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
import { readBuildOptions, type BuildOptions, type UserProvidedBuildOptions, type ResolvedViteConfig } from "../bin/shared/buildOptions";
import type { BuildOptions } from "../bin/keycloakify/buildOptions";
import type { UserProvidedBuildOptions } from "../bin/keycloakify/buildOptions/UserProvidedBuildOptions";
import MagicString from "magic-string";
export type Params = UserProvidedBuildOptions & {
@ -26,16 +36,20 @@ export function keycloakify(params?: Params) {
"configResolved": async resolvedConfig => {
shouldGenerateSourcemap = resolvedConfig.build.sourcemap !== false;
run_post_build_script_case: {
const envValue = process.env[vitePluginSubScriptEnvNames.runPostBuildScript];
run_post_build_script: {
const buildOptionJson = process.env[keycloakifyBuildOptionsForPostPostBuildScriptEnvName];
if (envValue === undefined) {
break run_post_build_script_case;
if (buildOptionJson === undefined) {
break run_post_build_script;
}
const buildOptions = JSON.parse(envValue) as BuildOptions;
if (postBuild === undefined) {
process.exit(0);
}
await postBuild?.(buildOptions);
const buildOptions: BuildOptions = JSON.parse(buildOptionJson);
await postBuild(buildOptions);
process.exit(0);
}
@ -49,7 +63,7 @@ export function keycloakify(params?: Params) {
if (out.startsWith(".") && command === "build" && resolvedConfig.envPrefix?.includes("STORYBOOK_") !== true) {
throw new Error(
[
`BASE_URL=${out} is not supported By Keycloakify. Use an absolute path instead.`,
`BASE_URL=${out} is not supported By Keycloakify. Use an absolute URL instead.`,
`If this is a problem, please open an issue at https://github.com/keycloakify/keycloakify/issues/new`
].join("\n")
);
@ -72,16 +86,17 @@ export function keycloakify(params?: Params) {
buildDirPath = pathJoin(reactAppRootDirPath, resolvedConfig.build.outDir);
resolve_vite_config_case: {
const envValue = process.env[vitePluginSubScriptEnvNames.resolveViteConfig];
const { cacheDirPath } = getCacheDirPath({
reactAppRootDirPath
});
if (envValue === undefined) {
break resolve_vite_config_case;
}
if (!fs.existsSync(cacheDirPath)) {
fs.mkdirSync(cacheDirPath, { "recursive": true });
}
console.log(vitePluginSubScriptEnvNames.resolveViteConfig);
console.log(
fs.writeFileSync(
pathJoin(cacheDirPath, resolvedViteConfigJsonBasename),
Buffer.from(
JSON.stringify(
id<ResolvedViteConfig>({
"publicDir": pathRelative(reactAppRootDirPath, resolvedConfig.publicDir),
@ -89,19 +104,16 @@ export function keycloakify(params?: Params) {
"buildDir": resolvedConfig.build.outDir,
urlPathname,
userProvidedBuildOptions
})
)
);
process.exit(0);
}
}),
null,
2
),
"utf8"
)
);
await copyKeycloakResourcesToPublic({
"buildOptions": readBuildOptions({
"cliCommandOptions": {
reactAppRootDirPath
}
})
"processArgv": ["--project", reactAppRootDirPath]
});
},
"transform": (code, id) => {

View File

@ -1,19 +1,24 @@
import React from "react";
import { Meta, StoryObj } from "@storybook/react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "account.ftl";
const { PageStory } = createPageStory({ pageId });
const meta = {
title: "account/Account",
component: PageStory
} satisfies Meta<typeof PageStory>;
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => <PageStory />
};
export const Default = () => <PageStory />;

View File

@ -1,173 +0,0 @@
import React from "react";
import { Meta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const { PageStory } = createPageStory({
pageId: "totp.ftl"
});
const meta = {
title: "account/Authenticator",
component: PageStory
} satisfies Meta<typeof PageStory>;
export default meta;
export const Default = () => (
<PageStory
kcContext={{
totp: {
enabled: false,
totpSecretEncoded: "HE4W MSTC OBKU CY2M ONXF OV3Q NYYU I3SH",
totpSecret: "99fJbpUAcLsnWWpn1DnG",
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACk0lEQVR4Xu2YQY6DMAxFjViw5AjcBC6GBBIXozfJEbpkger53wEKqOpmFvaikQYNeVRyHPvbiejXscp95jp+/D5zHT9+n7kO8qeIFDqKzjJo9dC1wUSPP7yG4IPq41lq9ZK+keLZSwXDGwMhOCZgdX4sBVD1qld+GYg/h6ScreBuIDo5FKfVM7Z8aWs9PB2E2/73DdOlwUrK9Ck+HDnzB7ziR8fjlD/OPI8pVQwCi899TkNw2M+tp9XSLFKPIq2UySIhBB906fCQTicFwiv1EUG6+d+bl4zPIYnUk5oIcS69/evPYStUp6P0dJhD/mhauijcth76mOsfw+GFrbfXKJx7LW2N15kijuWIMCYicLQOCEimDp1c0L8PzCLTs3/d+ZQLyl6VqeSIT9nz25szf2ZybHgC31yrXEQIbqaPjX0k9GqWy0N/nLkagsHWNXR0LZwsR357c0pjC6fm+meu5f6f6oszz/qj7GpYCdHf0LVH/gTgtJ/5bVavPJ9svwnBS9qaqwoHOh3G7Ln++HIIDgpKYpFW00dlkX7ruz836THBWQpzd23/xeDsFVroz15fRjsfMyaC8JX2Y8PZf+VIoKff+uTO6WSIUIfSkrl9/rbfnbPr30R8hnMtXA/98ea5lx4ZlSMgQlMsEnb73XnP+yNl/SuR3/lzTSZHMTirMpMcXjWr0U5Mp/rnzmk/TsXkC2/iKEJ5TRG4DZ5KrP/C0RiVmkp+5I8zN1uh2vv9Vs+bzJ4947Y+bz6wl6ZIcv87ZaU2+6PwnoKdb7VYmrf9Z02MxCmNdmparbVJtrA4nA+e9LgIS6dzfvly7j+4XWIuPJp8iE9PbvkzJHYNabt/o5MP+535t/Hj95nr+PH7zHX8m/8B+RAnloz5pi4AAAAASUVORK5CYII=",
policy: {
type: "totp"
},
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
otpCredentials: []
},
messagesPerField: {},
stateChecker: "ihTeSAMfNsobnPjYiktV8DY-5T4sVzVdrEZRdwfMm8Y",
realm: {
userManagedAccessAllowed: true,
internationalizationEnabled: false
},
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
},
keycloakifyVersion: "9.6.1",
themeVersion: "1.0.10",
themeType: "account",
themeName: "keycloakify",
pageId: "totp.ftl"
}}
/>
);
export const WithTotpEnabled = () => (
<PageStory
kcContext={{
totp: {
enabled: true,
totpSecretEncoded: "G55E MZKC JFUD MQLT MFIF EVSB JFLG M6SO",
totpSecret: "7zFeBIh6AsaPRVAIVfzN",
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACo0lEQVR4Xu2YPY6DMBCFJ6JwyRF8k+RiSCBxsXATjkDpAmX2vTGBwErbbDFTZIps4s8rmfl9RvRPW+W6crYvv66c7cuvK2cjX0Tkho9yW/q5PHSc5QYA62PwXnWqmzrRSUdNL+mygRC8kzQZWhqVO1CRds3YHopnfUkzp2c7ZAY+GIdXywOb0qsdJMXiFn9serYrncxNv/PDkdfUzObk/eNaX368mnl1kML8RH1vFoGzargA1DM/VeWhOpf9+by5iL5Q0NaEUETslHiSIz+dOc4q0tqBrcg7IsnpnZ8BeLmjqjFa4Fps4vlR3484nFHH6OP8o1cTc4I/Q3D4Uqw1TjpkeHqc2R/Rjvb89OUUDAL/CpycOf/o6fUjP505/phrOf8wn+tolsxyD8GZnzyrJSScrNyEcXhHJwrBh2yj2fShPlFB2PQxn935aK1HIB1G1nczm8+P+nbmC7si+zell53a4i97fnhz5Gddxc9iSgLPpPifGn9vDqN0YBL0lpozdx7nd+dDHSiFXkV+NlZO85Efzvzda8yrwkylvlEbhxE4bTJpiCEIkWNHbxD/w/++fJMOVX8p5Q70F0V2EI4LsUWd+ov6Wtgu5aM/OXNIf6jWbKq6zmekA77t88WZr5lXO6vvWaj6kbNo4nv/ceaon0TpYPqrmNJhue/x9+ZKLchbO+cLPrb+aI09BLeob1en2nqkKsUYfOvatSGa/ircmD7i78rNmJoYzXwIKh228z3+ztzef+Cb6S/lSxoWOXM2CO/ZuvlqARtLvX8u1Ie6+d+bd/X9pdS3lrrF/8jPCPytv9AVIbfvddxE4iNFLKL+hH/xCNudKgTvGX/r33ars/y062gQjljfWN8cyKm+f2NPOvqTL//Lvvy6crYvv66c7d/8B/9RFjk6Tp30AAAAAElFTkSuQmCC",
policy: {
type: "totp",
algorithm: "HmacSHA1"
},
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
otpCredentials: [
{
id: "7afaaf7d-f2d5-44f5-a966-e5297f0b2b7a",
userLabel: "mobile"
}
]
},
message: {
summary: "Mobile authenticator configured.",
type: "success"
},
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
},
messagesPerField: {},
stateChecker: "0UvyCNJHRJXmdahtRmn0tTPCU2nwLtWBUfPaaX1qb4g",
realm: {
userManagedAccessAllowed: true,
internationalizationEnabled: false
},
keycloakifyVersion: "9.6.1",
themeVersion: "1.0.10",
themeType: "account",
themeName: "keycloakify",
pageId: "totp.ftl"
}}
/>
);
export const WithManualMode = () => (
<PageStory
kcContext={{
mode: "manual",
totp: {
enabled: false,
totpSecretEncoded: "KZ5H CYTW GBVV ASDE JRXG MMCK HAZU E6TX",
totpSecret: "Vzqbv0kPHdLnf0J83Bzw",
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACpklEQVR4Xu2YQa6rMAxFXTFgyBLYSbsxJJC6sb6dZAkdMkDNv8cBStHTm/yBM2ikIpqTgePY1w6W/xyLnWc+x5efZz7Hl59nPgf8aWaXPFl+2ZhbzfWaGPTT3yr4mPPPs8nty4ZeKxfzRQ6q4IO1P8zq0c/iffvqtIlLTfw5psxsK3f7JirjTHDqWpQ3T9fC/fytn2956u32bNJv8RHIyZ/n0MvJh8cpvwJ5GffkQaBNYPo2auCyv30YVmtitm4yu1qT5mtXCR9svsqXeih1/I1IbZHLKniTskxPOvCGSB3Wud2/0Vz+5YH9uHZAvzORUAlXaXmY9FHxyZuWI0L5sfs3lkt1vDTbtVtM8bmovrCT26o/0bxozVAWIY3IuTLpsvk3mDNeRv9QqrJWEp+25Xc01/uMVudHpySiE3PXklN1cLSm8yCgKmuWICUIxip4vqM6Y+kalNX3hJNtz+9orgOXQ60noZPrd/H5u74E86I/pfXXm/obXPvOr2juVW8o9nsTS77T5Ix18CZ71sh+qQ7n3+LzY32J5WptXt291Bdaf8tcVw76Hcvpqr31R3CUOri7Q79r4ap61+5O12XoT1leOrFK+HZ/asga/sr0tz5F85wozWq4aMKcP1DK3f54Ttfv+a0iqG1wCU2H/iGWl156IionQYWmngTpan84H9aGy+8nl7I8J5ejOnjP0SNCC/0/lVpydKyPwZz7u/Xef80ouaRHHt7PP5j74BJFfBpJ3vLp460/wdxtxX5KM6XPMvktJ6/7i+YjvfRS/Gs3za3218LJH5qwzKKf7fzd3fXwEWmkf5WTKS3JN1YRTxKhiY9IC6mzUKmP/g3knL8cqoeUiKvJL/EZyT1/sJ/vg+X7G07e7Q/mf40vP898ji8/z3yO/+b/ANUwOXCzdQgqAAAAAElFTkSuQmCC",
policy: {
type: "totp",
algorithm: "HmacSHA1"
},
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
otpCredentials: []
},
messagesPerField: {},
stateChecker: "HiBl2ADzLwKwQS813LOEig1Ymm4xpEu_NacYtWJIuHU",
realm: {
userManagedAccessAllowed: true,
internationalizationEnabled: false
},
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual"
},
keycloakifyVersion: "9.6.1",
themeVersion: "1.0.10",
themeType: "account",
themeName: "keycloakify",
pageId: "totp.ftl"
}}
/>
);
export const MoreThanOneTotpProviders = () => (
<PageStory
kcContext={{
totp: {
enabled: true,
totpSecretEncoded: "G55E MZKC JFUD MQLT MFIF EVSB JFLG M6SO",
totpSecret: "7zFeBIh6AsaPRVAIVfzN",
manualUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=manual",
supportedApplications: ["totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName"],
totpSecretQrCode:
"iVBORw0KGgoAAAANSUhEUgAAAPYAAAD2AQAAAADNaUdlAAACo0lEQVR4Xu2YPY6DMBCFJ6JwyRF8k+RiSCBxsXATjkDpAmX2vTGBwErbbDFTZIps4s8rmfl9RvRPW+W6crYvv66c7cuvK2cjX0Tkho9yW/q5PHSc5QYA62PwXnWqmzrRSUdNL+mygRC8kzQZWhqVO1CRds3YHopnfUkzp2c7ZAY+GIdXywOb0qsdJMXiFn9serYrncxNv/PDkdfUzObk/eNaX368mnl1kML8RH1vFoGzargA1DM/VeWhOpf9+by5iL5Q0NaEUETslHiSIz+dOc4q0tqBrcg7IsnpnZ8BeLmjqjFa4Fps4vlR3484nFHH6OP8o1cTc4I/Q3D4Uqw1TjpkeHqc2R/Rjvb89OUUDAL/CpycOf/o6fUjP505/phrOf8wn+tolsxyD8GZnzyrJSScrNyEcXhHJwrBh2yj2fShPlFB2PQxn935aK1HIB1G1nczm8+P+nbmC7si+zell53a4i97fnhz5Gddxc9iSgLPpPifGn9vDqN0YBL0lpozdx7nd+dDHSiFXkV+NlZO85Efzvzda8yrwkylvlEbhxE4bTJpiCEIkWNHbxD/w/++fJMOVX8p5Q70F0V2EI4LsUWd+ov6Wtgu5aM/OXNIf6jWbKq6zmekA77t88WZr5lXO6vvWaj6kbNo4nv/ceaon0TpYPqrmNJhue/x9+ZKLchbO+cLPrb+aI09BLeob1en2nqkKsUYfOvatSGa/ircmD7i78rNmJoYzXwIKh228z3+ztzef+Cb6S/lSxoWOXM2CO/ZuvlqARtLvX8u1Ie6+d+bd/X9pdS3lrrF/8jPCPytv9AVIbfvddxE4iNFLKL+hH/xCNudKgTvGX/r33ars/y062gQjljfWN8cyKm+f2NPOvqTL//Lvvy6crYvv66c7d/8B/9RFjk6Tp30AAAAAElFTkSuQmCC",
policy: {
type: "totp",
algorithm: "HmacSHA1"
},
qrUrl: "http://localhost:8080/realms/myrealm/account/totp?mode=qr",
otpCredentials: [
{
id: "7afaaf7d-f2d5-44f5-a966-e5297f0b2b7a",
userLabel: "Samsung S23"
},
{
id: "fbe22500-d979-45a3-9666-84c99e27958e",
userLabel: "Apple Iphone 15"
}
]
},
url: {
totpUrl: "http://localhost:8080/realms/myrealm/account/totp"
},
messagesPerField: {},
stateChecker: "0UvyCNJHRJXmdahtRmn0tTPCU2nwLtWBUfPaaX1qb4g",
realm: {
userManagedAccessAllowed: true,
internationalizationEnabled: false
},
keycloakifyVersion: "9.6.1",
themeVersion: "1.0.10",
themeType: "account",
themeName: "keycloakify",
pageId: "totp.ftl"
}}
/>
);

View File

@ -1,34 +0,0 @@
import React from "react";
import { Meta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "federatedIdentity.ftl";
const { PageStory } = createPageStory({ pageId });
const meta = {
title: "account/FederatedIdentity",
component: PageStory
} satisfies Meta<typeof PageStory>;
export default meta;
export const Default = () => <PageStory />;
export const NotConnected = () => (
<PageStory
kcContext={{
pageId: "federatedIdentity.ftl",
federatedIdentity: {
identities: [
{
providerId: "google",
displayName: "keycloak-oidc",
connected: false
}
],
removeLinkPossible: true
}
}}
/>
);

View File

@ -1,354 +0,0 @@
import React from "react";
import { Meta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "log.ftl";
const { PageStory } = createPageStory({
pageId
});
const meta = {
title: "account/Log",
component: PageStory
} satisfies Meta<typeof PageStory>;
export default meta;
export const Default = () => (
<PageStory
kcContext={{
log: {
events: [
{
date: "2024-04-26T12:29:08Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T12:10:56Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T11:57:34Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "update totp"
},
{
date: "2024-04-26T11:57:21Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "update totp"
},
{
date: "2024-04-26T11:56:56Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "remove totp"
},
{
date: "2024-04-26T11:56:55Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "remove totp"
},
{
date: "2024-04-26T11:56:41Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "update totp"
},
{
date: "2024-04-26T11:56:36Z",
ipAddress: "127.0.0.1",
client: "account",
details: [],
event: "update totp"
},
{
date: "2024-04-26T11:32:54Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:42:54Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:42:52Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:42:40Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:42:09Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "false",
key: "remember_me"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:24:17Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [],
event: "logout"
},
{
date: "2024-04-26T09:23:54Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:23:50Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:23:47Z",
ipAddress: "127.0.0.1",
client: "account",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:23:15Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [],
event: "logout"
},
{
date: "2024-04-26T09:23:06Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:22:53Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [],
event: "logout"
},
{
date: "2024-04-26T09:21:29Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "false",
key: "remember_me"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-26T09:17:32Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-18T11:19:09Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-18T11:18:50Z",
ipAddress: "127.0.0.1",
client: "keycloakify-frontend",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
},
{
date: "2024-04-18T11:18:24Z",
ipAddress: "127.0.0.1",
client: "account",
details: [
{
value: "openid-connect",
key: "auth_method"
},
{
value: "john.doe",
key: "username"
}
],
event: "login"
}
]
}
}}
/>
);

View File

@ -1,15 +1,23 @@
import React from "react";
import { Meta } from "@storybook/react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "password.ftl";
const { PageStory } = createPageStory({ pageId });
const meta = {
title: "account/Password",
component: PageStory
} satisfies Meta<typeof PageStory>;
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;

View File

@ -1,55 +1,26 @@
import React from "react";
import { Meta } from "@storybook/react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "sessions.ftl";
const { PageStory } = createPageStory({ pageId });
const meta = {
title: "account/Sessions",
component: PageStory
} satisfies Meta<typeof PageStory>;
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
export const Default = () => (
<PageStory
kcContext={{
sessions: {
sessions: [
{
expires: "2024-04-26T18:14:19Z",
clients: ["account"],
ipAddress: "172.20.0.1",
started: "2024-04-26T08:14:19Z",
lastAccess: "2024-04-26T08:30:54Z",
id: "af835e30-4821-43b1-b4f7-e732d3cc15d2"
},
{
expires: "2024-04-26T18:14:09Z",
clients: ["security-admin-console", "account"],
ipAddress: "172.20.0.1",
started: "2024-04-26T08:14:09Z",
lastAccess: "2024-04-26T08:15:14Z",
id: "60a9d8b8-617d-441e-8643-08c3fe30e231"
}
]
},
stateChecker: "xQ7EOgFrLi4EvnJ8dbXKhwFGWk_bkOp0X89mhilt1os"
}}
/>
);
export const Default = () => <PageStory />;
export const WithError = () => (
<PageStory
kcContext={{
url: { passwordUrl: "/auth/realms/keycloakify/account/password" },
stateChecker: "xQ7EOgFrLi4EvnJ8dbXKhwFGWk_bkOp0X89mhilt1os",
message: {
summary: "Invalid existing password.",
type: "error"
}
}}
/>
);
export const WithMessage = () => <PageStory kcContext={{}} />;

View File

@ -1,15 +1,23 @@
import React from "react";
import { Meta } from "@storybook/react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "totp.ftl";
const { PageStory } = createPageStory({ pageId });
const meta = {
title: "account/Authenticator",
component: PageStory
} satisfies Meta<typeof PageStory>;
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;

View File

@ -5,7 +5,7 @@ import { replaceImportsInInlineCssCode } from "keycloakify/bin/keycloakify/repla
import { same } from "evt/tools/inDepth/same";
import { expect, it, describe } from "vitest";
import { isSameCode } from "../tools/isSameCode";
import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal } from "keycloakify/bin/shared/constants";
import { basenameOfTheKeycloakifyResourcesDir, nameOfTheGlobal } from "keycloakify/bin/constants";
describe("js replacer - vite", () => {
it("replaceImportsInJsCode_vite - 1", () => {

View File

@ -1,4 +1,4 @@
import { downloadAndUnzip } from "keycloakify/bin/shared/downloadAndUnzip";
import { downloadAndUnzip } from "keycloakify/bin/downloadAndUnzip";
import { join as pathJoin } from "path";
import { getThisCodebaseRootDirPath } from "keycloakify/bin/tools/getThisCodebaseRootDirPath";

1719
yarn.lock

File diff suppressed because it is too large Load Diff