Compare commits

..

1 Commits

Author SHA1 Message Date
fff6e7c7fa Enabling shorter import paths [automatic] 2021-02-20 11:42:26 +00:00
199 changed files with 15740 additions and 14468 deletions
.eslintignore.eslintrc.js.gitattributes
.github
.gitignore.prettierignore.prettierrc.jsonCHANGELOG.mdCONTRIBUTING.mdREADME.mdbundle.jsbundle.js.mapbundle.min.jsindex.d.tsindex.jsindex.js.mapmyFunction.d.tsmyFunction.jsmyFunction.js.mapmyObject.d.tsmyObject.jsmyObject.js.mappackage-lock.jsonpackage.jsonrenovate.json
scripts
src
account
bin
index.ts
lib
login
myFunction.tsmyObject.ts
test
tools
tsconfig.json
test
tools
tsconfig.esm.jsontsconfig.jsontsconfig.tsbuildinfotsproject.jsonvitest.config.tsyarn.lock
zz_esm

4
.eslintignore Normal file

@ -0,0 +1,4 @@
/node_modules/
/dist/
/.eslintrc.js
/CHANGELOG.md

16
.eslintrc.js Normal file

@ -0,0 +1,16 @@
module.exports = {
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
],
"rules": {
"no-extra-boolean-cast": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
},
};

3
.gitattributes vendored

@ -1,3 +0,0 @@
src/lib/i18n/generated_kcMessages/* linguist-documentation
src/bin/keycloakify/index.ts -linguist-detectable
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable

@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

25
.github/release.yaml vendored

@ -1,25 +0,0 @@
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:
- '*'

@ -1,139 +0,0 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test_lint:
runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps:
- 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: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run format:check
test:
runs-on: ${{ matrix.os }}
needs: test_lint
strategy:
matrix:
node: [ '16' ]
os: [ ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- name: Tell if project is using npm or yarn
id: step1
uses: garronej/ts-ci@v2.0.2
with:
action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn build
yarn test
- if: steps.step1.outputs.npm_or_yarn == 'npm'
run: |
npm run build
npm test
check_if_version_upgraded:
name: Check if version upgrade
# We run this only if it's a push on the default branch or if it's a PR from a
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is
# defined but GitHub Action don't allow it yet.
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
runs-on: ubuntu-latest
needs: test
outputs:
from_version: ${{ steps.step1.outputs.from_version }}
to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
steps:
- uses: garronej/ts-ci@v2.0.2
id: step1
with:
action_name: is_package_json_version_upgraded
branch: ${{ github.head_ref || github.ref }}
create_github_release:
runs-on: ubuntu-latest
# We create a release only if the version have been upgraded and we are on the main branch
# or if we are on a branch of the repo that has an PR open on main.
if: |
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
(
github.event_name == 'push' ||
needs.check_if_version_upgraded.outputs.is_pre_release == 'true'
)
needs:
- check_if_version_upgraded
steps:
- 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 }}
target_commitish: ${{ github.head_ref || github.ref }}
generate_release_notes: true
draft: false
prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_on_npm:
runs-on: ubuntu-latest
needs:
- create_github_release
- check_if_version_upgraded
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v3
with:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run build
- run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
env:
DRY_RUN: "0"
- name: Publishing on NPM
run: |
if [ "$(npm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
if [ "$NODE_AUTH_TOKEN" = "" ]; then
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
false
fi
EXTRA_ARGS=""
if [ "$IS_PRE_RELEASE" = "true" ]; then
EXTRA_ARGS="--tag next"
fi
npm publish $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}

12
.gitignore vendored

@ -41,15 +41,3 @@ jspm_packages
.DS_Store
/dist
/keycloakify_starter_test/
/sample_custom_react_project/
/sample_react_project/
/.yarn_home/
.idea
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
# VS Code devcontainers
.devcontainer

@ -1,15 +1,5 @@
node_modules/
/node_modules/
/dist/
/.eslintrc.js
/docs/
/CHANGELOG.md
/.yarn_home/
/src/test/apps/
/src/tools/types/
/build_keycloak/
/.vscode/
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
# Test Build Directories
/dist_test
/sample_react_project/
/sample_custom_react_project/
/keycloakify_starter_test/

@ -1,11 +1,11 @@
{
"printWidth": 150,
"printWidth": 80,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"trailingComma": "none",
"trailingComma": "all",
"bracketSpacing": true,
"arrowParens": "avoid"
}

4
CHANGELOG.md Normal file

@ -0,0 +1,4 @@
### **0.0.2** (2021-02-20)
- Update package.json

@ -1,3 +0,0 @@
Looking to contribute? Thank you! PR are more than welcome.
Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.

227
README.md

@ -1,203 +1,66 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
<img src="https://user-images.githubusercontent.com/6702424/80216211-00ef5280-863e-11ea-81de-59f3a3d4b8e4.png">
</p>
<p align="center">
<i>🔏 Create Keycloak themes using React 🔏</i>
<i>Provides a way to customise Keycloak login and register pages with React</i>
<br>
<br>
<a href="https://github.com/garronej/keycloakify/actions">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
</a>
<a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/keycloakify">
</a>
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/keycloakify">
</a>
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
</a>
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
<p align="center">
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
-
<a href="https://storybook.keycloakify.dev/storybook">Storybook</a>
-
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
</p>
<img src="https://github.com/garronej/keycloak-react-theming/workflows/ci/badge.svg?branch=develop">
<img src="https://img.shields.io/bundlephobia/minzip/keycloak-react-theming">
<img src="https://img.shields.io/npm/dw/keycloak-react-theming">
<img src="https://img.shields.io/npm/l/keycloak-react-theming">
</p>
<p align="center">
<i>Ultimately this build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
<a href="https://github.com/garronej/keycloak-react-theming">Home</a>
-
<a href="https://github.com/garronej/keycloak-react-theming">Documentation</a>
</p>
The more ⭐️ the project gets, the more time I spend improving and maintaining it. Thank you for your support 😊
# Install / Import
> 🗣 V7 have been released 🎉
> [It features major improvements](https://github.com/keycloakify/keycloakify#70-).
> Checkout [the migration guide](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
```bash
$ npm install --save keycloak-react-theming
```
# Changelog highlights
```typescript
import { myFunction, myObject } from "keycloak-react-theming";
```
## 7.0 🍾
Specific imports:
- Account theme support 🚀
- It's much easier to customize pages at the CSS level, you can now see in the browser dev tool the customizable classes.
- New interactive CLI tool `npx eject-keycloak-page`, that enables to select the page you want to customize at the component level.
- There is [a Storybook](https://storybook.keycloakify.dev)
- [Remember me is fixed](https://github.com/keycloakify/keycloakify/pull/272)
```typescript
import { myFunction } from "keycloak-react-theming/myFunction";
import { myObject } from "keycloak-react-theming/myObject";
```
## 6.13
## Import from HTML, with CDN
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
Import it via a bundle that creates a global ( wider browser support ):
## 6.12
```html
<script src="//unpkg.com/keycloak-react-theming/bundle.min.js"></script>
<script>
const { myFunction, myObject } = keycloak_react_theming;
</script>
```
Massive improvement in the developer experience:
Or import it as an ES module:
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
- A lot of comments have been added in the code of the starter to make it easier to get started.
- The doc has been updated: https://docs.keycloakify.dev
- A lot of improvements in the type system.
```html
<script type="module">
import {
myFunction,
myObject,
} from "//unpkg.com/keycloak-react-theming/zz_esm/index.js";
</script>
```
## 6.11.4
_You can specify the version you wish to import:_ [unpkg.com](https://unpkg.com)
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/239).
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
## Contribute
## 6.10.0
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
## 6.8.4
- `@emotion/react` is no longer a peer dependency of Keycloakify.
## 6.8.0
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
individual page (`<Login />`, `<RegisterUserProfile />`, ...) it enables to customize only the header and footer for
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/keycloakify/issues/191).
## 6.7.0
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/185).
## 6.6.0
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/184).
## 6.5.0
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/183).
## 6.4.0
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded.
- First print much quicker, use of React.lazy() everywhere.
- Real i18n API.
- Actual documentation for build options.
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
## 5.7.0
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
## 5.6.4
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
## v5.6.0
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
## v5.3.0
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
## v5.0.0
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
New i18n system.
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
## v4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
## v4.8.0
[Email template customization.](#email-template-customization)
## v4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## v4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
Register with user profile enabled: Out of the box `options` validator support.
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
## v4.6.0
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
## v4.5.3
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
with this new method your theme wont break on minor Keycloakify update.
## v4.3.0
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
Every time a page is added it's a breaking change for non CSS-only theme.
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## v3
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
## v2.5
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
```bash
npm install
npm run build
npm test
```

37
bundle.js Normal file

@ -0,0 +1,37 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.keycloak_react_theming = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myObject = exports.myFunction = void 0;
var myFunction_1 = require("./myFunction");
Object.defineProperty(exports, "myFunction", { enumerable: true, get: function () { return myFunction_1.myFunction; } });
var myObject_1 = require("./myObject");
Object.defineProperty(exports, "myObject", { enumerable: true, get: function () { return myObject_1.myObject; } });
},{"./myFunction":2,"./myObject":3}],2:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myFunction = void 0;
function myFunction() {
return Promise.resolve(["a", "b", "c"]);
}
exports.myFunction = myFunction;
},{}],3:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myObject = void 0;
var toUpperCase_1 = require("./tools/toUpperCase");
exports.myObject = { "p": toUpperCase_1.toUpperCase("foo") };
},{"./tools/toUpperCase":4}],4:[function(require,module,exports){
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.toUpperCase = void 0;
function toUpperCase(str) {
return str.toUpperCase();
}
exports.toUpperCase = toUpperCase;
},{}]},{},[1])(1)
});
//# sourceMappingURL=bundle.js.map

1
bundle.js.map Normal file

@ -0,0 +1 @@
{"version":3,"sources":["node_modules/browser-pack/_prelude.js","index.js","myFunction.js","myObject.js","tools/toUpperCase.js"],"names":[],"mappings":"AAAA;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACPA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;ACPA;AACA;AACA;AACA;AACA;AACA;;ACLA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"generated.js","sourceRoot":"","sourcesContent":["(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c=\"function\"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error(\"Cannot find module '\"+i+\"'\");throw a.code=\"MODULE_NOT_FOUND\",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u=\"function\"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.myObject = exports.myFunction = void 0;\nvar myFunction_1 = require(\"./myFunction\");\nObject.defineProperty(exports, \"myFunction\", { enumerable: true, get: function () { return myFunction_1.myFunction; } });\nvar myObject_1 = require(\"./myObject\");\nObject.defineProperty(exports, \"myObject\", { enumerable: true, get: function () { return myObject_1.myObject; } });\n//# sourceMappingURL=index.js.map","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.myFunction = void 0;\nfunction myFunction() {\n return Promise.resolve([\"a\", \"b\", \"c\"]);\n}\nexports.myFunction = myFunction;\n//# sourceMappingURL=myFunction.js.map","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.myObject = void 0;\nvar toUpperCase_1 = require(\"./tools/toUpperCase\");\nexports.myObject = { \"p\": toUpperCase_1.toUpperCase(\"foo\") };\n//# sourceMappingURL=myObject.js.map","\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.toUpperCase = void 0;\nfunction toUpperCase(str) {\n return str.toUpperCase();\n}\nexports.toUpperCase = toUpperCase;\n//# sourceMappingURL=toUpperCase.js.map"]}

1
bundle.min.js vendored Normal file

@ -0,0 +1 @@
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).keycloak_react_theming=e()}}((function(){return function e(t,n,o){function r(u,f){if(!n[u]){if(!t[u]){var c="function"==typeof require&&require;if(!f&&c)return c(u,!0);if(i)return i(u,!0);var d=new Error("Cannot find module '"+u+"'");throw d.code="MODULE_NOT_FOUND",d}var p=n[u]={exports:{}};t[u][0].call(p.exports,(function(e){return r(t[u][1][e]||e)}),p,p.exports,e,t,n,o)}return n[u].exports}for(var i="function"==typeof require&&require,u=0;u<o.length;u++)r(o[u]);return r}({1:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.myObject=n.myFunction=void 0;var o=e("./myFunction");Object.defineProperty(n,"myFunction",{enumerable:!0,get:function(){return o.myFunction}});var r=e("./myObject");Object.defineProperty(n,"myObject",{enumerable:!0,get:function(){return r.myObject}})},{"./myFunction":2,"./myObject":3}],2:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.myFunction=void 0,n.myFunction=function(){return Promise.resolve(["a","b","c"])}},{}],3:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.myObject=void 0;var o=e("./tools/toUpperCase");n.myObject={p:o.toUpperCase("foo")}},{"./tools/toUpperCase":4}],4:[function(e,t,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.toUpperCase=void 0,n.toUpperCase=function(e){return e.toUpperCase()}},{}]},{},[1])(1)}));

2
index.d.ts vendored Normal file

@ -0,0 +1,2 @@
export { myFunction } from "./myFunction";
export { myObject } from "./myObject";

8
index.js Normal file

@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myObject = exports.myFunction = void 0;
var myFunction_1 = require("./myFunction");
Object.defineProperty(exports, "myFunction", { enumerable: true, get: function () { return myFunction_1.myFunction; } });
var myObject_1 = require("./myObject");
Object.defineProperty(exports, "myObject", { enumerable: true, get: function () { return myObject_1.myObject; } });
//# sourceMappingURL=index.js.map

1
index.js.map Normal file

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":";;;AAAA,2CAA0C;AAAjC,wGAAA,UAAU,OAAA;AACnB,uCAAsC;AAA7B,oGAAA,QAAQ,OAAA"}

1
myFunction.d.ts vendored Normal file

@ -0,0 +1 @@
export declare function myFunction(): Promise<string[]>;

8
myFunction.js Normal file

@ -0,0 +1,8 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myFunction = void 0;
function myFunction() {
return Promise.resolve(["a", "b", "c"]);
}
exports.myFunction = myFunction;
//# sourceMappingURL=myFunction.js.map

1
myFunction.js.map Normal file

@ -0,0 +1 @@
{"version":3,"file":"myFunction.js","sourceRoot":"","sources":["src/myFunction.ts"],"names":[],"mappings":";;;AAAA,SAAgB,UAAU;IACtB,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;AAC5C,CAAC;AAFD,gCAEC"}

3
myObject.d.ts vendored Normal file

@ -0,0 +1,3 @@
export declare const myObject: {
p: string;
};

6
myObject.js Normal file

@ -0,0 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.myObject = void 0;
var toUpperCase_1 = require("./tools/toUpperCase");
exports.myObject = { "p": toUpperCase_1.toUpperCase("foo") };
//# sourceMappingURL=myObject.js.map

1
myObject.js.map Normal file

@ -0,0 +1 @@
{"version":3,"file":"myObject.js","sourceRoot":"","sources":["src/myObject.ts"],"names":[],"mappings":";;;AAAA,mDAAkD;AAErC,QAAA,QAAQ,GAAG,EAAE,GAAG,EAAE,yBAAW,CAAC,KAAK,CAAC,EAAE,CAAC"}

13324
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

141
package.json Normal file → Executable file

@ -1,37 +1,17 @@
{
"name": "keycloakify",
"version": "7.6.1",
"description": "Create Keycloak themes using React",
"name": "keycloak-react-theming",
"version": "0.0.2",
"description": "Provides a way to customise Keycloak login and register pages with React",
"repository": {
"type": "git",
"url": "git://github.com/keycloakify/keycloakify.git"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/tsconfig.json && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"build:watch": "tsc -p src/tsconfig.json && (concurrently \"tsc -p src/tsconfig.json -w\" \"tsc-alias -p src/tsconfig.json\")",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
"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": "yarn link-in-app keycloakify-starter",
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w "
},
"bin": {
"keycloakify": "dist/bin/keycloakify/index.js",
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js"
"url": "git://github.com/garronej/keycloak-react-theming.git"
},
"main": "index.js",
"types": "index.d.ts",
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix"
],
"*.{ts,tsx,json,md}": [
"prettier --write"
]
@ -44,59 +24,58 @@
"author": "u/garronej",
"license": "MIT",
"files": [
"src/",
"dist/",
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/tsconfig.tsbuildinfo"
"src/index.ts",
"src/myFunction.ts",
"src/myObject.ts",
"src/tools/getProjectRoot.ts",
"src/tools/toUpperCase.ts",
"bundle.js",
"bundle.js.map",
"bundle.min.js",
"index.d.ts",
"index.js",
"index.js.map",
"myFunction.d.ts",
"myFunction.js",
"myFunction.js.map",
"myObject.d.ts",
"myObject.js",
"myObject.js.map",
"tools/getProjectRoot.d.ts",
"tools/getProjectRoot.js",
"tools/getProjectRoot.js.map",
"tools/toUpperCase.d.ts",
"tools/toUpperCase.js",
"tools/toUpperCase.js.map",
"zz_esm/index.d.ts",
"zz_esm/index.js",
"zz_esm/index.js.map",
"zz_esm/myFunction.d.ts",
"zz_esm/myFunction.js",
"zz_esm/myFunction.js.map",
"zz_esm/myObject.d.ts",
"zz_esm/myObject.js",
"zz_esm/myObject.js.map",
"zz_esm/tools/toUpperCase.d.ts",
"zz_esm/tools/toUpperCase.js",
"zz_esm/tools/toUpperCase.js.map",
"zz_esm/tsconfig.esm.tsbuildinfo"
],
"keywords": [
"bluehats",
"keycloak",
"react",
"theme",
"FreeMarker",
"ftl",
"login",
"register"
],
"homepage": "https://www.keycloakify.dev",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"keywords": [],
"homepage": "https://github.com/garronej/keycloak-react-theming",
"devDependencies": {
"@babel/core": "^7.0.0",
"@types/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^18.15.3",
"@types/react": "18.0.9",
"@types/yauzl": "^2.10.0",
"concurrently": "^7.6.0",
"copyfiles": "^2.4.1",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "18.1.0",
"rimraf": "^3.0.2",
"scripting-tools": "^0.19.13",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.3",
"typescript": "^5.0.1-rc",
"vitest": "^0.29.8"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.18",
"make-fetch-happen": "^11.0.3",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"rfc4648": "^1.5.2",
"tsafe": "^1.6.0",
"yauzl": "^2.10.0",
"zod": "^3.17.10"
"@types/node": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"denoify": "^0.6.4",
"eslint": "^7.20.0",
"eslint-config-prettier": "^7.2.0",
"evt": "^1.8.11",
"husky": "^4.3.0",
"lint-staged": "^10.5.4",
"prettier": "^2.2.1",
"simplifyify": "^8.0.3",
"terser": "^5.3.8",
"typescript": "^4.1.5"
}
}

@ -1,27 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["main", "landingpage"],
"extends": ["config:base"],
"dependencyDashboard": false,
"bumpVersion": "patch",
"rangeStrategy": "bump",
"ignorePaths": [".github/**"],
"branchPrefix": "renovate_",
"vulnerabilityAlerts": {
"enabled": false
},
"packageRules": [
{
"packagePatterns": ["*"],
"excludePackagePatterns": ["tsafe", "evt"],
"enabled": false
},
{
"packagePatterns": ["tsafe", "evt"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "branch",
"groupName": "garronej_modules_update"
}
]
}

@ -1,122 +0,0 @@
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 { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getCliOptions } from "../src/bin/tools/cliOptions";
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.
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
async function main() {
const keycloakVersion = "21.0.1";
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
crawl(baseThemeDirPath).forEach(filePath => {
const match = filePath.match(re);
if (match === null) {
return;
}
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
});
}
fs.rmSync(tmpDirPath, { recursive: true, force: true });
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
if (themeType !== "login" && themeType !== "account") {
return;
}
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
""
].join("\n");
languages.forEach(language => {
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.writeFileSync(
filePath,
Buffer.from(
[
generatedFileHeader,
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
].join("\n"),
"utf8"
)
);
logger.log(`${filePath} wrote`);
});
fs.writeFileSync(
pathJoin(baseMessagesDirPath, "index.ts"),
Buffer.from(
[
generatedFileHeader,
"export async function getMessages(currentLanguageTag: string) {",
" const { default: messages } = await (() => {",
" switch (currentLanguageTag) {",
...languages.map(language => ` case "${language}": return import("./${language}");`),
' default: return { "default": {} };',
" }",
" })();",
" return messages;",
"}"
].join("\n"),
"utf8"
)
);
});
}
if (require.main === module) {
main().catch(e => console.error(e));
}

@ -1,143 +0,0 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import * as fs from "fs";
const singletonDependencies: string[] = ["react", "@types/react"];
const rootDirPath = getProjectRoot();
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
fs.writeFileSync(
pathJoin(rootDirPath, "dist", "package.json"),
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
return {
...packageJsonParsed,
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
"exports": !("exports" in packageJsonParsed)
? undefined
: Object.fromEntries(
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
key,
(value as string).replace(/^\.\/dist\//, "./")
])
)
};
})(),
null,
2
),
"utf8"
)
);
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
const commonThirdPartyDeps = (() => {
// For example [ "@emotion" ] it's more convenient than
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
// in singletonDependencies
const namespaceSingletonDependencies: string[] = [];
return [
...namespaceSingletonDependencies
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...singletonDependencies
];
})();
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
fs.mkdirSync(yarnGlobalDirPath);
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
...process.env,
"HOME": yarnGlobalDirPath
}
});
};
const testAppPaths = (() => {
const [, , ...testAppNames] = process.argv;
return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(rootDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter((path): path is string => path !== undefined);
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ===");
const total = commonThirdPartyDeps.length;
let current = 0;
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
current++;
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
);
execYarnLink({ "cwd": localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
})
);
export {};

@ -1,29 +0,0 @@
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) });

@ -1,26 +0,0 @@
import { lazy, Suspense } from "react";
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";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "password.ftl":
return <Password kcContext={kcContext} {...rest} />;
case "account.ftl":
return <Account kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}
</Suspense>
);
}

@ -1,131 +0,0 @@
import { clsx } from "keycloakify/tools/clsx";
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { type TemplateProps } from "keycloakify/account/TemplateProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
import { assert } from "keycloakify/tools/assert";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext;
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
url,
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
"styles": ["css/account.css"],
"htmlClassName": undefined,
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
});
if (!isReady) {
return null;
}
return (
<>
<header className="navbar navbar-default navbar-pf navbar-main header">
<nav className="navbar" role="navigation">
<div className="navbar-header">
<div className="container">
<h1 className="navbar-title">Keycloak</h1>
</div>
</div>
<div className="navbar-collapse navbar-collapse-1">
<div className="container">
<ul className="nav navbar-nav navbar-utility">
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<li>
<div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
</li>
))}
</ul>
</div>
</li>
)}
{referrer?.url !== undefined && (
<li>
<a href={referrer.url} id="referrer">
{msg("backTo", referrer.name)}
</a>
</li>
)}
<li>
<a href={url.getLogoutUrl()}>{msg("doSignOut")}</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div className="container">
<div className="bs-sidebar col-sm-3">
<ul>
<li className={clsx(active === "account" && "active")}>
<a href={url.accountUrl}>{msg("account")}</a>
</li>
{features.passwordUpdateSupported && (
<li className={clsx(active === "password" && "active")}>
<a href={url.passwordUrl}>{msg("password")}</a>
</li>
)}
<li className={clsx(active === "totp" && "active")}>
<a href={url.totpUrl}>{msg("authenticator")}</a>
</li>
{features.identityFederation && (
<li className={clsx(active === "social" && "active")}>
<a href={url.socialUrl}>{msg("federatedIdentity")}</a>
</li>
)}
<li className={clsx(active === "sessions" && "active")}>
<a href={url.sessionsUrl}>{msg("sessions")}</a>
</li>
<li className={clsx(active === "applications" && "active")}>
<a href={url.applicationsUrl}>{msg("applications")}</a>
</li>
{features.log && (
<li className={clsx(active === "log" && "active")}>
<a href={url.logUrl}>{msg("log")}</a>
</li>
)}
{realm.userManagedAccessAllowed && features.authorization && (
<li className={clsx(active === "authorization" && "active")}>
<a href={url.resourceUrl}>{msg("myResources")}</a>
</li>
)}
</ul>
</div>
<div className="col-sm-9 content-area">
{message !== undefined && (
<div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className="pficon pficon-ok"></span>}
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
<span className="kc-feedback-text">{message.summary}</span>
</div>
)}
{children}
</div>
</div>
</>
);
}

@ -1,14 +0,0 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
active: string;
classes?: Partial<Record<ClassKey, string>>;
children: ReactNode;
};
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";

@ -1,229 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import { useEffect, useState, useRef } from "react";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContext } from "../kcContext/KcContext";
import { Markdown } from "keycloakify/tools/Markdown";
export const fallbackLanguageTag = "en";
export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
};
};
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type GenericI18n<MessageKey extends string> = {
/**
* e.g: "en", "fr", "zh-CN"
*
* The current language
*/
currentLanguageTag: string;
/**
* To call when the user switch language.
* This will cause the page to be reloaded,
* on next load currentLanguageTag === newLanguageTag
*/
changeLocale: (newLanguageTag: string) => never;
/**
* e.g. "en" => "English", "fr" => "Français", ...
*
* Used to render a select that enable user to switch language.
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
* */
labelBySupportedLanguageTag: Record<string, string>;
/**
* Examples assuming currentLanguageTag === "en"
*
* msg("access-denied") === <span>Access denied</span>
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
*/
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
/**
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
*/
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
*/
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*/
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
};
export type I18n = GenericI18n<MessageKey>;
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
const { kcContext } = params;
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
const refHasStartedFetching = useRef(false);
useEffect(() => {
if (refHasStartedFetching.current) {
return;
}
refHasStartedFetching.current = true;
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
assert(false, "never");
},
"labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
})();
}, []);
return i18n ?? null;
}
return { useI18n };
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
messages: Record<MessageKey, string>;
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
const { fallbackMessages, messages } = params;
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderMarkdown } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
if (messageOrUndefined === undefined) {
return undefined;
}
const message = messageOrUndefined;
const messageWithArgsInjectedIfAny = (() => {
const startIndex = message
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
// No {0} in message (no arguments expected)
return message;
}
let messageWithArgsInjected = message;
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return messageWithArgsInjected;
})();
return doRenderMarkdown ? (
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
{messageWithArgsInjectedIfAny}
</Markdown>
) : (
messageWithArgsInjectedIfAny
);
}
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
const { key, args, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
doRenderMarkdown
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
return {
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
};
}
const keycloakifyExtraMessages = {
"en": {
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option"
},
"fr": {
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être égal à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entier",
"notAValidOption": "N'est pas une option valide",
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter"
/* spell-checker: enable */
}
};

@ -1 +0,0 @@
export type { I18n } from "./i18n";

@ -1,8 +0,0 @@
import Fallback from "keycloakify/account/Fallback";
export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

@ -1,84 +0,0 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
url: {
accountUrl: string;
passwordUrl: string;
totpUrl: string;
socialUrl: string;
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
getLogoutUrl: () => string;
};
features: {
passwordUpdateSupported: boolean;
identityFederation: boolean;
log: boolean;
authorization: boolean;
};
realm: {
internationalizationEnabled: boolean;
userManagedAccessAllowed: boolean;
};
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
referrer?: {
url?: string;
name: string;
};
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
existsError: (fieldName: string) => boolean;
get: (fieldName: string) => string;
exists: (fieldName: string) => boolean;
};
account: {
email?: string;
firstName: string;
lastName?: string;
username?: string;
};
};
export type Password = Common & {
pageId: "password.ftl";
password: {
passwordSet: boolean;
};
stateChecker: string;
};
export type Account = Common & {
pageId: "account.ftl";
url: {
referrerURI: string;
accountUrl: string;
};
realm: {
registrationEmailAsUsername: boolean;
editUsernameAllowed: boolean;
};
stateChecker: string;
};
}
assert<Equals<KcContext["pageId"], AccountThemePageId>>();

@ -1,76 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(
[
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
`If assets are missing make sure you have built your Keycloak theme at least once.`
].join(" "),
"background: red; color: yellow; font-size: medium"
);
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined };
}
if (!("account" in realKcContext)) {
return { "kcContext": undefined };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
}
return { "kcContext": realKcContext };
}

@ -1,11 +0,0 @@
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
}

@ -1 +0,0 @@
export type { KcContext } from "./KcContext";

@ -1,175 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContext.Common = {
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",
"getLogoutUrl": () => "#",
"logUrl": "#",
"passwordUrl": "#",
"sessionsUrl": "#",
"socialUrl": "#",
"totpUrl": "#"
},
"realm": {
"internationalizationEnabled": true,
"userManagedAccessAllowed": true
},
"messagesPerField": {
"printIfExists": () => {
return undefined;
},
"existsError": () => false,
"get": key => `Fake error for ${key}`,
"exists": () => false
},
"locale": {
"supported": [
/* spell-checker: disable */
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr"
}
/* spell-checker: enable */
],
"currentLanguageTag": "en"
},
"message": {
"type": "success",
"summary": "This is a test message"
},
"features": {
"authorization": true,
"identityFederation": true,
"log": true,
"passwordUpdateSupported": true
},
"referrer": undefined,
"account": {
"firstName": "john",
"lastName": "doe",
"email": "john.doe@code.gouv.fr",
"username": "doe_j"
}
};
export const kcContextMocks: KcContext[] = [
id<KcContext.Password>({
...kcContextCommonMock,
"pageId": "password.ftl",
"password": {
"passwordSet": true
},
"stateChecker": "state checker"
}),
id<KcContext.Account>({
...kcContextCommonMock,
"pageId": "account.ftl",
"url": {
...kcContextCommonMock.url,
"referrerURI": "#",
"accountUrl": "#"
},
"realm": {
...kcContextCommonMock.realm,
"registrationEmailAsUsername": true,
"editUsernameAllowed": true
},
"stateChecker": ""
})
];

@ -1,12 +0,0 @@
import { createUseClassName } from "keycloakify/lib/useGetClassName";
import type { ClassKey } from "keycloakify/account/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
"defaultClasses": {
"kcBodyClass": undefined,
"kcButtonClass": "btn",
"kcButtonPrimaryClass": "btn-primary",
"kcButtonLargeClass": "btn-lg",
"kcButtonDefaultClass": "btn-default"
}
});

@ -1,134 +0,0 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
}
});
const { url, realm, messagesPerField, stateChecker, account } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="account">
<div className="row">
<div className="col-md-10">
<h2>{msg("editAccountHtmlTitle")}</h2>
</div>
<div className="col-md-2 subtitle">
<span className="subtitle">
<span className="required">*</span> {msg("requiredFields")}
</span>
</div>
</div>
<form action={url.accountUrl} className="form-horizontal" method="post">
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
{!realm.registrationEmailAsUsername && (
<div className={clsx("form-group", messagesPerField.printIfExists("username", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="username" className="control-label">
{msg("username")}
</label>
{realm.editUsernameAllowed && <span className="required">*</span>}
</div>
<div className="col-sm-10 col-md-10">
<input
type="text"
className="form-control"
id="username"
name="username"
disabled={!realm.editUsernameAllowed}
value={account.username ?? ""}
/>
</div>
</div>
)}
<div className={clsx("form-group", messagesPerField.printIfExists("email", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="email" className="control-label">
{msg("email")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
</div>
</div>
<div className={clsx("form-group", messagesPerField.printIfExists("firstName", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="firstName" className="control-label">
{msg("firstName")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
</div>
</div>
<div className={clsx("form-group", messagesPerField.printIfExists("lastName", "has-error"))}>
<div className="col-sm-2 col-md-2">
<label htmlFor="lastName" className="control-label">
{msg("lastName")}
</label>{" "}
<span className="required">*</span>
</div>
<div className="col-sm-10 col-md-10">
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
</div>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
{url.referrerURI !== undefined && <a href={url.referrerURI}>${msg("backToApplication")}</a>}
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Save"
>
{msg("doSave")}
</button>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonDefaultClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Cancel"
>
{msg("doCancel")}
</button>
I
</div>
</div>
</div>
</form>
</Template>
);
}

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

@ -1,105 +0,0 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
"classes": {
...classes,
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
}
});
const { url, password, account, stateChecker } = kcContext;
const { msg } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
<div className="row">
<div className="col-md-10">
<h2>{msg("changePasswordHtmlTitle")}</h2>
</div>
<div className="col-md-2 subtitle">
<span className="subtitle">${msg("allFieldsRequired")}</span>
</div>
</div>
<form action={url.passwordUrl} className="form-horizontal" method="post">
<input
type="text"
id="username"
name="username"
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none;" }}
/>
{password.passwordSet && (
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password" className="control-label">
{msg("password")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
</div>
</div>
)}
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password-new" className="control-label">
{msg("passwordNew")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
</div>
</div>
<div className="form-group">
<div className="col-sm-2 col-md-2">
<label htmlFor="password-confirm" className="control-label two-lines">
{msg("passwordConfirm")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
</div>
</div>
<div className="form-group">
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
<button
type="submit"
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonLargeClass")
)}
name="submitAction"
value="Save"
>
{msg("doSave")}
</button>
</div>
</div>
</div>
</form>
</Template>
);
}

@ -1,41 +0,0 @@
#!/usr/bin/env node
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
import { getKeycloakBuildPath } from "./keycloakify/build-paths";
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath } = params;
await Promise.all(
["", "-community"].map(ext =>
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
})
)
);
}
async function main() {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(getKeycloakBuildPath(), "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
isSilent
});
}
if (require.main === module) {
main();
}

@ -1,71 +0,0 @@
#!/usr/bin/env node
import { getProjectRoot } from "./tools/getProjectRoot";
import cliSelect from "cli-select";
import {
loginThemePageIds,
accountThemePageIds,
type LoginThemePageId,
type AccountThemePageId,
themeTypes,
type ThemeType
} from "./keycloakify/generateFtl/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 "./keycloakify/build-paths";
(async () => {
const projectRootDir = getProjectRoot();
console.log("Select a theme type");
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();
if (themeSrcDirPath === undefined) {
throw new Error("Couldn't locate your theme sources");
}
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);
}
writeFile(targetFilePath, await readFile(pathJoin(projectRootDir, "src", themeType, "pages", pageBasename)));
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
})();

@ -1,58 +0,0 @@
#!/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 "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
import { getEmailThemeSrcDirPath } from "./keycloakify/build-paths";
export async function main() {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
if (emailThemeSrcDirPath === undefined) {
logger.warn("Couldn't locate your theme source directory");
process.exit(-1);
}
if (fs.existsSync(emailThemeSrcDirPath)) {
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
const { keycloakVersion } = await promptKeycloakVersion();
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": emailThemeSrcDirPath
});
{
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
}
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
}
if (require.main === module) {
main();
}

@ -1,183 +0,0 @@
import { assert } from "tsafe/assert";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
import { typeGuard } from "tsafe/typeGuard";
import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import { getAppInputPath, getKeycloakBuildPath } from "./build-paths";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeName: string;
extraLoginPages: string[] | undefined;
extraAccountPages: string[] | undefined;
extraThemeProperties?: string[];
groupId: string;
artifactId: string;
bundler: Bundler;
keycloakVersionDefaultAssets: string;
// Directory of your built react project. Defaults to {cwd}/build
appInputPath: string;
// Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak
keycloakBuildPath: string;
customUserAttributes: string[];
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export function readBuildOptions(params: { CNAME: string | undefined; isExternalAssetsCliParamProvided: boolean; isSilent: boolean }): BuildOptions {
const { CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = getParsedPackageJson();
const url = (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (CNAME !== undefined) {
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
}
if (url === undefined) {
return undefined;
}
return {
"origin": url.origin,
"pathname": (() => {
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
})();
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } =
keycloakify ?? {};
const themeName =
keycloakify.themeName ??
name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
"bundler": (() => {
const { KEYCLOAKIFY_BUNDLER } = process.env;
assert(
typeGuard<Bundler | undefined>(
KEYCLOAKIFY_BUNDLER,
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
),
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
);
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
})(),
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
process.env.KEYCLOAKIFY_GROUP_ID ??
groupId ??
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"version": process.env.KEYCLOAKIFY_VERSION ?? version,
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
isSilent,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
appInputPath: getAppInputPath(),
keycloakBuildPath: getKeycloakBuildPath(),
"customUserAttributes": keycloakify.customUserAttributes ?? []
};
})();
if (isExternalAssetsCliParamProvided) {
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
...common,
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
url !== undefined,
[
"Can't compile in external assets mode if we don't know where",
"the app will be hosted.",
"You should provide a homepage field in the package.json (or create a",
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
}
}
return id<BuildOptions.Standalone>({
...common,
"isStandalone": true,
"urlPathname": url?.pathname
});
}

@ -1,72 +0,0 @@
import * as fs from "fs";
import { exclude } from "tsafe";
import { crawl } from "../tools/crawl";
import { pathJoin } from "../tools/pathJoin";
import { getParsedPackageJson } from "./parsedPackageJson";
const DEFAULT_APP_INPUT_PATH = "build";
const DEFAULT_KEYCLOAK_BUILD_PATH = "build_keycloak";
const THEME_SRC_DIR_BASENAME = "keycloak-theme";
export const getReactProjectDirPath = () => process.cwd();
export const getCnamePath = () => pathJoin(getReactProjectDirPath(), "public", "CNAME");
const parseAppInputPath = (path?: string) => {
if (!path) {
return pathJoin(process.cwd(), DEFAULT_APP_INPUT_PATH);
} else if (path.startsWith("./")) {
return pathJoin(process.cwd(), path.replace("./", ""));
}
return path;
};
const parseKeycloakBuildPath = (path?: string) => {
if (!path) {
return pathJoin(process.cwd(), DEFAULT_KEYCLOAK_BUILD_PATH);
} else if (path.startsWith("./")) {
return pathJoin(process.cwd(), path.replace("./", ""));
}
return path;
};
export const getAppInputPath = () => {
return parseAppInputPath(getParsedPackageJson().keycloakify?.appInputPath);
};
export const getKeycloakBuildPath = () => {
return parseKeycloakBuildPath(getParsedPackageJson().keycloakify?.keycloakBuildPath);
};
export const getThemeSrcDirPath = () => {
const srcDirPath = pathJoin(getReactProjectDirPath(), "src");
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
.map(fileRelativePath => {
const split = fileRelativePath.split(THEME_SRC_DIR_BASENAME);
if (split.length !== 2) {
return undefined;
}
return pathJoin(srcDirPath, split[0] + THEME_SRC_DIR_BASENAME);
})
.filter(exclude(undefined))[0];
if (themeSrcDirPath === undefined) {
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
return { "themeSrcDirPath": srcDirPath };
}
return { "themeSrcDirPath": undefined };
}
return { themeSrcDirPath };
};
export const getEmailThemeSrcDirPath = () => {
const { themeSrcDirPath } = getThemeSrcDirPath();
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
return { emailThemeSrcDirPath };
};

@ -1 +0,0 @@
export const ftlValuesGlobalName = "kcContext";

@ -1,380 +0,0 @@
<script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= {
<#assign fieldNames = [
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
]>
<#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#recover>
</#attempt>
"printIfExists": function (fieldName, x) {
<#if !messagesPerField?? >
return undefined;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>x<#else>undefined</#if>;
<#else>
return <#if messagesPerField.existsError('${fieldName}')>x<#else>undefined</#if>;
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"existsError": function (fieldName) {
<#if !messagesPerField?? >
return false;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>true<#else>false</#if>;
<#else>
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"get": function (fieldName) {
<#if !messagesPerField?? >
return '';
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#if messagesPerField.existsError('username', 'password')>
return 'Invalid username or password.';
</#if>
<#else>
<#if messagesPerField.existsError('${fieldName}')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
</#if>
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? >
return false;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.exists('username') || messagesPerField.exists('password')>true<#else>false</#if>;
<#else>
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
}
};
<#if account??>
out["url"]["getLogoutUrl"] = function () {
<#attempt>
return "${url.getLogoutUrl()}";
<#recover>
</#attempt>
};
</#if>
out["pageId"] = "${pageId}";
return out;
})()
<#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = "">
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
</#attempt>
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
</#if>
<#local keys = "">
<#attempt>
<#local keys = object?keys>
<#recover>
<#return "ABORT: We can't list keys on this object">
</#attempt>
<#local out_seq = []>
<#list keys as key>
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
<#continue>
</#if>
<#if
(
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
are_same_path(path, ["url"])
) || (
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"]) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
key == "identityProviderBrokerCtx" &&
are_same_path(path, []) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
are_same_path(path, ["realm"])
) || (
"error.ftl" == pageId &&
are_same_path(path, ["realm"]) &&
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#continue>
</#if>
<#recover>
</#attempt>
</#if>
<#attempt>
<#if !object[key]??>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
<#continue>
</#attempt>
<#local propertyValue = "">
<#attempt>
<#local propertyValue = object[key]>
<#recover>
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
<#continue>
</#attempt>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
</#list>
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
</#if>
<#local isMethod = "">
<#attempt>
<#local isMethod = object?is_method>
<#recover>
<#return "ABORT: Can't test if it'sa method.">
</#attempt>
<#if isMethod>
<#if are_same_path(path, ["auth", "showUsername"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showUsername()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showResetCredentials"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
</#attempt>
</#if>
<#return "ABORT: It's a method">
</#if>
<#local isBoolean = "">
<#attempt>
<#local isBoolean = object?is_boolean>
<#recover>
<#return "ABORT: Can't test if it's a boolean">
</#attempt>
<#if isBoolean>
<#return object?c>
</#if>
<#local isEnumerable = "">
<#attempt>
<#local isEnumerable = object?is_enumerable>
<#recover>
<#return "ABORT: Can't test if it's an enumerable">
</#attempt>
<#if isEnumerable>
<#local out_seq = []>
<#local i = 0>
<#list object as array_item>
<#if !array_item??>
<#local out_seq += ["null,"]>
<#continue>
</#if>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += [rec_out + ","]>
</#list>
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>
</#attempt>
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#if path?size != searchedPath?size>
<#return false>
</#if>
<#local i=0>
<#list path as property>
<#local searchedProperty=searchedPath[i]>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
<#if searchedProperty?is_string && !property?is_string>
<#return false>
</#if>
<#if searchedProperty?is_number && !property?is_number>
<#return false>
</#if>
<#if searchedProperty?string != property?string>
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
</script>

@ -1,203 +0,0 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
export const themeTypes = ["login", "account"] as const;
export type ThemeType = (typeof themeTypes)[number];
export const loginThemePageIds = [
"login.ftl",
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl",
"update-email.ftl",
"select-authenticator.ftl"
] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
export type LoginThemePageId = (typeof loginThemePageIds)[number];
export type AccountThemePageId = (typeof accountThemePageIds)[number];
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
customUserAttributes: string[];
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = {
isStandalone: false;
};
export type SameDomain = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = Common &
CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export function generateFtlFilesCodeFactory(params: {
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!,
buildOptions
});
$(element).text(fixedJsCode);
});
$("style").each((...[, element]) => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!,
buildOptions
});
$(element).text(fixedCssCode);
});
(
[
["link", "href"],
["script", "src"]
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
const href = $(element).attr(attrName);
if (href === undefined) {
return;
}
$(element).attr(
attrName,
buildOptions.isStandalone
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
);
})
);
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildOptions
}).cssCodeToPrependInHead,
"</style>",
""
].join("\n")
);
}
}
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace(
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>',
" </#list>",
"</#if>"
].join("\n")
};
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]
].join("\n")
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params: { pageId: string }): {
ftlCode: string;
} {
const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html();
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
return { ftlCode };
}
return { generateFtlFilesCode };
}

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

@ -1,88 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { themeTypes } from "./generateFtl/generateFtl";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
groupId: string;
artifactId?: string;
version: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string;
doBundlesEmailTemplate: boolean;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
buildOptions: { groupId, themeName, version, artifactId },
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artifactId}</artifactId>`,
` <version>${version}</version>`,
` <name>${artifactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [
{
"name": themeName,
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
}
]
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${version}.jar`)
};
}

@ -1,238 +0,0 @@
import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
customUserAttributes: string[];
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateKeycloakThemeResources(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
emailThemeSrcDirPath: string | undefined;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
}): Promise<{ doBundlesEmailTemplate: boolean }> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, keycloakVersion, buildOptions } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
let allCssGlobalsToDefine: Record<string, string> = {};
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
for (const themeType of themeTypes) {
const themeDirPath = getThemeDirPath(themeType);
copy_app_resources_to_theme_path: {
const isFirstPass = themeType.indexOf(themeType) === 0;
if (!isFirstPass && !buildOptions.isStandalone) {
break copy_app_resources_to_theme_path;
}
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
register_css_variables: {
if (!isFirstPass) {
break register_css_variables;
}
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
}
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
}
const generateFtlFilesCode = (() => {
if (generateFtlFilesCode_glob !== undefined) {
return generateFtlFilesCode_glob;
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions
});
return generateFtlFilesCode;
})();
[
...(() => {
switch (themeType) {
case "login":
return loginThemePageIds;
case "account":
return accountThemePageIds;
}
})(),
...((() => {
switch (themeType) {
case "login":
return buildOptions.extraLoginPages;
case "account":
return buildOptions.extraAccountPages;
}
})() ?? [])
].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["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(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.rmSync(tmpDirPath, { recursive: true, force: true });
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
}
let doBundlesEmailTemplate: boolean;
email: {
if (emailThemeSrcDirPath === undefined) {
doBundlesEmailTemplate = false;
break email;
}
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email")
});
}
return { doBundlesEmailTemplate };
}

@ -1,61 +0,0 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: {
keycloakVersion: string;
keycloakThemeBuildingDirPath: string;
buildOptions: BuildOptionsLike;
}) {
const {
keycloakThemeBuildingDirPath,
keycloakVersion,
buildOptions: { themeName }
} = params;
const keycloakThemePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(/\\/g, "/");
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from(
[
"#!/usr/bin/env bash",
"",
`docker rm ${containerName} || true`,
"",
`cd "${keycloakThemeBuildingDirPath.replace(/\\/g, "/")}"`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v "${keycloakThemePath}":"/opt/keycloak/themes/${themeName}":rw \\`,
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
""
].join("\n"),
"utf8"
),
{ "mode": 0o755 }
);
}

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

@ -1,152 +0,0 @@
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
import jar from "../tools/jar";
import { assert } from "tsafe/assert";
import { Equals } from "tsafe";
import { getEmailThemeSrcDirPath } from "./build-paths";
import { getCnamePath, getAppInputPath, getKeycloakBuildPath, getReactProjectDirPath } from "./build-paths";
export async function main() {
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const buildOptions = readBuildOptions({
"CNAME": (() => {
const cnameFilePath = getCnamePath();
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
});
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
keycloakThemeBuildingDirPath: buildOptions.keycloakBuildPath,
"emailThemeSrcDirPath": (() => {
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
return;
}
return emailThemeSrcDirPath;
})(),
"reactAppBuildDirPath": getAppInputPath(),
buildOptions,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets
});
const { jarFilePath } = generateJavaStackFiles({
keycloakThemeBuildingDirPath: buildOptions.keycloakBuildPath,
doBundlesEmailTemplate,
buildOptions
});
switch (buildOptions.bundler) {
case "none":
logger.log("😱 Skipping bundling step, there will be no jar");
break;
case "keycloakify":
logger.log("🫶 Let keycloakify do its thang");
await jar({
"rootPath": pathJoin(buildOptions.keycloakBuildPath, "src", "main", "resources"),
"version": buildOptions.version,
"groupId": buildOptions.groupId,
"artifactId": buildOptions.artifactId,
"targetPath": jarFilePath
});
break;
case "mvn":
logger.log("🫙 Run maven to deliver a jar");
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakBuildPath });
break;
default:
assert<Equals<typeof buildOptions.bundler, never>>(false);
}
// We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "20.0.1";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath: buildOptions.keycloakBuildPath,
"keycloakVersion": containerKeycloakVersion,
buildOptions
});
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(getReactProjectDirPath(), jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
"",
//TODO: Restore when we find a good Helm chart for Keycloak.
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
"",
"value.yaml: ",
" extraInitContainers: |",
" - name: realm-ext-provider",
" image: curlimages/curl",
" imagePullPolicy: IfNotPresent",
" command:",
" - sh",
" args:",
" - -c",
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
" volumeMounts:",
" - name: extensions",
" mountPath: /extensions",
" ",
" extraVolumeMounts: |",
" - name: extensions",
" mountPath: /opt/keycloak/providers",
" extraEnv: |",
" - name: KEYCLOAK_USER",
" value: admin",
" - name: KEYCLOAK_PASSWORD",
" value: xxxxxxxxx",
" - name: JAVA_OPTS",
" value: -Dkeycloak.profile=preview",
"",
"",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ .${pathSep}${pathRelative(
getReactProjectDirPath(),
pathJoin(getKeycloakBuildPath(), generateStartKeycloakTestingContainer.basename)
)} 👈`,
"",
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
`- Create a realm: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
`- Create a 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.themeName}`,
` 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")
);
}

@ -1,62 +0,0 @@
import * as fs from "fs";
import { assert } from "tsafe";
import type { Equals } from "tsafe";
import { z } from "zod";
import { pathJoin } from "../tools/pathJoin";
const reactProjectDirPath = process.cwd();
export const bundlers = ["mvn", "keycloakify", "none"] as const;
export type Bundler = (typeof bundlers)[number];
type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
keycloakify?: {
/** @deprecated: use extraLoginPages instead */
extraPages?: string[];
extraLoginPages?: string[];
extraAccountPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
artifactId?: string;
groupId?: string;
bundler?: Bundler;
keycloakVersionDefaultAssets?: string;
appInputPath?: string;
keycloakBuildPath?: string;
customUserAttributes?: string[];
themeName?: string;
};
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraLoginPages": z.array(z.string()).optional(),
"extraAccountPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
"bundler": z.enum(bundlers).optional(),
"keycloakVersionDefaultAssets": z.string().optional(),
"appInputPath": z.string().optional(),
"keycloakBuildPath": z.string().optional(),
"customUserAttributes": z.array(z.string()).optional(),
"themeName": z.string().optional()
})
.optional()
});
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
export const getParsedPackageJson = () => {
if (parsedPackageJson) return parsedPackageJson;
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8")));
return parsedPackageJson;
};

@ -1,86 +0,0 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
};
export type ExternalAssets = {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
/*
NOTE:
When we have urlOrigin defined it means that
we are building with --external-assets
so we have to make sur that the fixed js code will run
inside and outside keycloak.
When urlOrigin isn't defined we can assume the fixedJsCode
will always run in keycloak context.
*/
const { jsCode, buildOptions } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
(...[, n, u, e]) => `
${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
? `
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){}
});
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
}
}
return "${u}";
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };
}

@ -1,64 +0,0 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = {
urlPathname: string | undefined;
};
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine, buildOptions } = params;
return {
"cssCodeToPrependInHead": [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName =>
[
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/"
)
].join(" ")
)
.map(line => ` ${line};`),
"}"
].join("\n")
};
}

@ -1,47 +0,0 @@
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
urlPathname: string | undefined;
};
export type Standalone = Common & {
isStandalone: true;
};
export type ExternalAssets = Common & {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
fixedCssCode: string;
} {
const { cssCode, buildOptions } = params;
const fixedCssCode = cssCode.replace(
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(${
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
})`
);
return { fixedCssCode };
}

@ -1,5 +0,0 @@
import { pathJoin } from "./tools/pathJoin";
export const mockTestingSubDirOfPublicDirBasename = "keycloak_static";
export const mockTestingResourcesPath = pathJoin(mockTestingSubDirOfPublicDirBasename, "resources");
export const mockTestingResourcesCommonPath = pathJoin(mockTestingResourcesPath, "resources_common");

@ -1,47 +0,0 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
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("Initialize the directory with email template from which keycloak version?");
const tags = [
...(await getLatestsSemVersionedTag({
"count": 10,
"doIgnoreBeta": true,
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
"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 };
}

@ -1,73 +0,0 @@
export type NpmModuleVersion = {
major: number;
minor: number;
patch: number;
betaPreRelease?: number;
};
export namespace NpmModuleVersion {
export function parse(versionStr: string): NpmModuleVersion {
const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
if (!match) {
throw new Error(`${versionStr} is not a valid NPM version`);
}
return {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": parseInt(match[3]),
...(() => {
const str = match[4];
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
})()
};
}
export function stringify(v: NpmModuleVersion) {
return `${v.major}.${v.minor}.${v.patch}${v.betaPreRelease === undefined ? "" : `-beta.${v.betaPreRelease}`}`;
}
/**
*
* v1 < v2 => -1
* v1 === v2 => 0
* v1 > v2 => 1
*
*/
export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
const noUndefined = (n: number | undefined) => n ?? Infinity;
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
}
}
return 0;
}
/*
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0-beta.4")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("4.0.0")) === -1 )
*/
export function bumpType(params: { versionBehindStr: string; versionAheadStr: string }): "major" | "minor" | "patch" | "betaPreRelease" | "same" {
const versionAhead = parse(params.versionAheadStr);
const versionBehind = parse(params.versionBehindStr);
if (compare(versionBehind, versionAhead) === 1) {
throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
}
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (versionBehind[level] !== versionAhead[level]) {
return level;
}
}
return "same";
}
}

@ -1,15 +0,0 @@
import parseArgv from "minimist";
export type CliOptions = {
isSilent: boolean;
hasExternalAssets: boolean;
};
export const getCliOptions = (processArgv: string[]): CliOptions => {
const argv = parseArgv(processArgv);
return {
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
};

@ -1,27 +0,0 @@
import * as fs from "fs";
import * as path from "path";
/** List all files in a given directory return paths relative to the dir_path */
export const crawl = (() => {
const crawlRec = (dir_path: string, paths: string[]) => {
for (const file_name of fs.readdirSync(dir_path)) {
const file_path = path.join(dir_path, file_name);
if (fs.lstatSync(file_path).isDirectory()) {
crawlRec(file_path, paths);
continue;
}
paths.push(file_path);
}
};
return function crawl(dir_path: string): string[] {
const paths: string[] = [];
crawlRec(dir_path, paths);
return paths.map(file_path => path.relative(dir_path, file_path));
};
})();

@ -1,55 +0,0 @@
import { Readable } from "stream";
const crc32tab = [
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
];
/**
*
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
* @returns a promise for a checksum (uint32)
*/
export function crc32(input: Readable | String | Buffer): Promise<number> {
if (typeof input === "string") {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Buffer) {
let crc = ~0;
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
return Promise.resolve((crc ^ -1) >>> 0);
} else if (input instanceof Readable) {
return new Promise<number>((resolve, reject) => {
let crc = ~0;
input.setMaxListeners(Infinity);
input.on("end", () => resolve((crc ^ -1) >>> 0));
input.on("error", e => reject(e));
input.on("data", (chunk: Buffer) => {
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
});
});
} else {
throw new Error("Unsupported input " + typeof input);
}
}

@ -1,61 +0,0 @@
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
import { pipeline } from "stream/promises";
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
import { promisify } from "util";
import { crc32 } from "./crc32";
import tee from "./tee";
const deflateRaw = promisify(deflateRawCb);
/**
* A stream transformer that records the number of bytes
* passed in its `size` property.
*/
class ByteCounter extends PassThrough {
size: number = 0;
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
if ("length" in chunk) this.size += chunk.length;
super._transform(chunk, encoding, callback);
}
}
/**
* @param data buffer containing the data to be compressed
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
* of the source data
*/
export async function deflateBuffer(data: Buffer) {
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
return { deflated, crc32: checksum };
}
/**
* @param input a byte stream, containing data to be compressed
* @param sink a method that will accept chunks of compressed data; We don't pass
* a writable here, since we don't want the writablestream to be closed after
* a single file
* @returns a promise, which will resolve with the crc32 checksum and the
* compressed size
*/
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
const deflateWriter = new Writable({
write(chunk, _, callback) {
sink(chunk);
callback();
}
});
// tee the input stream, so we can compress and calc crc32 in parallel
const [rs1, rs2] = tee(input);
const byteCounter = new ByteCounter();
const [_, crc] = await Promise.all([
// pipe input into zip compressor, count the bytes
// returned and pass compressed data to the sink
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
// calc checksum
crc32(rs2)
]);
return { crc32: crc, compressedSize: byteCounter.size };
}

@ -1,87 +0,0 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, stat, writeFile } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe";
import { promisify } from "util";
import { getProjectRoot } from "./getProjectRoot";
import { transformCodebase } from "./transformCodebase";
import { unzip } from "./unzip";
const exec = promisify(execCallback);
function hash(s: string) {
return createHash("sha256").update(s).digest("hex");
}
async function exists(path: string) {
try {
await stat(path);
return true;
} catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return false;
throw error;
}
}
/**
* Get npm configuration as map
*/
async function getNmpConfig(): Promise<Record<string, string>> {
const { stdout } = await exec("npm config get", { encoding: "utf8" });
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2))
.reduce((cfg, [key, value]) => ({ ...cfg, [key]: value }), {});
}
/**
* Get proxy configuration from npm config files. Note that we don't care about
* proxy config in env vars, because make-fetch-happen will do that for us.
*
* @returns proxy configuration
*/
async function getNpmProxyConfig(): Promise<Pick<FetchOptions, "proxy" | "noProxy">> {
const cfg = await getNmpConfig();
const proxy = cfg["https-proxy"] ?? cfg["proxy"];
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
return { proxy, noProxy };
}
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
const projectRoot = getProjectRoot();
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
if (!(await exists(zipFilePath))) {
const proxyOpts = await getNpmProxyConfig();
const response = await fetch(url, proxyOpts);
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
* does not support node-fetch 3.x. So we stick around with this band-aid until
* octokit upgrades.
*/
response.body?.setMaxListeners(Number.MAX_VALUE);
assert(typeof response.body !== "undefined" && response.body != null);
await writeFile(zipFilePath, response.body);
}
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
transformCodebase({
"srcDirPath": extractDirPath,
"destDirPath": destDirPath
});
}

@ -1,17 +0,0 @@
import { getProjectRoot } from "./getProjectRoot";
import { join as pathJoin } from "path";
import { constants } from "fs";
import { chmod, stat } from "fs/promises";
(async () => {
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
const promises = Object.values<string>(bin).map(async scriptPath => {
const fullPath = pathJoin(getProjectRoot(), scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);
});
await Promise.all(promises);
})();

@ -1,7 +0,0 @@
import { relative as pathRelative } from "path";
export function isInside(params: { dirPath: string; filePath: string }) {
const { dirPath, filePath } = params;
return !pathRelative(dirPath, filePath).startsWith("..");
}

@ -1,94 +0,0 @@
import { Readable, Transform } from "stream";
import { dirname, relative, sep } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
import zip, { type ZipSource } from "./zip";
import { mkdir } from "fs/promises";
import trimIndent from "./trimIndent";
type JarArgs = {
rootPath: string;
targetPath: string;
groupId: string;
artifactId: string;
version: string;
};
/**
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
* the contents of the pom.properties file which is going to be added to the archive.
*/
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
const manifest: ZipSource = {
path: "META-INF/MANIFEST.MF",
data: Buffer.from(trimIndent`
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Keycloakify
Built-By: unknown
Build-Jdk: 19.0.0
`)
};
const pomProps: ZipSource = {
path: `META-INF/maven/${groupId}/${artifactId}/pom.properties`,
data: Buffer.from(trimIndent`# Generated by keycloakify
# ${new Date().toString()}
artifactId=${artifactId}
groupId=${groupId}
version=${version}
`)
};
/**
* Convert every path entry to a ZipSource record, and when all records are
* processed, append records for MANIFEST.mf and pom.properties
*/
const pathToRecord = () =>
new Transform({
objectMode: true,
transform: function (fsPath, _, cb) {
const path = relative(rootPath, fsPath).split(sep).join("/");
this.push({ path, fsPath });
cb();
},
final: function () {
this.push(manifest);
this.push(pomProps);
this.push(null);
}
});
await mkdir(dirname(targetPath), { recursive: true });
// Create an async pipeline, wait until everything is fully processed
await new Promise<void>((resolve, reject) => {
// walk all files in `rootPath` recursively
Readable.from(walk(rootPath))
// transform every path into a ZipSource object
.pipe(pathToRecord())
// let the zip lib convert all ZipSource objects into a byte stream
.pipe(zip())
// write that byte stream to targetPath
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
.on("finish", () => resolve())
.on("error", e => reject(e));
});
}
/**
* Standalone usage, call e.g. `ts-node jar.ts dirWithSources some-jar.jar`
*/
if (require.main === module) {
const main = () =>
jar({
rootPath: process.argv[2],
targetPath: process.argv[3],
artifactId: process.env.ARTIFACT_ID ?? "artifact",
groupId: process.env.GROUP_ID ?? "group",
version: process.env.VERSION ?? "1.0.0"
});
main();
}

@ -1,7 +0,0 @@
import { capitalize } from "tsafe/capitalize";
export function kebabCaseToCamelCase(kebabCaseString: string): string {
const [first, ...rest] = kebabCaseString.split("-");
return [first, ...rest.map(capitalize)].join("");
}

@ -1,27 +0,0 @@
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);
}
};
};

@ -1,40 +0,0 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
import { NpmModuleVersion } from "../NpmModuleVersion";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; doIgnoreBeta: boolean; count: number }): Promise<
{
tag: string;
version: NpmModuleVersion;
}[]
> {
const { owner, repo, doIgnoreBeta, count } = params;
const semVersionedTags: { tag: string; version: NpmModuleVersion }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
let version: NpmModuleVersion;
try {
version = NpmModuleVersion.parse(tag.replace(/^[vV]?/, ""));
} catch {
continue;
}
if (doIgnoreBeta && version.betaPreRelease !== undefined) {
continue;
}
semVersionedTags.push({ tag, version });
}
return semVersionedTags.sort(({ version: vX }, { version: vY }) => NpmModuleVersion.compare(vY, vX)).slice(0, count);
}
return { getLatestsSemVersionedTag };
}

@ -1,49 +0,0 @@
import type { Octokit } from "@octokit/rest";
const per_page = 99;
export function listTagsFactory(params: { octokit: Octokit }) {
const { octokit } = params;
const octokit_repo_listTags = async (params: { owner: string; repo: string; per_page: number; page: number }) => {
return octokit.repos.listTags(params);
};
async function* listTags(params: { owner: string; repo: string }): AsyncGenerator<string> {
const { owner, repo } = params;
let page = 1;
while (true) {
const resp = await octokit_repo_listTags({
owner,
repo,
per_page,
"page": page++
});
for (const branch of resp.data.map(({ name }) => name)) {
yield branch;
}
if (resp.data.length < 99) {
break;
}
}
}
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
async function getLatestTag(params: { owner: string; repo: string }): Promise<string | undefined> {
const { owner, repo } = params;
const itRes = await listTags({ owner, repo }).next();
if (itRes.done) {
return undefined;
}
return itRes.value;
}
return { listTags, getLatestTag };
}

@ -1,11 +0,0 @@
export type PromiseSettledAndPartitioned<T> = [T[], any[]];
export function partitionPromiseSettledResults<T>() {
return [
([successes, failures]: PromiseSettledAndPartitioned<T>, item: PromiseSettledResult<T>) =>
item.status === "rejected"
? ([successes, [item.reason, ...failures]] as PromiseSettledAndPartitioned<T>)
: ([[item.value, ...successes], failures] as PromiseSettledAndPartitioned<T>),
[[], []] as PromiseSettledAndPartitioned<T>
] as const;
}

@ -1,6 +0,0 @@
export function pathJoin(...path: string[]): string {
return path
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
.join("/");
}

@ -1,39 +0,0 @@
import { PassThrough, Readable } from "stream";
export default function tee(input: Readable) {
const a = new PassThrough();
const b = new PassThrough();
let aFull = false;
let bFull = false;
a.setMaxListeners(Infinity);
a.on("drain", () => {
aFull = false;
if (!aFull && !bFull) input.resume();
});
b.on("drain", () => {
bFull = false;
if (!aFull && !bFull) input.resume();
});
input.on("error", e => {
a.emit("error", e);
b.emit("error", e);
});
input.on("data", chunk => {
aFull = !a.write(chunk);
bFull = !b.write(chunk);
if (aFull || bFull) input.pause();
});
input.on("end", () => {
a.end();
b.end();
});
return [a, b] as const;
}

@ -1,46 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import { crawl } from "./crawl";
import { id } from "tsafe/id";
type TransformSourceCode = (params: { sourceCode: Buffer; filePath: string }) =>
| {
modifiedSourceCode: Buffer;
newFileName?: string;
}
| undefined;
/** Apply a transformation function to every file of directory */
export function transformCodebase(params: { srcDirPath: string; destDirPath: string; transformSourceCode?: TransformSourceCode }) {
const {
srcDirPath,
destDirPath,
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
"modifiedSourceCode": sourceCode
}))
} = params;
for (const file_relative_path of crawl(srcDirPath)) {
const filePath = path.join(srcDirPath, file_relative_path);
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path)
});
if (transformSourceCodeResult === undefined) {
continue;
}
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
"recursive": true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
modifiedSourceCode
);
}
}

@ -1,46 +0,0 @@
/**
* Concatenate the string fragments and interpolated values
* to get a single string.
*/
function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
const chunks: string[] = [];
for (let i = 0; i < strings.length; i++) {
let lastStringLineLength = 0;
if (strings[i]) {
chunks.push(strings[i]);
// remember last indent of the string portion
lastStringLineLength = strings[i].split("\n").at(-1)?.length ?? 0;
}
if (args[i]) {
// if the interpolation value has newlines, indent the interpolation values
// using the last known string indent
const chunk = String(args[i]).replace(/([\r?\n])/g, "$1" + " ".repeat(lastStringLineLength));
chunks.push(chunk);
}
}
return chunks.join("");
}
/**
* Shift all lines left by the *smallest* indentation level,
* and remove initial newline and all trailing spaces.
*/
export default function trimIndent(strings: TemplateStringsArray, ...args: any[]) {
// Remove initial and final newlines
let string = populateTemplate(strings, ...args)
.replace(/^[\r\n]/, "")
.replace(/\r?\n *$/, "");
const dents =
string
.match(/^([ \t])+/gm)
?.filter(s => /^\s+$/.test(s))
?.map(s => s.length) ?? [];
// No dents? no change required
if (!dents || dents.length == 0) return string;
const minDent = Math.min(...dents);
// The min indentation is 0, no change needed
if (!minDent) return string;
const re = new RegExp(`^${" ".repeat(minDent)}`, "gm");
const dedented = string.replace(re, "");
return dedented;
}

@ -1,92 +0,0 @@
import fsp from "node:fs/promises";
import fs from "fs";
import path from "node:path";
import yauzl from "yauzl";
import stream from "node:stream";
import { promisify } from "node:util";
const pipeline = promisify(stream.pipeline);
async function pathExists(path: string) {
try {
await fsp.stat(path);
return true;
} catch (error) {
if ((error as { code: string }).code === "ENOENT") {
return false;
}
throw error;
}
}
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
// add trailing slash to unzipSubPath and targetFolder
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
unzipSubPath += "/";
}
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
targetFolder += "/";
}
if (!fs.existsSync(targetFolder)) {
fs.mkdirSync(targetFolder, { recursive: true });
}
return new Promise<void>((resolve, reject) => {
yauzl.open(file, { lazyEntries: true }, async (err, zipfile) => {
if (err) {
reject(err);
return;
}
zipfile.readEntry();
zipfile.on("entry", async entry => {
if (unzipSubPath) {
// Skip files outside of the unzipSubPath
if (!entry.fileName.startsWith(unzipSubPath)) {
zipfile.readEntry();
return;
}
// Remove the unzipSubPath from the file name
entry.fileName = entry.fileName.substring(unzipSubPath.length);
}
const target = path.join(targetFolder, entry.fileName);
// Directory file names end with '/'.
// Note that entries for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
if (/[\/\\]$/.test(target)) {
await fsp.mkdir(target, { recursive: true });
zipfile.readEntry();
return;
}
// Skip existing files
if (await pathExists(target)) {
zipfile.readEntry();
return;
}
zipfile.openReadStream(entry, async (err, readStream) => {
if (err) {
reject(err);
return;
}
await pipeline(readStream, fs.createWriteStream(target));
zipfile.readEntry();
});
});
zipfile.once("end", function () {
zipfile.close();
resolve();
});
});
});
}

@ -1,19 +0,0 @@
import { readdir } from "fs/promises";
import { resolve } from "path";
/**
* Asynchronously and recursively walk a directory tree, yielding every file and directory
* found
*
* @param root the starting directory
* @returns AsyncGenerator
*/
export default async function* walk(root: string): AsyncGenerator<string, void, void> {
for (const entry of await readdir(root, { withFileTypes: true })) {
const absolutePath = resolve(root, entry.name);
if (entry.isDirectory()) {
yield absolutePath;
yield* walk(absolutePath);
} else yield absolutePath;
}
}

@ -1,246 +0,0 @@
import { Transform, TransformOptions } from "stream";
import { createReadStream } from "fs";
import { stat } from "fs/promises";
import { Blob } from "buffer";
import { deflateBuffer, deflateStream } from "./deflate";
/**
* Zip source
* @property filename the name of the entry in the archie
* @property path of the source file, if the source is an actual file
* @property data the actual data buffer, if the source is constructed in-memory
*/
export type ZipSource = { path: string } & ({ fsPath: string } | { data: Buffer });
export type ZipRecord = {
path: string;
compression: "deflate" | undefined;
uncompressedSize: number;
compressedSize?: number;
crc32?: number;
offset?: number;
};
/**
* @returns the actual byte size of an string
*/
function utf8size(s: string) {
return new Blob([s]).size;
}
/**
* @param record
* @returns a buffer representing a Zip local header
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Local_file_header
*/
function localHeader(record: ZipRecord) {
const { path, compression, uncompressedSize } = record;
const filenameSize = utf8size(path);
const buf = Buffer.alloc(30 + filenameSize);
buf.writeUInt32LE(0x04_03_4b_50, 0); // local header signature
buf.writeUInt16LE(10, 4); // min version
// we write 0x08 because crc and compressed size are unknown at
buf.writeUInt16LE(0x08, 6); // general purpose bit flag
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 8);
buf.writeUInt16LE(0, 10); // modified time
buf.writeUInt16LE(0, 12); // modified date
buf.writeUInt32LE(0, 14); // crc unknown
buf.writeUInt32LE(0, 18); // compressed size unknown
buf.writeUInt32LE(uncompressedSize, 22);
buf.writeUInt16LE(filenameSize, 26);
buf.writeUInt16LE(0, 28); // extra field length
buf.write(path, 30, "utf-8");
return buf;
}
/**
* @param record
* @returns a buffer representing a Zip central header
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Central_directory_file_header
*/
function centralHeader(record: ZipRecord) {
const { path, compression, crc32, compressedSize, uncompressedSize, offset } = record;
const filenameSize = utf8size(path);
const buf = Buffer.alloc(46 + filenameSize);
const isFile = !path.endsWith("/");
if (typeof offset === "undefined") throw new Error("Illegal argument");
// we don't want to deal with possibly messed up file or directory
// permissions, so we ignore the original permissions
const externalAttr = isFile ? 0x81a40000 : 0x41ed0000;
buf.writeUInt32LE(0x0201_4b50, 0); // central header signature
buf.writeUInt16LE(10, 4); // version
buf.writeUInt16LE(10, 6); // min version
buf.writeUInt16LE(0, 8); // general purpose bit flag
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 10);
buf.writeUInt16LE(0, 12); // modified time
buf.writeUInt16LE(0, 14); // modified date
buf.writeUInt32LE(crc32 || 0, 16);
buf.writeUInt32LE(compressedSize || 0, 20);
buf.writeUInt32LE(uncompressedSize, 24);
buf.writeUInt16LE(filenameSize, 28);
buf.writeUInt16LE(0, 30); // extra field length
buf.writeUInt16LE(0, 32); // comment field length
buf.writeUInt16LE(0, 34); // disk number
buf.writeUInt16LE(0, 36); // internal
buf.writeUInt32LE(externalAttr, 38); // external
buf.writeUInt32LE(offset, 42); // offset where file starts
buf.write(path, 46, "utf-8");
return buf;
}
/**
* @returns a buffer representing an Zip End-Of-Central-Directory block
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#End_of_central_directory_record_(EOCD)
*/
function eocd({ offset, cdSize, nRecords }: { offset: number; cdSize: number; nRecords: number }) {
const buf = Buffer.alloc(22);
buf.writeUint32LE(0x06054b50, 0); // eocd signature
buf.writeUInt16LE(0, 4); // disc number
buf.writeUint16LE(0, 6); // disc where central directory starts
buf.writeUint16LE(nRecords, 8); // records on this disc
buf.writeUInt16LE(nRecords, 10); // records total
buf.writeUInt32LE(cdSize, 12); // byte size of cd
buf.writeUInt32LE(offset, 16); // cd offset
buf.writeUint16LE(0, 20); // comment length
return buf;
}
/**
* @returns a stream Transform, which reads a stream of ZipRecords and
* writes a bytestream
*/
export default function zip() {
/**
* This is called when the input stream of ZipSource items is finished.
* Will write central directory and end-of-central-direcotry blocks.
*/
const final = () => {
// write central directory
let cdSize = 0;
for (const record of records) {
const head = centralHeader(record);
zipTransform.push(head);
cdSize += head.length;
}
// write end-of-central-directory
zipTransform.push(eocd({ offset, cdSize, nRecords: records.length }));
// signal stream end
zipTransform.push(null);
};
/**
* Write a directory entry to the archive
* @param path
*/
const writeDir = async (path: string) => {
const record: ZipRecord = {
path: path + "/",
offset,
compression: undefined,
uncompressedSize: 0
};
const head = localHeader(record);
zipTransform.push(head);
records.push(record);
offset += head.length;
};
/**
* Write a file entry to the archive
* @param archivePath path of the file in archive
* @param fsPath path to file on filesystem
* @param size of the actual, uncompressed, file
*/
const writeFile = async (archivePath: string, fsPath: string, size: number) => {
const record: ZipRecord = {
path: archivePath,
offset,
compression: "deflate",
uncompressedSize: size
};
const head = localHeader(record);
zipTransform.push(head);
const { crc32, compressedSize } = await deflateStream(createReadStream(fsPath), chunk => zipTransform.push(chunk));
record.crc32 = crc32;
record.compressedSize = compressedSize;
records.push(record);
offset += head.length + compressedSize;
};
/**
* Write archive record based on filesystem file or directory
* @param archivePath path of item in archive
* @param fsPath path to item on filesystem
*/
const writeFromPath = async (archivePath: string, fsPath: string) => {
const fileStats = await stat(fsPath);
fileStats.isDirectory() ? await writeDir(archivePath) /**/ : await writeFile(archivePath, fsPath, fileStats.size) /**/;
};
/**
* Write archive record based on data in a buffer
* @param path
* @param data
*/
const writeFromBuffer = async (path: string, data: Buffer) => {
const { deflated, crc32 } = await deflateBuffer(data);
const record: ZipRecord = {
path,
compression: "deflate",
crc32,
uncompressedSize: data.length,
compressedSize: deflated.length,
offset
};
const head = localHeader(record);
zipTransform.push(head);
zipTransform.push(deflated);
records.push(record);
offset += head.length + deflated.length;
};
/**
* Write an archive record
* @param source
*/
const writeRecord = async (source: ZipSource) => {
if ("fsPath" in source) await writeFromPath(source.path, source.fsPath);
else if ("data" in source) await writeFromBuffer(source.path, source.data);
else throw new Error("Illegal argument " + typeof source + " " + JSON.stringify(source));
};
/**
* The actual stream transform function
* @param source
* @param _ encoding, ignored
* @param cb
*/
const transform: TransformOptions["transform"] = async (source: ZipSource, _, cb) => {
await writeRecord(source);
cb();
};
/** offset and records keep local state during processing */
let offset = 0;
const records: ZipRecord[] = [];
const zipTransform = new Transform({
readableObjectMode: false,
writableObjectMode: true,
transform,
final
});
return zipTransform;
}

@ -1,11 +0,0 @@
{
"extends": "../../tsproject.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES5",
"esModuleInterop": true,
"lib": ["es2015", "DOM", "ES2019.Object"],
"outDir": "../../dist/bin",
"rootDir": "."
}
}

@ -1 +1,2 @@
export { createKeycloakAdapter } from "keycloakify/lib/keycloakJsAdapter";
export { myFunction } from "./myFunction";
export { myObject } from "./myObject";

@ -1,114 +0,0 @@
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;
};
}
/**
* 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;
}
}
};
}

@ -1,18 +0,0 @@
import { clsx } from "keycloakify/tools/clsx";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
export function createUseClassName<ClassKey extends string>(params: { defaultClasses: Record<ClassKey, string | undefined> }) {
const { defaultClasses } = params;
function useGetClassName(params: { doUseDefaultCss: boolean; classes: Partial<Record<ClassKey, string>> | undefined }) {
const { classes } = params;
const getClassName = useConstCallback((classKey: ClassKey): string => {
return clsx(classKey, defaultClasses[classKey], classes?.[classKey]);
});
return { getClassName };
}
return { useGetClassName };
}

@ -1,93 +0,0 @@
import { useReducer, useEffect } from "react";
import { headInsert } from "keycloakify/tools/headInsert";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { clsx } from "keycloakify/tools/clsx";
export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean;
stylesCommon?: string[];
styles?: string[];
scripts?: string[];
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
htmlClassName: string | undefined;
bodyClassName: string | undefined;
}) {
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, htmlClassName, bodyClassName } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
useEffect(() => {
if (!doFetchDefaultThemeResources) {
return;
}
let isUnmounted = false;
Promise.all(
[
...(stylesCommon ?? []).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...(styles ?? []).map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
"type": "css",
href,
"position": "prepend"
})
)
).then(() => {
if (isUnmounted) {
return;
}
setReady();
});
(scripts ?? []).forEach(relativePath =>
headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
return () => {
isUnmounted = true;
};
}, []);
useSetClassName({
"target": "html",
"className": htmlClassName
});
useSetClassName({
"target": "body",
"className": bodyClassName
});
return { isReady };
}
function useSetClassName(params: { target: "html" | "body"; className: string | undefined }) {
const { target, className } = params;
useEffect(() => {
if (className === undefined) {
return;
}
const htmlClassList = document.getElementsByTagName(target)[0].classList;
const tokens = clsx(className).split(" ");
htmlClassList.add(...tokens);
return () => {
htmlClassList.remove(...tokens);
};
}, [className]);
}

@ -1,89 +0,0 @@
import { lazy, Suspense } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { assert, type Equals } from "tsafe/assert";
import type { I18n } from "./i18n";
import type { KcContext } from "./kcContext";
const Login = lazy(() => import("keycloakify/login/pages/Login"));
const Register = lazy(() => import("keycloakify/login/pages/Register"));
const RegisterUserProfile = lazy(() => import("keycloakify/login/pages/RegisterUserProfile"));
const Info = lazy(() => import("keycloakify/login/pages/Info"));
const Error = lazy(() => import("keycloakify/login/pages/Error"));
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("keycloakify/login/pages/LoginVerifyEmail"));
const Terms = lazy(() => import("keycloakify/login/pages/Terms"));
const LoginOtp = lazy(() => import("keycloakify/login/pages/LoginOtp"));
const LoginPassword = lazy(() => import("keycloakify/login/pages/LoginPassword"));
const LoginUsername = lazy(() => import("keycloakify/login/pages/LoginUsername"));
const WebauthnAuthenticate = lazy(() => import("keycloakify/login/pages/WebauthnAuthenticate"));
const LoginUpdatePassword = lazy(() => import("keycloakify/login/pages/LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("keycloakify/login/pages/LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("keycloakify/login/pages/LoginIdpLinkConfirm"));
const LoginPageExpired = lazy(() => import("keycloakify/login/pages/LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("keycloakify/login/pages/LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("keycloakify/login/pages/LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("keycloakify/login/pages/LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
const SelectAuthenticator = lazy(() => import("keycloakify/login/pages/SelectAuthenticator"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login kcContext={kcContext} {...rest} />;
case "register.ftl":
return <Register kcContext={kcContext} {...rest} />;
case "register-user-profile.ftl":
return <RegisterUserProfile kcContext={kcContext} {...rest} />;
case "info.ftl":
return <Info kcContext={kcContext} {...rest} />;
case "error.ftl":
return <Error kcContext={kcContext} {...rest} />;
case "login-reset-password.ftl":
return <LoginResetPassword kcContext={kcContext} {...rest} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail kcContext={kcContext} {...rest} />;
case "terms.ftl":
return <Terms kcContext={kcContext} {...rest} />;
case "login-otp.ftl":
return <LoginOtp kcContext={kcContext} {...rest} />;
case "login-username.ftl":
return <LoginUsername kcContext={kcContext} {...rest} />;
case "login-password.ftl":
return <LoginPassword kcContext={kcContext} {...rest} />;
case "webauthn-authenticate.ftl":
return <WebauthnAuthenticate kcContext={kcContext} {...rest} />;
case "login-update-password.ftl":
return <LoginUpdatePassword kcContext={kcContext} {...rest} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile kcContext={kcContext} {...rest} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm kcContext={kcContext} {...rest} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail kcContext={kcContext} {...rest} />;
case "login-page-expired.ftl":
return <LoginPageExpired kcContext={kcContext} {...rest} />;
case "login-config-totp.ftl":
return <LoginConfigTotp kcContext={kcContext} {...rest} />;
case "logout-confirm.ftl":
return <LogoutConfirm kcContext={kcContext} {...rest} />;
case "update-user-profile.ftl":
return <UpdateUserProfile kcContext={kcContext} {...rest} />;
case "idp-review-user-profile.ftl":
return <IdpReviewUserProfile kcContext={kcContext} {...rest} />;
case "update-email.ftl":
return <UpdateEmail kcContext={kcContext} {...rest} />;
case "select-authenticator.ftl":
return <SelectAuthenticator kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}
</Suspense>
);
}

@ -1,195 +0,0 @@
import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx";
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { type TemplateProps } from "keycloakify/login/TemplateProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const {
displayInfo = false,
displayMessage = true,
displayRequiredFields = false,
displayWide = false,
showAnotherWayIfPresent = true,
headerNode,
showUsernameNode = null,
infoNode = null,
kcContext,
i18n,
doUseDefaultCss,
classes,
children
} = props;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
const { isReady } = usePrepareTemplate({
"doFetchDefaultThemeResources": doUseDefaultCss,
url,
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"htmlClassName": getClassName("kcHtmlClass"),
"bodyClassName": undefined
});
if (!isReady) {
return null;
}
return (
<div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={getClassName("kcHeaderClass")}>
<div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
{msg("loginTitleHtml", realm.displayNameHtml)}
</div>
</div>
<div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
<header className={getClassName("kcFormHeaderClass")}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<div id="kc-locale">
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
</li>
))}
</ul>
</div>
</div>
</div>
)}
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
displayRequiredFields ? (
<div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle">
<span className="required">*</span>
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
<h1 id="kc-page-title">{headerNode}</h1>
</div>
</div>
) : (
<h1 id="kc-page-title">{headerNode}</h1>
)
) : displayRequiredFields ? (
<div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle">
<span className="required">*</span> {msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</div>
</div>
) : (
<>
{showUsernameNode}
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</>
)}
</header>
<div id="kc-content">
<div id="kc-content-wrapper">
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
{message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
{message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
{message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
<span
className="kc-feedback-text"
dangerouslySetInnerHTML={{
"__html": message.summary
}}
/>
</div>
)}
{children}
{auth !== undefined && auth.showTryAnotherWayLink && showAnotherWayIfPresent && (
<form
id="kc-select-try-another-way-form"
action={url.loginAction}
method="post"
className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
>
<div
className={clsx(
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
)}
>
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
href="#"
id="try-another-way"
onClick={() => {
document.forms["kc-select-try-another-way-form" as never].submit();
return false;
}}
>
{msg("doTryAnotherWay")}
</a>
</div>
</div>
</form>
)}
{displayInfo && (
<div id="kc-info" className={getClassName("kcSignUpClass")}>
<div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
{infoNode}
</div>
</div>
)}
</div>
</div>
</div>
</div>
);
}

@ -1,96 +0,0 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
kcContext: KcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>;
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
showUsernameNode?: ReactNode;
infoNode?: ReactNode;
children: ReactNode;
};
export type ClassKey =
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
| "kcHeaderWrapperClass"
| "kcFormCardClass"
| "kcFormCardAccountClass"
| "kcFormHeaderClass"
| "kcLocaleWrapperClass"
| "kcContentWrapperClass"
| "kcLabelWrapperClass"
| "kcFormGroupClass"
| "kcResetFlowIcon"
| "kcFeedbackSuccessIcon"
| "kcFeedbackWarningIcon"
| "kcFeedbackErrorIcon"
| "kcFeedbackInfoIcon"
| "kcFormSocialAccountContentClass"
| "kcFormSocialAccountClass"
| "kcSignUpClass"
| "kcInfoAreaWrapperClass"
| "kcLogoClass"
| "kcContainerClass"
| "kcContentClass"
| "kcFeedbackAreaClass"
| "kcLocaleClass"
| "kcAlertIconClasserror"
| "kcFormAreaClass"
| "kcFormSocialAccountListClass"
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass"
| "kcInputWrapperClass"
| "kcFormOptionsClass"
| "kcFormButtonsClass"
| "kcFormSettingClass"
| "kcTextareaClass"
| "kcInfoAreaClass"
| "kcFormGroupHeader"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonDefaultClass"
| "kcButtonLargeClass"
| "kcButtonBlockClass"
| "kcInputLargeClass"
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass"
| "kcSelectOTPListClass"
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass";

@ -1,229 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import { useEffect, useState, useRef } from "react";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContext } from "../kcContext/KcContext";
import { Markdown } from "keycloakify/tools/Markdown";
export const fallbackLanguageTag = "en";
export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
};
};
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type GenericI18n<MessageKey extends string> = {
/**
* e.g: "en", "fr", "zh-CN"
*
* The current language
*/
currentLanguageTag: string;
/**
* To call when the user switch language.
* This will cause the page to be reloaded,
* on next load currentLanguageTag === newLanguageTag
*/
changeLocale: (newLanguageTag: string) => never;
/**
* e.g. "en" => "English", "fr" => "Français", ...
*
* Used to render a select that enable user to switch language.
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
* */
labelBySupportedLanguageTag: Record<string, string>;
/**
* Examples assuming currentLanguageTag === "en"
*
* msg("access-denied") === <span>Access denied</span>
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
*/
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
/**
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
*/
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
*/
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
/**
* Examples assuming currentLanguageTag === "en"
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*/
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
};
export type I18n = GenericI18n<MessageKey>;
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
[languageTag: string]: { [key in ExtraMessageKey]: string };
}) {
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
const { kcContext } = params;
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
const refHasStartedFetching = useRef(false);
useEffect(() => {
if (refHasStartedFetching.current) {
return;
}
refHasStartedFetching.current = true;
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
"messages": {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
currentLanguageTag,
"changeLocale": newLanguageTag => {
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
assert(false, "never");
},
"labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
)
});
})();
}, []);
return i18n ?? null;
}
return { useI18n };
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
fallbackMessages: Record<MessageKey, string>;
messages: Record<MessageKey, string>;
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
const { fallbackMessages, messages } = params;
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
const { key, args, doRenderMarkdown } = props;
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
if (messageOrUndefined === undefined) {
return undefined;
}
const message = messageOrUndefined;
const messageWithArgsInjectedIfAny = (() => {
const startIndex = message
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
// No {0} in message (no arguments expected)
return message;
}
let messageWithArgsInjected = message;
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return messageWithArgsInjected;
})();
return doRenderMarkdown ? (
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
{messageWithArgsInjectedIfAny}
</Markdown>
) : (
messageWithArgsInjectedIfAny
);
}
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
const { key, args, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
doRenderMarkdown
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
return {
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
};
}
const keycloakifyExtraMessages = {
"en": {
"shouldBeEqual": "{0} should be equal to {1}",
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option"
},
"fr": {
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être égal à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entier",
"notAValidOption": "N'est pas une option valide",
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter"
/* spell-checker: enable */
}
};

@ -1 +0,0 @@
export type { I18n } from "./i18n";

@ -1,9 +0,0 @@
import Fallback from "keycloakify/login/Fallback";
export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { PageProps } from "keycloakify/login/pages/PageProps";

@ -1,545 +0,0 @@
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
/** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages.
* (ex: url.loginAction is undefined on error.ftl)
*/
export type KcContext =
| KcContext.Login
| KcContext.Register
| KcContext.RegisterUserProfile
| KcContext.Info
| KcContext.Error
| KcContext.LoginResetPassword
| KcContext.LoginVerifyEmail
| KcContext.Terms
| KcContext.LoginOtp
| KcContext.LoginUsername
| KcContext.WebauthnAuthenticate
| KcContext.LoginPassword
| KcContext.LoginUpdatePassword
| KcContext.LoginUpdateProfile
| KcContext.LoginIdpLinkConfirm
| KcContext.LoginIdpLinkEmail
| KcContext.LoginPageExpired
| KcContext.LoginConfigTotp
| KcContext.LogoutConfirm
| KcContext.UpdateUserProfile
| KcContext.IdpReviewUserProfile
| KcContext.UpdateEmail
| KcContext.SelectAuthenticator;
export declare namespace KcContext {
export type Common = {
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
loginUrl: string;
};
realm: {
name: string;
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
registrationEmailAsUsername: boolean;
};
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
auth?: {
showUsername?: boolean;
showResetCredentials?: boolean;
showTryAnotherWayLink?: boolean;
attemptedUsername?: string;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
client: {
clientId: string;
name?: string;
description?: string;
};
isAppInitiatedAction: boolean;
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
existsError: (fieldName: string) => boolean;
get: (fieldName: string) => string;
exists: (fieldName: string) => boolean;
};
};
export type Login = Common & {
pageId: "login.ftl";
url: {
loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: {
loginWithEmailAllowed: boolean;
rememberMe: boolean;
password: boolean;
resetPasswordAllowed: boolean;
registrationAllowed: boolean;
};
auth: {
selectedCredential?: string;
};
registrationDisabled: boolean;
login: {
username?: string;
rememberMe?: string;
password?: string;
};
usernameEditDisabled: boolean;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[];
};
};
export type Register = RegisterUserProfile.CommonWithLegacy & {
pageId: "register.ftl";
register: {
formData: {
firstName?: string;
displayName?: string;
lastName?: string;
email?: string;
username?: string;
};
};
};
export type RegisterUserProfile = RegisterUserProfile.CommonWithLegacy & {
pageId: "register-user-profile.ftl";
profile: {
context: "REGISTRATION_PROFILE";
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
export namespace RegisterUserProfile {
export type CommonWithLegacy = Common & {
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[];
};
};
}
export type Info = Common & {
pageId: "info.ftl";
messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[];
skipLink: boolean;
pageRedirectUri?: string;
actionUri?: string;
client: {
baseUrl?: string;
};
};
export type Error = Common & {
pageId: "error.ftl";
client?: {
baseUrl?: string;
};
message: NonNullable<Common["message"]>;
};
export type LoginResetPassword = Common & {
pageId: "login-reset-password.ftl";
realm: {
loginWithEmailAllowed: boolean;
};
url: {
loginResetCredentialsUrl: string;
};
};
export type LoginVerifyEmail = Common & {
pageId: "login-verify-email.ftl";
//NOTE: Optional because maybe it wasn't defined in older keycloak versions.
user?: {
email: string;
};
};
export type Terms = Common & {
pageId: "terms.ftl";
};
export type LoginOtp = Common & {
pageId: "login-otp.ftl";
otpLogin: {
userOtpCredentials: { id: string; userLabel: string }[];
};
};
export type LoginUsername = Common & {
pageId: "login-username.ftl";
url: {
loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: {
loginWithEmailAllowed: boolean;
rememberMe: boolean;
password: boolean;
resetPasswordAllowed: boolean;
registrationAllowed: boolean;
};
registrationDisabled: boolean;
login: {
username?: string;
rememberMe?: string;
};
usernameHidden?: boolean;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[];
};
};
export type LoginPassword = Common & {
pageId: "login-password.ftl";
url: {
loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: {
resetPasswordAllowed: boolean;
};
auth?: {
showUsername?: boolean;
showResetCredentials?: boolean;
showTryAnotherWayLink?: boolean;
attemptedUsername?: string;
};
social: {
displayInfo: boolean;
};
login: {
password?: string;
};
};
export type WebauthnAuthenticate = Common & {
pageId: "webauthn-authenticate.ftl";
authenticators: {
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
};
challenge: string;
// I hate this:
userVerification: UserVerificationRequirement | "not specified";
rpId: string;
createTimeout: string;
isUserIdentified: "true" | "false";
shouldDisplayAuthenticators: boolean;
social: {
displayInfo: boolean;
};
login: {};
};
export namespace WebauthnAuthenticate {
export type WebauthnAuthenticator = {
credentialId: string;
transports: {
iconClass: string;
displayNameProperties: MessageKey[];
};
label: string;
createdAt: string;
};
}
export type LoginUpdatePassword = Common & {
pageId: "login-update-password.ftl";
username: string;
};
export type LoginUpdateProfile = Common & {
pageId: "login-update-profile.ftl";
user: {
editUsernameAllowed: boolean;
username?: string;
email?: string;
firstName?: string;
lastName?: string;
};
};
export type LoginIdpLinkConfirm = Common & {
pageId: "login-idp-link-confirm.ftl";
idpAlias: string;
};
export type LoginIdpLinkEmail = Common & {
pageId: "login-idp-link-email.ftl";
brokerContext: {
username: string;
};
idpAlias: string;
};
export type LoginPageExpired = Common & {
pageId: "login-page-expired.ftl";
};
export type LoginConfigTotp = Common & {
pageId: "login-config-totp.ftl";
mode?: "qr" | "manual" | undefined | null;
totp: {
totpSecretEncoded: string;
qrUrl: string;
policy: {
supportedApplications: string[];
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
} & (
| {
type: "totp";
period: number;
}
| {
type: "hotp";
initialCounter: number;
}
);
totpSecretQrCode: string;
manualUrl: string;
totpSecret: string;
otpCredentials: { id: string; userLabel: string }[];
};
};
export type LogoutConfirm = Common & {
pageId: "logout-confirm.ftl";
url: {
logoutConfirmAction: string;
};
client: {
baseUrl?: string;
};
logoutConfirm: {
code: string;
skipLink?: boolean;
};
};
export type UpdateUserProfile = Common & {
pageId: "update-user-profile.ftl";
profile: {
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
export type IdpReviewUserProfile = Common & {
pageId: "idp-review-user-profile.ftl";
profile: {
context: "IDP_REVIEW";
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
export type UpdateEmail = Common & {
pageId: "update-email.ftl";
email: {
value?: string;
};
};
export type SelectAuthenticator = Common & {
pageId: "select-authenticator.ftl";
auth: {
authenticationSelections: SelectAuthenticator.AuthenticationSelection[];
};
};
export namespace SelectAuthenticator {
export type AuthenticationSelection = {
authExecId: string;
displayName:
| "otp-display-name"
| "password-display-name"
| "auth-username-form-display-name"
| "auth-username-password-form-display-name"
| "webauthn-display-name"
| "webauthn-passwordless-display-name";
helpText:
| "otp-help-text"
| "password-help-text"
| "auth-username-form-help-text"
| "auth-username-password-form-help-text"
| "webauthn-help-text"
| "webauthn-passwordless-help-text";
iconCssClass?:
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass";
};
}
}
export type Attribute = {
name: string;
displayName?: string;
required: boolean;
value?: string;
group?: string;
groupDisplayHeader?: string;
groupDisplayDescription?: string;
readOnly: boolean;
validators: Validators;
annotations: Record<string, string>;
groupAnnotations: Record<string, string>;
autocomplete?:
| "on"
| "off"
| "name"
| "honorific-prefix"
| "given-name"
| "additional-name"
| "family-name"
| "honorific-suffix"
| "nickname"
| "email"
| "username"
| "new-password"
| "current-password"
| "one-time-code"
| "organization-title"
| "organization"
| "street-address"
| "address-line1"
| "address-line2"
| "address-line3"
| "address-level4"
| "address-level3"
| "address-level2"
| "address-level1"
| "country"
| "country-name"
| "postal-code"
| "cc-name"
| "cc-given-name"
| "cc-additional-name"
| "cc-family-name"
| "cc-number"
| "cc-exp"
| "cc-exp-month"
| "cc-exp-year"
| "cc-csc"
| "cc-type"
| "transaction-currency"
| "transaction-amount"
| "language"
| "bday"
| "bday-day"
| "bday-month"
| "bday-year"
| "sex"
| "tel"
| "tel-country-code"
| "tel-national"
| "tel-area-code"
| "tel-local"
| "tel-extension"
| "impp"
| "url"
| "photo";
};
export type Validators = Partial<{
length: Validators.DoIgnoreEmpty & Validators.Range;
double: Validators.DoIgnoreEmpty & Validators.Range;
integer: Validators.DoIgnoreEmpty & Validators.Range;
email: Validators.DoIgnoreEmpty;
"up-immutable-attribute": {};
"up-attribute-required-by-metadata-value": {};
"up-username-has-value": {};
"up-duplicate-username": {};
"up-username-mutation": {};
"up-email-exists-as-username": {};
"up-blank-attribute-value": Validators.ErrorMessage & {
"fail-on-null": boolean;
};
"up-duplicate-email": {};
"local-date": Validators.DoIgnoreEmpty;
pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string };
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
uri: Validators.DoIgnoreEmpty;
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
/** Made up validator that only exists in Keycloakify */
_compareToOther: Validators.DoIgnoreEmpty &
Validators.ErrorMessage & {
name: string;
shouldBe: "equal" | "different";
};
options: Validators.Options;
}>;
export declare namespace Validators {
export type DoIgnoreEmpty = {
"ignore.empty.value"?: boolean;
};
export type ErrorMessage = {
"error-message"?: string;
};
export type Range = {
/** "0", "1", "2"... yeah I know, don't tell me */
min?: `${number}`;
max?: `${number}`;
};
export type Options = {
options: string[];
};
}
assert<Equals<KcContext["pageId"], LoginThemePageId>>();

@ -1,135 +0,0 @@
import type { KcContext, Attribute } from "./KcContext";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(
[
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
`If assets are missing make sure you have built your Keycloak theme at least once.`
].join(" "),
"background: red; color: yellow; font-size: medium"
);
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
if (
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
) {
assert(
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
);
const { attributes } = kcContextDefaultMock.profile;
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes = [];
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName = {};
const partialAttributes = [
...((partialKcContextCustomMock as DeepPartial<KcContext.RegisterUserProfile>).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute
});
if (partialAttribute !== undefined) {
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute
});
}
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
}
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined };
}
if (!("login" in realKcContext)) {
return { "kcContext": undefined };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
}
return { "kcContext": realKcContext };
}

@ -1,11 +0,0 @@
import type { KcContext } from "./KcContext";
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
}

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