Compare commits

..

1 Commits

Author SHA1 Message Date
1ca979da30 Fix CI (changelog ignore) 2022-01-19 00:40:03 +01:00
57 changed files with 2387 additions and 1242 deletions

View File

@ -27,12 +27,12 @@ jobs:
needs: test_formatting
strategy:
matrix:
node: [ '15', '14' ]
node: [ '15', '14', '13' ]
name: Test with Node v${{ matrix.node }}
steps:
- name: Tell if project is using npm or yarn
id: step1
uses: garronej/ts-ci@v1.1.6
uses: garronej/ts-ci@v1.1.4
with:
action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v2.3.4
@ -64,7 +64,7 @@ jobs:
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_release_beta: ${{steps.step1.outputs.is_release_beta }}
steps:
- uses: garronej/ts-ci@v1.1.6
- uses: garronej/ts-ci@v1.1.4
id: step1
with:
action_name: is_package_json_version_upgraded
@ -75,7 +75,7 @@ jobs:
needs: check_if_version_upgraded
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
steps:
- uses: garronej/ts-ci@v1.1.6
- uses: garronej/ts-ci@v1.1.4
with:
action_name: update_changelog
branch: ${{ github.head_ref || github.ref }}

3
.gitignore vendored
View File

@ -46,6 +46,3 @@ jspm_packages
/.yarn_home/
.idea
/keycloak_theme_email
/build_keycloak

View File

@ -3,5 +3,4 @@ node_modules/
/CHANGELOG.md
/.yarn_home/
/src/test/apps/
/src/tools/types/
/sample_react_project
/src/tools/types/

View File

@ -1,120 +1,3 @@
# **5.0.0** (2022-04-27)
- i18n rebuild from the ground up
## **4.10.0** (2022-04-26)
- Merge pull request #92 from Tasyp/add-login-idp-link-email
feat: add login-idp-link-email page
- feat: add mock data for login-idp-link-email page
- feat: supply broker context with context
## **4.9.0** (2022-04-25)
- Test by default with kc 18. Update instructions to use quay.io/keycloak/keycloak instead of jboss/keycloak #93
### **4.8.7** (2022-04-25)
- Update instructions to test on Keycloak 18 https://github.com/keycloak/keycloak-web/issues/306 #93
- Move the documentation form the readme to docs.keycloakify.dev
- Update README.md
- Update demo video
### **4.8.6** (2022-04-22)
- always offer to download v11.0.3
### **4.8.5** (2022-04-22)
- #91
### **4.8.4** (2022-04-22)
- #90
### **4.8.3** (2022-04-20)
### **4.8.2** (2022-04-20)
- Tell pepoles they can test with different keycloak version
### **4.8.1** (2022-04-20)
- Add missing shebang
- Add video demo for npx download-builtin-keycloak-theme
## **4.8.0** (2022-04-20)
- Document email template customization feature #9
- Add mention of download-builtin-keycloak-theme
- Let the choice of kc version be auto in GH Action
- Only test on node v15 and v14 (bellow is no longer supported (rmSync)
- Feature email customization #9
### **4.7.6** (2022-04-12)
- Fix bugs with language switch #85
### **4.7.5** (2022-04-09)
- Fix #85
### **4.7.4** (2022-04-09)
- M1 Mac compat (for real this time)
### **4.7.3** (2022-04-08)
- Mention that there is still problems with M1 Mac
### **4.7.2** (2022-04-06)
- #43: M1 Mac support
### **4.7.1** (2022-03-30)
- Improve browser autofill
- factorization
## **4.7.0** (2022-03-17)
- Add support for options validator
- remove duplicate dependency
## **4.6.0** (2022-03-07)
- Remove powerhooks as dev dependency
### **4.5.5** (2022-03-07)
- Update tss-react
### **4.5.4** (2022-03-06)
- Remove tss-react from peerDependencies (it becomes a dependency)
- (dev script) Use tsconfig.json to tell we are at the root of the project
### **4.5.3** (2022-01-26)
- Themes no longer have to break on minor Keycloakify update
### **4.5.2** (2022-01-20)
- Test container uses Keycloak 16.1.0
- Merge pull request #78 from InseeFrLab/Ann2827/pull
Ann2827/pull
- Refactor #78
- Compat with Keycloak 16 (and probably 17, 18) #79
- Warning about compat issues with Keycloak 16
- fix: changes
- fix: Errors on pages login-idp-link-confirm and login-idp-link-email
ref: https://github.com/InseeFrLab/keycloakify/issues/75
### **4.5.1** (2022-01-18)
- fix previous version

497
README.md
View File

@ -23,60 +23,465 @@
<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>
</p>
</p>
<p align="center">
<i>Ultimately this build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<i>Ultimately this build tool generates a Keycloak theme</i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
> **New in v4.4.0**: Feature [`login-page-expired.ftl`](https://user-images.githubusercontent.com/6702424/147856832-38c042a7-9fc8-473f-9595-e00123095ca6.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/812754109c61157741f4a0b222026deb1538a02d/src/KcApp/KcApp.tsx#L18) and [this](https://github.com/garronej/keycloakify-demo-app/blob/812754109c61157741f4a0b222026deb1538a02d/src/KcApp/KcApp.tsx#L39) to update.
# Motivations
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
It involves, however, a lot of raw JS/CSS/[FTL]() hacking, and bundling the theme is not exactly straightforward.
Beyond that, if you use Keycloak for a specific app you want your login page to be tightly integrated with it.
Ideally, you don't want the user to notice when he is being redirected away.
Trying to reproduce the look and feel of a specific app in another stack is not an easy task not to mention
the cheer amount of maintenance that it involves.
<p align="center">
<i>Without keycloakify, users suffers from a harsh context switch, no fronted form pre-validation</i><br>
<img src="https://user-images.githubusercontent.com/6702424/134997335-a28b4a57-0884-47ec-9341-a0e49f835c4d.gif">
</p>
Wouldn't it be great if we could just design the login and register pages as if they were part of our app?
Here is `keycloakify` for you 🍸
<p align="center">
<i> <a href="https://datalab.sspcloud.fr">With keycloakify:</a> </i>
<br>
<img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif">
</p>
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
If you already have a Keycloak custom theme, it can be easily ported to Keycloakify.
---
- [Motivations](#motivations)
- [Requirements](#requirements)
- [My framework doesnt seem to be supported, what can I do?](#my-framework-doesnt-seem-to-be-supported-what-can-i-do)
- [How to use](#how-to-use)
- [Setting up the build tool](#setting-up-the-build-tool)
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
- [Advanced pages configuration](#advanced-pages-configuration)
- [Hot reload](#hot-reload)
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
- [User profile and frontend form validation](#user-profile-and-frontend-form-validation)
- [Support for Terms and conditions](#support-for-terms-and-conditions)
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
- [GitHub Actions](#github-actions)
- [Limitations](#limitations)
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported)
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-the-src-dir)
- [Example of setup that **won't** work](#example-of-setup-that-wont-work)
- [Possible workarounds](#possible-workarounds)
- [Implement context persistence (optional)](#implement-context-persistence-optional)
- [Kickstart video](#kickstart-video)
- [FTL errors related to `ftl_object_to_js_code_declaring_an_object` in Keycloak logs.](#ftl-errors-related-to-ftl_object_to_js_code_declaring_an_object-in-keycloak-logs)
- [Adding custom message (to `i18n/useKcMessage.tsx`)](#adding-custom-message-to-i18nusekcmessagetsx)
- [Email domain whitelist](#email-domain-whitelist)
- [Changelog highlights](#changelog-highlights)
- [v4](#v4)
- [v3](#v3)
- [v2.5](#v25)
- [v2](#v2)
# Requirements
On Windows OS you'll have to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). More info [here](https://github.com/InseeFrLab/keycloakify/issues/54%23issuecomment-984834217)
Tested with the following Keycloak versions:
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
- [15.0.2](https://hub.docker.com/layers/jboss/keycloak/15.0.2/images/sha256-d8ed1ee5df42a178c341f924377da75db49eab08ea9f058ff39a8ed7ee05ec93?context=explore)
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
(before you customize it) will always be the ones of Keycloak v11.
This tool assumes you are bundling your app with Webpack (tested with the versions that ships with CRA v4.44.2 and v5.0.0) .
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
and a `build/static/` directory generated by webpack.
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432)
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
- `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `curl`, `unzip` are assumed to be available.
- `docker` must be up and running when running `start_keycloak_testing_container.sh` (Instructions provided after running `yarn keycloak`).
## My framework doesnt seem to be supported, what can I do?
Currently Keycloakify is only compatible with `create-react-app` apps.
It doesnt mean that you can't use Keycloakify if you are using Next.js, Express or any other
framework that involves SSR but your Keycloak theme will need to be a standalone project.
Find specific instructions about how to get started [**here**](https://github.com/garronej/keycloakify-demo-app#keycloak-theme-only).
To share your styles between your main app and your login pages you will need to externalize your design system by making it a
separate module. Checkout [ts_ci](https://github.com/garronej/ts_ci), it can help with that.
# How to use
## Setting up the build tool
```bash
yarn add keycloakify @emotion/react tss-react powerhooks
```
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
```json
"scripts": {
"keycloak": "yarn build && build-keycloak-theme",
}
```
```bash
yarn keycloak # generates keycloak-theme.jar
```
On the console will be printed all the instructions about how to load the generated theme in Keycloak
### Changing just the look of the default Keycloak theme
The first approach is to only customize the style of the default Keycloak login by providing
your own class names.
If you have created a new React project specifically to create a Keycloak theme and nothing else then
your index should look something like:
`src/index.tsx`
```tsx
import { App } from "./<wherever>/App";
import { KcApp, defaultKcProps, getKcContext } from "keycloakify";
import { css } from "tss-react/@emotion/css";
const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" });
reactDom.render(
<KcApp
kcContext={kcContext}
{...{
...defaultKcProps,
"kcHeaderWrapperClass": myClassName,
}}
/>,
document.getElementById("root"),
);
```
If you share a unique project for your app and the Keycloak theme, your index should look
more like this:
`src/index.tsx`
```tsx
import { App } from "./<wherever>/App";
import { KcApp, defaultKcProps, getKcContext } from "keycloakify";
import { css } from "tss-react/@emotion/css";
const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" });
reactDom.render(
// Unless the app is currently being served by Keycloak
// kcContext is undefined.
kcContext !== undefined ? (
<KcApp
kcContext={kcContext}
{...{
...defaultKcProps,
"kcHeaderWrapperClass": myClassName,
}}
/>
) : (
<App />
), // Your actual app
document.getElementById("root"),
);
```
<p align="center">
<i>result:</i></br>
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
</p>
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx)
(the [index.tsx](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) )
and the result you can expect:
<p align="center">
<i> <a href="https://datalab.sspcloud.fr">Customization using only CSS:</a> </i>
<br>
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
</p>
### Advanced pages configuration
If you want to go beyond only customizing the CSS you can re-implement some of the
pages or even add new ones.
If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel).
If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/ui/components/KcApp).
The web app is in production [here](https://datalab.sspcloud.fr).
Main takeaways are:
- You must declare your custom pages in the package.json. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/package.json#L17-L22)
- (TS only) You must declare theses page in the type argument of the getter
function for the `kcContext` in order to have the correct typings. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L16-L21)
- (TS only) If you use Keycloak plugins that defines non standard `.ftl` values
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
that define `authorizedMailDomains` in `register.ftl`) you should
declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13)
- You should provide sample data for all the non standard value if you want to be able
to debug the page outside of keycloak. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L28-L43)
WARNING: If you chose to go this way use:
```json
"dependencies": {
"keycloakify": "~X.Y.Z"
}
```
in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app.
### Hot reload
Rebuild the theme each time you make a change to see the result is not practical.
If you want to test your login screens outside of Keycloak you can mock a given `kcContext`:
```tsx
import {
KcApp,
defaultKcProps,
getKcContext
} from "keycloakify";
const { kcContext } = getKcContext({
"mockPageId": "login.ftl"
});
reactDom.render(
<KcApp
kcContext={kcContextMocks.kcLoginContext}
{...defaultKcProps}
/>
document.getElementById("root")
);
```
Then `yarn start`, you will see your login page.
Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-app/blob/main/src/index.tsx)
## Enable loading in a blink of an eye of login pages ⚡ (--external-assets)
By default the theme generated is standalone. Meaning that when your users
reach the login pages all scripts, images and stylesheet are downloaded from the Keycloak server.
If you are specifically building a theme to integrate with an app or a website that allows users
to first browse unauthenticated before logging in, you will get a significant
performance boost if you jump through those hoops:
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2) or in a `public/CNAME` file. [ex](https://github.com/garronej/keycloakify-demo-app/blob/main/public/CNAME).
- Build the theme using `npx build-keycloak-theme --external-assets` [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/.github/workflows/ci.yaml#L21)
- Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app.
- Make sure not to build your app and the keycloak theme separately
and remember to update the Keycloak theme every time you update your app.
- Be mindful that if your app is down your login pages are down as well.
Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-app#about-keycloakify)
# User profile and frontend form validation
<p align="center">
<a href="https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/keycloakify_fontend_validation.mp4">
<img src="https://user-images.githubusercontent.com/6702424/138880146-6fef3280-c4a5-46d2-bbb3-8b9598c057a5.gif">
</a>
</p>
NOTE: In reality the regexp used in this gif doesn't work server side, the regexp pattern should be `^[^@]@gmail\.com$` 😬.
User Profile is a Keycloak feature that enables to
[define, from the admin console](https://user-images.githubusercontent.com/6702424/136872461-1f5b64ef-d2ef-4c6b-bb8d-07d4729552b3.png),
what information you want to collect on your users in the register page and to validate inputs
[**on the frontend**, in realtime](https://github.com/InseeFrLab/keycloakify/blob/6dca6a93d8cfe634ee4d8574ad0c091641220092/src/lib/getKcContext/KcContextBase.ts#L225-L261)!
NOTE: User profile is only available in Keycloak 15 and it's a beta feature that
[needs to be enabled when launching keycloak](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/build-keycloak-theme.ts#L116-L117)
and [enabled in the console](https://user-images.githubusercontent.com/6702424/136874428-b071d614-c7f7-440d-9b2e-670faadc0871.png).
Keycloakify, in [`register-user-profile.ftl`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/RegisterUserProfile.tsx),
provides frontend validation out of the box.
For implementing your own `register-user-profile.ftl` page, you can use [`import { useFormValidationSlice } from "keycloakify";`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/useFormValidationSlice.tsx).
Find usage example [`here`](https://github.com/InseeFrLab/keycloakify/blob/d3a07edfcb3739e30032dc96fc2a55944dfc3387/src/lib/components/RegisterUserProfile.tsx#L79-L112).
As for right now [it's not possible to define a pattern for the password](https://keycloak.discourse.group/t/make-password-policies-available-to-freemarker/11632)
from the admin console. You can however pass validators for it to the `useFormValidationSlice` function.
# Support for Terms and conditions
[Many organizations have a requirement that when a new user logs in for the first time, they need to agree to the terms and conditions of the website.](https://www.keycloak.org/docs/4.8/server_admin/#terms-and-conditions).
First you need to enable the required action on the Keycloak server admin console:
![image](https://user-images.githubusercontent.com/6702424/114280501-dad2e600-9a39-11eb-9c39-a225572dd38a.png)
Then to load your own therms of services using [like this](https://github.com/garronej/keycloakify-demo-app/blob/8168c928a66605f2464f9bd28a4dc85fb0a231f9/src/index.tsx#L42-L66).
# Some pages still have the default theme. Why?
This project only support out of the box the most common user facing pages of Keycloak login.
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
If you need to customize pages that are not supported yet or if you need to implement some non standard `.ftl` pages please refer to [Advanced pages configuration](#advanced-pages-configuration).
# GitHub Actions
![image](https://user-images.githubusercontent.com/6702424/114286938-47aea600-9a63-11eb-936e-17159e8826e8.png)
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
the building and publishing of the theme (the .jar file).
# Limitations
## `process.env.PUBLIC_URL` not supported.
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
(This isn't recommended anyway).
## `@font-face` importing fonts from the `src/` dir
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
this limitation doesn't apply, you can import fonts however you see fit.
### Example of setup that **won't** work
- We have a `fonts/` directory in `src/`
- We import the font like this [`src: url("/fonts/my-font.woff2") format("woff2");`](https://github.com/garronej/keycloakify-demo-app/blob/07d54a3012ef354ee12b1374c6f7ad1cb125d56b/src/fonts.scss#L4) in a `.scss` a file.
### Possible workarounds
- Use [`--external-assets`](#enable-loading-in-a-blink-of-a-eye-of-login-pages-).
- If it is possible, use Google Fonts or any other font provider.
- If you want to host your font recommended approach is to move your fonts into the `public`
directory and to place your `@font-face` statements in the `public/index.html`.
Example [here](https://github.com/garronej/keycloakify-demo-app/blob/9aa2dbaec28a7786d6b2983c9a59d393dec1b2d6/public/index.html#L27-L73)
(and the font are [here](https://github.com/garronej/keycloakify-demo-app/tree/main/public/fonts/WorkSans)).
- You can also [use non relative url](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/src/fonts.scss#L16) but don't forget [`Access-Control-Allow-Origin`](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/nginx.conf#L17-L19).
# Implement context persistence (optional)
If, before logging in, a user has selected a specific language
you don't want it to be reset to default when the user gets redirected to
the login or register pages.
Same goes for the dark mode, you don't want, if the user had it enabled
to show the login page with light themes.
The problem is that you are probably using `localStorage` to persist theses values across
reload but, as the Keycloak pages are not served on the same domain that the rest of your
app you won't be able to carry over states using `localStorage`.
The only reliable solution is to inject parameters into the URL before
redirecting to Keycloak. We integrate with
[`keycloak-js`](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc),
by providing you a way to tell `keycloak-js` that you would like to inject
some search parameters before redirecting.
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
You can implement your own mechanism to pass the states in the URL and
restore it on the other side but we recommend using `powerhooks/useGlobalState`
from the library [`powerhooks`](https://www.powerhooks.dev) that provide an elegant
way to handle states such as `isDarkModeEnabled` or `selectedLanguage`.
Let's modify [the example](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc) from the official `keycloak-js` documentation to
enables the states of `useGlobalStates` to be injected in the URL before redirecting.
Note that the states are automatically restored on the other side by `powerhooks`
```typescript
import keycloak_js from "keycloak-js";
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
import { createKeycloakAdapter } from "keycloakify";
//...
const keycloakInstance = keycloak_js({
"url": "http://keycloak-server/auth",
"realm": "myrealm",
"clientId": "myapp",
});
keycloakInstance.init({
"onLoad": "check-sso",
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html",
"adapter": createKeycloakAdapter({
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
keycloakInstance,
}),
});
//...
```
If you really want to go the extra miles and avoid having the white
flash of the blank html before the js bundle have been evaluated
[here is a snippet](https://github.com/InseeFrLab/onyxia-web/blob/e1c1f309aaa3d5f860df39ba0b75cce89c88a9de/public/index.html#L117-L166) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
# Kickstart video
_NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded_
[![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8)
# FTL errors related to `ftl_object_to_js_code_declaring_an_object` in Keycloak logs.
If you ever encounter one of these errors:
```log
FTL stack trace ("~" means nesting-related):
- Failed at: #local value = object[key] [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 70, column 21]
- Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
- Reached through: @ftl_object_to_js_code_declaring_an_object object=value depth=(dep... [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 81, column 27]
- Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
- Reached through: @ftl_object_to_js_code_declaring_an_object object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
```
It's just noise, they can be safely ignored.
You can, however, and are encouraged to, report any that you would spot.
Just open an issue about it and I will release a patched version of Keycloakify in the better delays.
# Adding custom message (to `i18n/useKcMessage.tsx`)
You can reproduce [this approach](https://github.com/garronej/keycloakify-demo-app/blob/main/src/kcMessagesExtension.ts)
( don't forget to [evaluate the code](https://github.com/garronej/keycloakify-demo-app/blob/0a6d349dba89a5702f98ba48bca6c76ac7265e1f/src/index.tsx#L15) ).
This approach is a bit hacky as it doesn't provide type safety but it works.
# Email domain whitelist
NOTE: This have been kind of deprecated by [user attribute](#user-profile-and-frontend-form-validation) you could
use a pattern [like this one](https://github.com/InseeFrLab/onyxia-web/blob/f1206e0329b3b8d401ca7bffa95ca9c213cb190a/src/app/components/KcApp/kcContext.ts#L106) to whitelist email domains.
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
and `kcRegisterContext["authorizedMailDomains"]` to validate on.
# Changelog highlights
## v4.10.0
Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/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/InseeFrLab/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.
> **New in 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

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "5.0.0",
"version": "4.5.2-beta.0",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -22,7 +22,6 @@
},
"bin": {
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
"create-keycloak-theme-email-directory": "dist/bin/create-keycloak-theme-email-directory.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
},
"lint-staged": {
@ -57,34 +56,32 @@
"homepage": "https://github.com/garronej/keycloakify",
"peerDependencies": {
"@emotion/react": "^11.4.1",
"react": "^16.8.0 || ^17.0.0"
"powerhooks": "^0.10.0",
"react": "^16.8.0 || ^17.0.0",
"tss-react": "^1.1.0 || ^3.0.0"
},
"devDependencies": {
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/node": "^17.0.25",
"@types/node": "^10.0.0",
"@types/react": "^17.0.0",
"copyfiles": "^2.4.1",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"powerhooks": "^0.11.0",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "^17.0.1",
"rimraf": "^3.0.2",
"tss-react": "^3.0.0",
"typescript": "^4.2.3"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "2.0.0-beta.39",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.1",
"path-browserify": "^1.0.1",
"powerhooks": "^0.14.0",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tsafe": "^0.9.0",
"tss-react": "^3.5.2"
"tsafe": "^0.9.0"
}
}

View File

@ -0,0 +1,3 @@
export const keycloakVersions = ["11.0.3", "15.0.2"] as const;
export type KeycloakVersion = typeof keycloakVersions[number];

View File

@ -2,7 +2,7 @@ import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
import { URL } from "url";
import * as fs from "fs";
@ -19,7 +19,6 @@ const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-asset
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_theme_email");
function sanitizeThemeName(name: string) {
return name
@ -35,9 +34,8 @@ export function main() {
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
const themeName = sanitizeThemeName(parsedPackageJson.name);
const { doBundleEmailTemplate } = generateKeycloakThemeResources({
generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
themeName,
...(() => {
@ -74,37 +72,33 @@ export function main() {
extraPagesId,
extraThemeProperties,
//We have to leave it at that otherwise we break our default theme.
//Problem is that we can't guarantee that the the old resources
//Problem is that we can't guarantee that the the old resources common
//will still be available on the newer keycloak version.
"keycloakVersion": "11.0.3",
});
const { jarFilePath } = generateJavaStackFiles({
"version": parsedPackageJson.version,
version: parsedPackageJson.version,
themeName,
"homepage": parsedPackageJson.homepage,
homepage: parsedPackageJson.homepage,
keycloakThemeBuildingDirPath,
doBundleEmailTemplate,
});
child_process.execSync("mvn package", {
"cwd": keycloakThemeBuildingDirPath,
});
//We want, however to test in a container running the latest Keycloak version
const containerKeycloakVersion = "18.0.0";
generateStartKeycloakTestingContainer({
generateDebugFiles({
keycloakThemeBuildingDirPath,
themeName,
"keycloakVersion": containerKeycloakVersion,
"keycloakVersion": "15.0.2",
});
console.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
`It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image.`,
"",
"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
"",
@ -124,7 +118,7 @@ export function main() {
" ",
" extraVolumeMounts: |",
" - name: extensions",
" mountPath: /opt/keycloak/providers",
" mountPath: /opt/jboss/keycloak/standalone/deployments",
" extraEnv: |",
" - name: KEYCLOAK_USER",
" value: admin",
@ -134,16 +128,14 @@ export function main() {
" 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:`,
"To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:",
"",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename))} 👈`,
"",
"Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename))} 👈`,
"",
"Once your container is up and running: ",
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
"- Log into the admin console 👉 http://localhost:8080 username: admin, password: admin 👈",
'- Create a realm named "myrealm"',
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
'- Create a client with id "myclient" and root url: "https://www.keycloak.org/app/"',
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
"",

View File

@ -0,0 +1,84 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
/** Files for being able to run a hot reload keycloak container */
export function generateDebugFiles(params: { keycloakVersion: "11.0.3" | "15.0.2"; themeName: string; keycloakThemeBuildingDirPath: string }) {
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
Buffer.from(
[
`FROM jboss/keycloak:${keycloakVersion}`,
"",
"USER root",
"",
"WORKDIR /",
"",
"ADD configuration /opt/jboss/keycloak/standalone/configuration/",
"",
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
].join("\n"),
"utf8",
),
);
const dockerImage = `${themeName}/keycloak-hot-reload`;
const containerName = "keycloak-testing-container";
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename),
Buffer.from(
[
"#!/bin/bash",
"",
`cd ${keycloakThemeBuildingDirPath}`,
"",
`docker rm ${containerName} || true`,
"",
`docker build . -t ${dockerImage}`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_USER=admin \\",
" -e KEYCLOAK_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v ${pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName,
)}:/opt/jboss/keycloak/themes/${themeName}:rw \\`,
` -it ${dockerImage}:latest`,
"",
].join("\n"),
"utf8",
),
{ "mode": 0o755 },
);
const standaloneHaFilePath = pathJoin(keycloakThemeBuildingDirPath, "configuration", `standalone-ha.xml`);
try {
fs.mkdirSync(pathDirname(standaloneHaFilePath));
} catch {}
fs.writeFileSync(
standaloneHaFilePath,
fs
.readFileSync(pathJoin(__dirname, `standalone-ha_${keycloakVersion}.xml`))
.toString("utf8")
.replace(
new RegExp(
["<staticMaxAge>2592000</staticMaxAge>", "<cacheThemes>true</cacheThemes>", "<cacheTemplates>true</cacheTemplates>"].join("\\s*"),
"g",
),
["<staticMaxAge>-1</staticMaxAge>", "<cacheThemes>false</cacheThemes>", "<cacheTemplates>false</cacheTemplates>"].join("\n"),
),
);
}

View File

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

View File

@ -0,0 +1,666 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns="urn:jboss:domain:13.0">
<extensions>
<extension module="org.jboss.as.clustering.infinispan"/>
<extension module="org.jboss.as.clustering.jgroups"/>
<extension module="org.jboss.as.connector"/>
<extension module="org.jboss.as.deployment-scanner"/>
<extension module="org.jboss.as.ee"/>
<extension module="org.jboss.as.ejb3"/>
<extension module="org.jboss.as.jaxrs"/>
<extension module="org.jboss.as.jmx"/>
<extension module="org.jboss.as.jpa"/>
<extension module="org.jboss.as.logging"/>
<extension module="org.jboss.as.mail"/>
<extension module="org.jboss.as.modcluster"/>
<extension module="org.jboss.as.naming"/>
<extension module="org.jboss.as.remoting"/>
<extension module="org.jboss.as.security"/>
<extension module="org.jboss.as.transactions"/>
<extension module="org.jboss.as.weld"/>
<extension module="org.keycloak.keycloak-server-subsystem"/>
<extension module="org.wildfly.extension.bean-validation"/>
<extension module="org.wildfly.extension.core-management"/>
<extension module="org.wildfly.extension.elytron"/>
<extension module="org.wildfly.extension.io"/>
<extension module="org.wildfly.extension.microprofile.config-smallrye"/>
<extension module="org.wildfly.extension.microprofile.health-smallrye"/>
<extension module="org.wildfly.extension.microprofile.metrics-smallrye"/>
<extension module="org.wildfly.extension.request-controller"/>
<extension module="org.wildfly.extension.security.manager"/>
<extension module="org.wildfly.extension.undertow"/>
</extensions>
<management>
<security-realms>
<security-realm name="ManagementRealm">
<authentication>
<local default-user="$local" skip-group-loading="true"/>
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
</authentication>
<authorization map-groups-to-roles="false">
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
</authorization>
</security-realm>
<security-realm name="ApplicationRealm">
<server-identities>
<ssl>
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
</ssl>
</server-identities>
<authentication>
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
</authentication>
<authorization>
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
</authorization>
</security-realm>
</security-realms>
<audit-log>
<formatters>
<json-formatter name="json-formatter"/>
</formatters>
<handlers>
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
</handlers>
<logger log-boot="true" log-read-only="false" enabled="false">
<handlers>
<handler name="file"/>
</handlers>
</logger>
</audit-log>
<management-interfaces>
<http-interface security-realm="ManagementRealm">
<http-upgrade enabled="true"/>
<socket-binding http="management-http"/>
</http-interface>
</management-interfaces>
<access-control provider="simple">
<role-mapping>
<role name="SuperUser">
<include>
<user name="$local"/>
</include>
</role>
</role-mapping>
</access-control>
</management>
<profile>
<subsystem xmlns="urn:jboss:domain:logging:8.0">
<console-handler name="CONSOLE">
<formatter>
<named-formatter name="COLOR-PATTERN"/>
</formatter>
</console-handler>
<logger category="com.arjuna">
<level name="WARN"/>
</logger>
<logger category="io.jaegertracing.Configuration">
<level name="WARN"/>
</logger>
<logger category="org.jboss.as.config">
<level name="DEBUG"/>
</logger>
<logger category="sun.rmi">
<level name="WARN"/>
</logger>
<logger category="org.keycloak">
<level name="${env.KEYCLOAK_LOGLEVEL:INFO}"/>
</logger>
<root-logger>
<level name="${env.ROOT_LOGLEVEL:INFO}"/>
<handlers>
<handler name="CONSOLE"/>
</handlers>
</root-logger>
<formatter name="PATTERN">
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
<formatter name="COLOR-PATTERN">
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
</subsystem>
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
<driver>h2</driver>
<pool>
<max-pool-size>100</max-pool-size>
</pool>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ee:5.0">
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
<concurrent>
<context-services>
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
</context-services>
<managed-thread-factories>
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
</managed-thread-factories>
<managed-executor-services>
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-threshold="60000" keepalive-time="5000"/>
</managed-executor-services>
<managed-scheduled-executor-services>
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-threshold="60000" keepalive-time="3000"/>
</managed-scheduled-executor-services>
</concurrent>
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ejb3:7.0">
<session-bean>
<stateless>
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
</stateless>
<stateful default-access-timeout="5000" cache-ref="distributable" passivation-disabled-cache-ref="simple"/>
<singleton default-access-timeout="5000"/>
</session-bean>
<pools>
<bean-instance-pools>
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
</bean-instance-pools>
</pools>
<caches>
<cache name="simple"/>
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
</caches>
<passivation-stores>
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
</passivation-stores>
<async thread-pool-name="default"/>
<timer-service thread-pool-name="default" default-data-store="default-file-store">
<data-stores>
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
</data-stores>
</timer-service>
<remote connector-ref="http-remoting-connector" thread-pool-name="default">
<channel-creation-options>
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
</channel-creation-options>
</remote>
<thread-pools>
<thread-pool name="default">
<max-threads count="10"/>
<keepalive-time time="60" unit="seconds"/>
</thread-pool>
</thread-pools>
<default-security-domain value="other"/>
<default-missing-method-permissions-deny-access value="true"/>
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<log-system-exceptions value="true"/>
</subsystem>
<subsystem xmlns="urn:wildfly:elytron:10.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
<providers>
<aggregate-providers name="combined-providers">
<providers name="elytron"/>
<providers name="openssl"/>
</aggregate-providers>
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
<provider-loader name="openssl" module="org.wildfly.openssl"/>
</providers>
<audit-logging>
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
</audit-logging>
<security-domains>
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
<realm name="local"/>
</security-domain>
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
<realm name="local" role-mapper="super-user-mapper"/>
</security-domain>
</security-domains>
<security-realms>
<identity-realm name="local" identity="$local"/>
<properties-realm name="ApplicationRealm">
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
<properties-realm name="ManagementRealm">
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
</security-realms>
<mappers>
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
<permission-mapping>
<principal name="anonymous"/>
<permission-set name="default-permissions"/>
</permission-mapping>
<permission-mapping match-all="true">
<permission-set name="login-permission"/>
<permission-set name="default-permissions"/>
</permission-mapping>
</simple-permission-mapper>
<constant-realm-mapper name="local" realm-name="local"/>
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
<constant-role-mapper name="super-user-mapper">
<role name="SuperUser"/>
</constant-role-mapper>
</mappers>
<permission-sets>
<permission-set name="login-permission">
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
</permission-set>
<permission-set name="default-permissions">
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
</permission-set>
</permission-sets>
<http>
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="DIGEST">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<provider-http-server-mechanism-factory name="global"/>
</http>
<sasl>
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ApplicationRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
<properties>
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
</properties>
</configurable-sasl-server-factory>
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
<filters>
<filter provider-name="WildFlyElytron"/>
</filters>
</mechanism-provider-filtering-sasl-server-factory>
<provider-sasl-server-factory name="global"/>
</sasl>
</subsystem>
<subsystem xmlns="urn:jboss:domain:infinispan:10.0">
<cache-container name="keycloak" module="org.keycloak.keycloak-model-infinispan">
<transport lock-timeout="60000"/>
<local-cache name="realms">
<object-memory size="10000"/>
</local-cache>
<local-cache name="users">
<object-memory size="10000"/>
</local-cache>
<local-cache name="authorization">
<object-memory size="10000"/>
</local-cache>
<local-cache name="keys">
<object-memory size="1000"/>
<expiration max-idle="3600000"/>
</local-cache>
<replicated-cache name="work"/>
<distributed-cache name="sessions" owners="1"/>
<distributed-cache name="authenticationSessions" owners="1"/>
<distributed-cache name="offlineSessions" owners="1"/>
<distributed-cache name="clientSessions" owners="1"/>
<distributed-cache name="offlineClientSessions" owners="1"/>
<distributed-cache name="loginFailures" owners="1"/>
<distributed-cache name="actionTokens" owners="2">
<object-memory size="-1"/>
<expiration interval="300000" max-idle="-1"/>
</distributed-cache>
</cache-container>
<cache-container name="server" aliases="singleton cluster" default-cache="default" module="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>
<replicated-cache name="default">
<transaction mode="BATCH"/>
</replicated-cache>
</cache-container>
<cache-container name="web" default-cache="dist" module="org.wildfly.clustering.web.infinispan">
<transport lock-timeout="60000"/>
<replicated-cache name="sso">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
</replicated-cache>
<distributed-cache name="dist">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
<distributed-cache name="routing"/>
</cache-container>
<cache-container name="ejb" aliases="sfsb" default-cache="dist" module="org.wildfly.clustering.ejb.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
</cache-container>
<cache-container name="hibernate" module="org.infinispan.hibernate-cache">
<transport lock-timeout="60000"/>
<local-cache name="local-query">
<object-memory size="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<invalidation-cache name="entity">
<transaction mode="NON_XA"/>
<object-memory size="10000"/>
<expiration max-idle="100000"/>
</invalidation-cache>
<replicated-cache name="timestamps"/>
</cache-container>
</subsystem>
<subsystem xmlns="urn:jboss:domain:io:3.0">
<worker name="default"/>
<buffer-pool name="default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
<subsystem xmlns="urn:jboss:domain:jca:5.0">
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
<bean-validation enabled="true"/>
<default-workmanager>
<short-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</short-running-threads>
<long-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</long-running-threads>
</default-workmanager>
<cached-connection-manager/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jgroups:8.0">
<channels default="ee">
<channel name="ee" stack="udp" cluster="ejb"/>
</channels>
<stacks>
<stack name="udp">
<transport type="UDP" socket-binding="jgroups-udp"/>
<protocol type="PING"/>
<protocol type="MERGE3"/>
<socket-protocol type="FD_SOCK" socket-binding="jgroups-udp-fd"/>
<protocol type="FD_ALL"/>
<protocol type="VERIFY_SUSPECT"/>
<protocol type="pbcast.NAKACK2"/>
<protocol type="UNICAST3"/>
<protocol type="pbcast.STABLE"/>
<protocol type="pbcast.GMS"/>
<protocol type="UFC"/>
<protocol type="MFC"/>
<protocol type="FRAG3"/>
</stack>
<stack name="tcp">
<transport type="TCP" socket-binding="jgroups-tcp"/>
<socket-protocol type="MPING" socket-binding="jgroups-mping"/>
<protocol type="MERGE3"/>
<socket-protocol type="FD_SOCK" socket-binding="jgroups-tcp-fd"/>
<protocol type="FD_ALL"/>
<protocol type="VERIFY_SUSPECT"/>
<protocol type="pbcast.NAKACK2"/>
<protocol type="UNICAST3"/>
<protocol type="pbcast.STABLE"/>
<protocol type="pbcast.GMS"/>
<protocol type="MFC"/>
<protocol type="FRAG3"/>
</stack>
</stacks>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
<expose-resolved-model/>
<expose-expression-model/>
<remoting-connector/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
<jpa default-datasource="" default-extended-persistence-inheritance="DEEP"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
<web-context>auth</web-context>
<providers>
<provider>
classpath:${jboss.home.dir}/providers/*
</provider>
</providers>
<master-realm-name>master</master-realm-name>
<scheduled-task-interval>900</scheduled-task-interval>
<theme>
<staticMaxAge>2592000</staticMaxAge>
<cacheThemes>true</cacheThemes>
<cacheTemplates>true</cacheTemplates>
<welcomeTheme>${env.KEYCLOAK_WELCOME_THEME:keycloak}</welcomeTheme>
<default>${env.KEYCLOAK_DEFAULT_THEME:keycloak}</default>
<dir>${jboss.home.dir}/themes</dir>
</theme>
<spi name="eventsStore">
<provider name="jpa" enabled="true">
<properties>
<property name="exclude-events" value="[&quot;REFRESH_TOKEN&quot;]"/>
</properties>
</provider>
</spi>
<spi name="userCache">
<provider name="default" enabled="true"/>
</spi>
<spi name="userSessionPersister">
<default-provider>jpa</default-provider>
</spi>
<spi name="timer">
<default-provider>basic</default-provider>
</spi>
<spi name="connectionsHttpClient">
<provider name="default" enabled="true"/>
</spi>
<spi name="connectionsJpa">
<provider name="default" enabled="true">
<properties>
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
<property name="initializeEmpty" value="true"/>
<property name="migrationStrategy" value="update"/>
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
</properties>
</provider>
</spi>
<spi name="realmCache">
<provider name="default" enabled="true"/>
</spi>
<spi name="connectionsInfinispan">
<default-provider>default</default-provider>
<provider name="default" enabled="true">
<properties>
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
</properties>
</provider>
</spi>
<spi name="jta-lookup">
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
<provider name="jboss" enabled="true"/>
</spi>
<spi name="publicKeyStorage">
<provider name="infinispan" enabled="true">
<properties>
<property name="minTimeBetweenRequests" value="10"/>
</properties>
</provider>
</spi>
<spi name="x509cert-lookup">
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
<provider name="default" enabled="true"/>
</spi>
<spi name="hostname">
<default-provider>${keycloak.hostname.provider:default}</default-provider>
<provider name="default" enabled="true">
<properties>
<property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
<property name="forceBackendUrlToFrontendUrl" value="false"/>
</properties>
</provider>
<provider name="fixed" enabled="true">
<properties>
<property name="hostname" value="${keycloak.hostname.fixed.hostname:localhost}"/>
<property name="httpPort" value="${keycloak.hostname.fixed.httpPort:-1}"/>
<property name="httpsPort" value="${keycloak.hostname.fixed.httpsPort:-1}"/>
<property name="alwaysHttps" value="${keycloak.hostname.fixed.alwaysHttps:false}"/>
</properties>
</provider>
</spi>
</subsystem>
<subsystem xmlns="urn:jboss:domain:mail:4.0">
<mail-session name="default" jndi-name="java:jboss/mail/Default">
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
</mail-session>
</subsystem>
<subsystem xmlns="urn:wildfly:microprofile-config-smallrye:1.0"/>
<subsystem xmlns="urn:wildfly:microprofile-health-smallrye:2.0" security-enabled="false" empty-liveness-checks-status="${env.MP_HEALTH_EMPTY_LIVENESS_CHECKS_STATUS:UP}" empty-readiness-checks-status="${env.MP_HEALTH_EMPTY_READINESS_CHECKS_STATUS:UP}"/>
<subsystem xmlns="urn:wildfly:microprofile-metrics-smallrye:2.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
<proxy name="default" advertise-socket="modcluster" listener="ajp">
<dynamic-load-provider>
<load-metric type="cpu"/>
</dynamic-load-provider>
</proxy>
</subsystem>
<subsystem xmlns="urn:jboss:domain:naming:2.0">
<remote-naming/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
<subsystem xmlns="urn:jboss:domain:security:2.0">
<security-domains>
<security-domain name="other" cache-type="default">
<authentication>
<login-module code="Remoting" flag="optional">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
<login-module code="RealmDirect" flag="required">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
</authentication>
</security-domain>
<security-domain name="jboss-web-policy" cache-type="default">
<authorization>
<policy-module code="Delegating" flag="required"/>
</authorization>
</security-domain>
<security-domain name="jaspitest" cache-type="default">
<authentication-jaspi>
<login-module-stack name="dummy">
<login-module code="Dummy" flag="optional"/>
</login-module-stack>
<auth-module code="Dummy"/>
</authentication-jaspi>
</security-domain>
<security-domain name="jboss-ejb-policy" cache-type="default">
<authorization>
<policy-module code="Delegating" flag="required"/>
</authorization>
</security-domain>
</security-domains>
</subsystem>
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
<deployment-permissions>
<maximum-set>
<permission class="java.security.AllPermission"/>
</maximum-set>
</deployment-permissions>
</subsystem>
<subsystem xmlns="urn:jboss:domain:transactions:5.0">
<core-environment node-identifier="${jboss.tx.node.id:1}">
<process-id>
<uuid/>
</process-id>
</core-environment>
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:undertow:11.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
<buffer-cache name="default"/>
<server name="default-server">
<ajp-listener name="ajp" socket-binding="ajp"/>
<http-listener name="default" read-timeout="30000" socket-binding="http" redirect-socket="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
<https-listener name="https" read-timeout="30000" socket-binding="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" security-realm="ApplicationRealm" enable-http2="true"/>
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<http-invoker security-realm="ApplicationRealm"/>
</host>
</server>
<servlet-container name="default">
<jsp-config/>
<websockets/>
</servlet-container>
<handlers>
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers>
</subsystem>
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
</profile>
<interfaces>
<interface name="management">
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
</interface>
<interface name="private">
<inet-address value="${jboss.bind.address.private:127.0.0.1}"/>
</interface>
<interface name="public">
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
</interface>
</interfaces>
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
<socket-binding name="http" port="${jboss.http.port:8080}"/>
<socket-binding name="https" port="${jboss.https.port:8443}"/>
<socket-binding name="jgroups-mping" interface="private" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
<socket-binding name="jgroups-tcp" interface="private" port="7600"/>
<socket-binding name="jgroups-tcp-fd" interface="private" port="57600"/>
<socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>
<socket-binding name="jgroups-udp-fd" interface="private" port="54200"/>
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
<socket-binding name="txn-recovery-environment" port="4712"/>
<socket-binding name="txn-status-manager" port="4713"/>
<outbound-socket-binding name="mail-smtp">
<remote-destination host="localhost" port="25"/>
</outbound-socket-binding>
</socket-binding-group>
</server>

View File

@ -0,0 +1,693 @@
<?xml version='1.0' encoding='UTF-8'?>
<server xmlns="urn:jboss:domain:16.0">
<extensions>
<extension module="org.jboss.as.clustering.infinispan"/>
<extension module="org.jboss.as.clustering.jgroups"/>
<extension module="org.jboss.as.connector"/>
<extension module="org.jboss.as.deployment-scanner"/>
<extension module="org.jboss.as.ee"/>
<extension module="org.jboss.as.ejb3"/>
<extension module="org.jboss.as.jaxrs"/>
<extension module="org.jboss.as.jmx"/>
<extension module="org.jboss.as.jpa"/>
<extension module="org.jboss.as.logging"/>
<extension module="org.jboss.as.mail"/>
<extension module="org.jboss.as.modcluster"/>
<extension module="org.jboss.as.naming"/>
<extension module="org.jboss.as.remoting"/>
<extension module="org.jboss.as.security"/>
<extension module="org.jboss.as.transactions"/>
<extension module="org.jboss.as.weld"/>
<extension module="org.keycloak.keycloak-server-subsystem"/>
<extension module="org.wildfly.extension.bean-validation"/>
<extension module="org.wildfly.extension.core-management"/>
<extension module="org.wildfly.extension.elytron"/>
<extension module="org.wildfly.extension.health"/>
<extension module="org.wildfly.extension.io"/>
<extension module="org.wildfly.extension.metrics"/>
<extension module="org.wildfly.extension.request-controller"/>
<extension module="org.wildfly.extension.security.manager"/>
<extension module="org.wildfly.extension.undertow"/>
</extensions>
<management>
<security-realms>
<security-realm name="ManagementRealm">
<authentication>
<local default-user="$local" skip-group-loading="true"/>
<properties path="mgmt-users.properties" relative-to="jboss.server.config.dir"/>
</authentication>
<authorization map-groups-to-roles="false">
<properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
</authorization>
</security-realm>
<security-realm name="ApplicationRealm">
<server-identities>
<ssl>
<keystore path="application.keystore" relative-to="jboss.server.config.dir" keystore-password="password" alias="server" key-password="password" generate-self-signed-certificate-host="localhost"/>
</ssl>
</server-identities>
<authentication>
<local default-user="$local" allowed-users="*" skip-group-loading="true"/>
<properties path="application-users.properties" relative-to="jboss.server.config.dir"/>
</authentication>
<authorization>
<properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
</authorization>
</security-realm>
</security-realms>
<audit-log>
<formatters>
<json-formatter name="json-formatter"/>
</formatters>
<handlers>
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
</handlers>
<logger log-boot="true" log-read-only="false" enabled="false">
<handlers>
<handler name="file"/>
</handlers>
</logger>
</audit-log>
<management-interfaces>
<http-interface security-realm="ManagementRealm">
<http-upgrade enabled="true"/>
<socket-binding http="management-http"/>
</http-interface>
</management-interfaces>
<access-control provider="simple">
<role-mapping>
<role name="SuperUser">
<include>
<user name="$local"/>
</include>
</role>
</role-mapping>
</access-control>
</management>
<profile>
<subsystem xmlns="urn:jboss:domain:logging:8.0">
<console-handler name="CONSOLE">
<formatter>
<named-formatter name="COLOR-PATTERN"/>
</formatter>
</console-handler>
<logger category="com.arjuna">
<level name="WARN"/>
</logger>
<logger category="io.jaegertracing.Configuration">
<level name="WARN"/>
</logger>
<logger category="org.jboss.as.config">
<level name="DEBUG"/>
</logger>
<logger category="sun.rmi">
<level name="WARN"/>
</logger>
<logger category="org.keycloak">
<level name="${env.KEYCLOAK_LOGLEVEL:INFO}"/>
</logger>
<root-logger>
<level name="${env.ROOT_LOGLEVEL:INFO}"/>
<handlers>
<handler name="CONSOLE"/>
</handlers>
</root-logger>
<formatter name="PATTERN">
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
<formatter name="COLOR-PATTERN">
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
</formatter>
</subsystem>
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
</subsystem>
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ee:6.0">
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
<concurrent>
<context-services>
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
</context-services>
<managed-thread-factories>
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
</managed-thread-factories>
<managed-executor-services>
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="5000"/>
</managed-executor-services>
<managed-scheduled-executor-services>
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="3000"/>
</managed-scheduled-executor-services>
</concurrent>
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:ejb3:9.0">
<session-bean>
<stateless>
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
</stateless>
<stateful default-access-timeout="5000" cache-ref="distributable" passivation-disabled-cache-ref="simple"/>
<singleton default-access-timeout="5000"/>
</session-bean>
<pools>
<bean-instance-pools>
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
</bean-instance-pools>
</pools>
<caches>
<cache name="simple"/>
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
</caches>
<passivation-stores>
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
</passivation-stores>
<async thread-pool-name="default"/>
<timer-service thread-pool-name="default" default-data-store="default-file-store">
<data-stores>
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
</data-stores>
</timer-service>
<remote cluster="ejb" connectors="http-remoting-connector" thread-pool-name="default">
<channel-creation-options>
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
</channel-creation-options>
</remote>
<thread-pools>
<thread-pool name="default">
<max-threads count="10"/>
<keepalive-time time="60" unit="seconds"/>
</thread-pool>
</thread-pools>
<default-security-domain value="other"/>
<default-missing-method-permissions-deny-access value="true"/>
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<log-system-exceptions value="true"/>
</subsystem>
<subsystem xmlns="urn:wildfly:elytron:13.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
<providers>
<aggregate-providers name="combined-providers">
<providers name="elytron"/>
<providers name="openssl"/>
</aggregate-providers>
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
<provider-loader name="openssl" module="org.wildfly.openssl"/>
</providers>
<audit-logging>
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
</audit-logging>
<security-domains>
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
<realm name="local"/>
</security-domain>
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
<realm name="local" role-mapper="super-user-mapper"/>
</security-domain>
</security-domains>
<security-realms>
<identity-realm name="local" identity="$local"/>
<properties-realm name="ApplicationRealm">
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
<properties-realm name="ManagementRealm">
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
</properties-realm>
</security-realms>
<mappers>
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
<permission-mapping>
<principal name="anonymous"/>
<permission-set name="default-permissions"/>
</permission-mapping>
<permission-mapping match-all="true">
<permission-set name="login-permission"/>
<permission-set name="default-permissions"/>
</permission-mapping>
</simple-permission-mapper>
<constant-realm-mapper name="local" realm-name="local"/>
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
<constant-role-mapper name="super-user-mapper">
<role name="SuperUser"/>
</constant-role-mapper>
</mappers>
<permission-sets>
<permission-set name="login-permission">
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
</permission-set>
<permission-set name="default-permissions">
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
</permission-set>
</permission-sets>
<http>
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
<mechanism-configuration>
<mechanism mechanism-name="DIGEST">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</http-authentication-factory>
<provider-http-server-mechanism-factory name="global"/>
</http>
<sasl>
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ApplicationRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
<mechanism-configuration>
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
<mechanism mechanism-name="DIGEST-MD5">
<mechanism-realm realm-name="ManagementRealm"/>
</mechanism>
</mechanism-configuration>
</sasl-authentication-factory>
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
<properties>
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
</properties>
</configurable-sasl-server-factory>
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
<filters>
<filter provider-name="WildFlyElytron"/>
</filters>
</mechanism-provider-filtering-sasl-server-factory>
<provider-sasl-server-factory name="global"/>
</sasl>
<tls>
<key-stores>
<key-store name="applicationKS">
<credential-reference clear-text="password"/>
<implementation type="JKS"/>
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
</key-store>
</key-stores>
<key-managers>
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
<credential-reference clear-text="password"/>
</key-manager>
</key-managers>
<server-ssl-contexts>
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
</server-ssl-contexts>
</tls>
</subsystem>
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
<subsystem xmlns="urn:jboss:domain:infinispan:12.0">
<cache-container name="ejb" default-cache="dist" aliases="sfsb" modules="org.wildfly.clustering.ejb.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
</cache-container>
<cache-container name="keycloak" modules="org.keycloak.keycloak-model-infinispan">
<transport lock-timeout="60000"/>
<local-cache name="realms">
<heap-memory size="10000"/>
</local-cache>
<local-cache name="users">
<heap-memory size="10000"/>
</local-cache>
<local-cache name="authorization">
<heap-memory size="10000"/>
</local-cache>
<local-cache name="keys">
<heap-memory size="1000"/>
<expiration max-idle="3600000"/>
</local-cache>
<replicated-cache name="work">
<expiration lifespan="900000000000000000"/>
</replicated-cache>
<distributed-cache name="sessions" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="clientSessions" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="loginFailures" owners="1">
<expiration lifespan="900000000000000000"/>
</distributed-cache>
<distributed-cache name="actionTokens" owners="2">
<heap-memory size="-1"/>
<expiration interval="300000" lifespan="900000000000000000" max-idle="-1"/>
</distributed-cache>
</cache-container>
<cache-container name="server" default-cache="default" aliases="singleton cluster" modules="org.wildfly.clustering.server">
<transport lock-timeout="60000"/>
<replicated-cache name="default">
<transaction mode="BATCH"/>
</replicated-cache>
</cache-container>
<cache-container name="web" default-cache="dist" modules="org.wildfly.clustering.web.infinispan">
<transport lock-timeout="60000"/>
<replicated-cache name="sso">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
</replicated-cache>
<distributed-cache name="dist">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
<distributed-cache name="routing"/>
</cache-container>
<cache-container name="hibernate" modules="org.infinispan.hibernate-cache">
<transport lock-timeout="60000"/>
<local-cache name="local-query">
<heap-memory size="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<invalidation-cache name="entity">
<transaction mode="NON_XA"/>
<heap-memory size="10000"/>
<expiration max-idle="100000"/>
</invalidation-cache>
<replicated-cache name="timestamps"/>
</cache-container>
</subsystem>
<subsystem xmlns="urn:jboss:domain:io:3.0">
<worker name="default"/>
<buffer-pool name="default"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
<subsystem xmlns="urn:jboss:domain:jca:5.0">
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
<bean-validation enabled="true"/>
<default-workmanager>
<short-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</short-running-threads>
<long-running-threads>
<core-threads count="50"/>
<queue-length count="50"/>
<max-threads count="50"/>
<keepalive-time time="10" unit="seconds"/>
</long-running-threads>
</default-workmanager>
<cached-connection-manager/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jgroups:8.0">
<channels default="ee">
<channel name="ee" stack="udp" cluster="ejb"/>
</channels>
<stacks>
<stack name="udp">
<transport type="UDP" socket-binding="jgroups-udp"/>
<protocol type="PING"/>
<protocol type="MERGE3"/>
<socket-protocol type="FD_SOCK" socket-binding="jgroups-udp-fd"/>
<protocol type="FD_ALL"/>
<protocol type="VERIFY_SUSPECT"/>
<protocol type="pbcast.NAKACK2"/>
<protocol type="UNICAST3"/>
<protocol type="pbcast.STABLE"/>
<protocol type="pbcast.GMS"/>
<protocol type="UFC"/>
<protocol type="MFC"/>
<protocol type="FRAG3"/>
</stack>
<stack name="tcp">
<transport type="TCP" socket-binding="jgroups-tcp"/>
<socket-protocol type="MPING" socket-binding="jgroups-mping"/>
<protocol type="MERGE3"/>
<socket-protocol type="FD_SOCK" socket-binding="jgroups-tcp-fd"/>
<protocol type="FD_ALL"/>
<protocol type="VERIFY_SUSPECT"/>
<protocol type="pbcast.NAKACK2"/>
<protocol type="UNICAST3"/>
<protocol type="pbcast.STABLE"/>
<protocol type="pbcast.GMS"/>
<protocol type="MFC"/>
<protocol type="FRAG3"/>
</stack>
</stacks>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
<expose-resolved-model/>
<expose-expression-model/>
<remoting-connector/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
<jpa default-extended-persistence-inheritance="DEEP"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
<web-context>auth</web-context>
<providers>
<provider>
classpath:${jboss.home.dir}/providers/*
</provider>
</providers>
<master-realm-name>master</master-realm-name>
<scheduled-task-interval>900</scheduled-task-interval>
<theme>
<staticMaxAge>2592000</staticMaxAge>
<cacheThemes>true</cacheThemes>
<cacheTemplates>true</cacheTemplates>
<welcomeTheme>${env.KEYCLOAK_WELCOME_THEME:keycloak}</welcomeTheme>
<default>${env.KEYCLOAK_DEFAULT_THEME:keycloak}</default>
<dir>${jboss.home.dir}/themes</dir>
</theme>
<spi name="eventsStore">
<provider name="jpa" enabled="true">
<properties>
<property name="exclude-events" value="[&quot;REFRESH_TOKEN&quot;]"/>
</properties>
</provider>
</spi>
<spi name="userCache">
<provider name="default" enabled="true"/>
</spi>
<spi name="userSessionPersister">
<default-provider>jpa</default-provider>
</spi>
<spi name="timer">
<default-provider>basic</default-provider>
</spi>
<spi name="connectionsHttpClient">
<provider name="default" enabled="true"/>
</spi>
<spi name="connectionsJpa">
<provider name="default" enabled="true">
<properties>
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
<property name="initializeEmpty" value="true"/>
<property name="migrationStrategy" value="update"/>
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
</properties>
</provider>
</spi>
<spi name="realmCache">
<provider name="default" enabled="true"/>
</spi>
<spi name="connectionsInfinispan">
<default-provider>default</default-provider>
<provider name="default" enabled="true">
<properties>
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
</properties>
</provider>
</spi>
<spi name="jta-lookup">
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
<provider name="jboss" enabled="true"/>
</spi>
<spi name="publicKeyStorage">
<provider name="infinispan" enabled="true">
<properties>
<property name="minTimeBetweenRequests" value="10"/>
</properties>
</provider>
</spi>
<spi name="x509cert-lookup">
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
<provider name="default" enabled="true"/>
</spi>
<spi name="hostname">
<default-provider>${keycloak.hostname.provider:default}</default-provider>
<provider name="default" enabled="true">
<properties>
<property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
<property name="forceBackendUrlToFrontendUrl" value="false"/>
</properties>
</provider>
<provider name="fixed" enabled="true">
<properties>
<property name="hostname" value="${keycloak.hostname.fixed.hostname:localhost}"/>
<property name="httpPort" value="${keycloak.hostname.fixed.httpPort:-1}"/>
<property name="httpsPort" value="${keycloak.hostname.fixed.httpsPort:-1}"/>
<property name="alwaysHttps" value="${keycloak.hostname.fixed.alwaysHttps:false}"/>
</properties>
</provider>
</spi>
</subsystem>
<subsystem xmlns="urn:jboss:domain:mail:4.0">
<mail-session name="default" jndi-name="java:jboss/mail/Default">
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
</mail-session>
</subsystem>
<subsystem xmlns="urn:wildfly:metrics:1.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
<proxy name="default" advertise-socket="modcluster" listener="ajp">
<dynamic-load-provider>
<load-metric type="cpu"/>
</dynamic-load-provider>
</proxy>
</subsystem>
<subsystem xmlns="urn:jboss:domain:naming:2.0">
<remote-naming/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
<subsystem xmlns="urn:jboss:domain:security:2.0">
<security-domains>
<security-domain name="other" cache-type="default">
<authentication>
<login-module code="Remoting" flag="optional">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
<login-module code="RealmDirect" flag="required">
<module-option name="password-stacking" value="useFirstPass"/>
</login-module>
</authentication>
</security-domain>
<security-domain name="jboss-web-policy" cache-type="default">
<authorization>
<policy-module code="Delegating" flag="required"/>
</authorization>
</security-domain>
<security-domain name="jaspitest" cache-type="default">
<authentication-jaspi>
<login-module-stack name="dummy">
<login-module code="Dummy" flag="optional"/>
</login-module-stack>
<auth-module code="Dummy"/>
</authentication-jaspi>
</security-domain>
<security-domain name="jboss-ejb-policy" cache-type="default">
<authorization>
<policy-module code="Delegating" flag="required"/>
</authorization>
</security-domain>
</security-domains>
</subsystem>
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
<deployment-permissions>
<maximum-set>
<permission class="java.security.AllPermission"/>
</maximum-set>
</deployment-permissions>
</subsystem>
<subsystem xmlns="urn:jboss:domain:transactions:6.0">
<core-environment node-identifier="${jboss.tx.node.id:1}">
<process-id>
<uuid/>
</process-id>
</core-environment>
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
</subsystem>
<subsystem xmlns="urn:jboss:domain:undertow:12.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
<buffer-cache name="default"/>
<server name="default-server">
<ajp-listener name="ajp" socket-binding="ajp"/>
<http-listener name="default" socket-binding="http" redirect-socket="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
<https-listener name="https" socket-binding="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" security-realm="ApplicationRealm" enable-http2="true"/>
<host name="default-host" alias="localhost">
<location name="/" handler="welcome-content"/>
<http-invoker security-realm="ApplicationRealm"/>
</host>
</server>
<servlet-container name="default">
<jsp-config/>
<websockets/>
</servlet-container>
<handlers>
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
</handlers>
</subsystem>
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
</profile>
<interfaces>
<interface name="management">
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
</interface>
<interface name="private">
<inet-address value="${jboss.bind.address.private:127.0.0.1}"/>
</interface>
<interface name="public">
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
</interface>
</interfaces>
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
<socket-binding name="http" port="${jboss.http.port:8080}"/>
<socket-binding name="https" port="${jboss.https.port:8443}"/>
<socket-binding name="jgroups-mping" interface="private" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
<socket-binding name="jgroups-tcp" interface="private" port="7600"/>
<socket-binding name="jgroups-tcp-fd" interface="private" port="57600"/>
<socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>
<socket-binding name="jgroups-udp-fd" interface="private" port="54200"/>
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
<socket-binding name="txn-recovery-environment" port="4712"/>
<socket-binding name="txn-status-manager" port="4713"/>
<outbound-socket-binding name="mail-smtp">
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
</outbound-socket-binding>
</socket-binding-group>
</server>

View File

@ -122,19 +122,10 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/InseeFrLab/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl"]?seq_contains(pageId)
) || (
["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)
pageId == "saml-post-form.ftl"
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
@ -153,6 +144,15 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
</#if>
<#if
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"])
>
<#continue>
</#if>
<#if key == "identityProviderBrokerCtx" && are_same_path(path, []) ><#continue></#if>
<#attempt>
<#if !object[key]??>
<#continue>

View File

@ -18,7 +18,6 @@ export const pageIds = [
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
] as const;

View File

@ -2,16 +2,10 @@ import * as url from "url";
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
export function generateJavaStackFiles(params: {
version: string;
themeName: string;
homepage?: string;
keycloakThemeBuildingDirPath: string;
doBundleEmailTemplate: boolean;
}): {
export function generateJavaStackFiles(params: { version: string; themeName: string; homepage?: string; keycloakThemeBuildingDirPath: string }): {
jarFilePath: string;
} {
const { themeName, version, homepage, keycloakThemeBuildingDirPath, doBundleEmailTemplate } = params;
const { themeName, version, homepage, keycloakThemeBuildingDirPath } = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
@ -69,7 +63,7 @@ export function generateJavaStackFiles(params: {
"themes": [
{
"name": themeName,
"types": ["login", ...(doBundleEmailTemplate ? ["email"] : [])],
"types": ["login"],
},
],
},

View File

@ -1,6 +1,6 @@
import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { join as pathJoin } from "path";
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
@ -12,19 +12,17 @@ export function generateKeycloakThemeResources(params: {
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
keycloakThemeEmailDirPath: string;
urlPathname: string;
//If urlOrigin is not undefined then it means --externals-assets
urlOrigin: undefined | string;
extraPagesId: string[];
extraThemeProperties: string[];
keycloakVersion: string;
}): { doBundleEmailTemplate: boolean } {
keycloakVersion: "11.0.3" | "15.0.2";
}) {
const {
themeName,
reactAppBuildDirPath,
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
urlPathname,
urlOrigin,
extraPagesId,
@ -81,28 +79,6 @@ export function generateKeycloakThemeResources(params: {
},
});
let doBundleEmailTemplate: boolean;
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-theme-email-directory 👈`,
].join("\n"),
);
doBundleEmailTemplate = false;
break email;
}
doBundleEmailTemplate = true;
transformCodebase({
"srcDirPath": keycloakThemeEmailDirPath,
"destDirPath": pathJoin(themeDirPath, "..", "email"),
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine,
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
@ -136,22 +112,20 @@ export function generateKeycloakThemeResources(params: {
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(resourcesCommonPath)),
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesCommonPath),
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename);
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(" "),
),
Buffer.from(["This is just a test folder that helps develop", "the login and register page without having to yarn build"].join(" ")),
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
@ -163,6 +137,4 @@ export function generateKeycloakThemeResources(params: {
pathJoin(themeDirPath, "theme.properties"),
Buffer.from("parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
);
return { doBundleEmailTemplate };
}

View File

@ -1,44 +0,0 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
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; themeName: string; keycloakThemeBuildingDirPath: string }) {
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from(
[
"#!/bin/bash",
"",
`docker rm ${containerName} || true`,
"",
`cd ${keycloakThemeBuildingDirPath}`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v ${pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName,
)}:/opt/keycloak/themes/${themeName}:rw \\`,
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
"",
].join("\n"),
"utf8",
),
{ "mode": 0o755 },
);
}

View File

@ -17,12 +17,12 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOr
const { jsCode, urlOrigin } = params;
const fixedJsCode = jsCode
.replace(/(\w+\.\w+)\+"static\//g, (...[, group]) =>
.replace(/([a-z]+\.[a-z]+)\+"static\//g, (...[, group]) =>
urlOrigin === undefined
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`,
)
.replace(/".chunk.css",(\w)+=(\w+\.\w+)\+(\w+),/, (...[, group1, group2, group3]) =>
.replace(/".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/, (...[, group1, group2, group3]) =>
urlOrigin === undefined
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,

View File

@ -1,36 +0,0 @@
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { keycloakThemeEmailDirPath } from "./build-keycloak-theme";
import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
if (require.main === module) {
(async () => {
if (fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
const { keycloakVersion } = await promptKeycloakVersion();
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": keycloakThemeEmailDirPath,
});
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})();
}

View File

@ -3,9 +3,9 @@
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import type { KeycloakVersion } from "./KeycloakVersion";
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string }) {
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: KeycloakVersion; destDirPath: string }) {
const { keycloakVersion, destDirPath } = params;
for (const ext of ["", "-community"]) {
@ -18,16 +18,22 @@ export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string;
}
if (require.main === module) {
(async () => {
const { keycloakVersion } = await promptKeycloakVersion();
const keycloakVersion = (() => {
const keycloakVersion = process.argv[2] as KeycloakVersion | undefined;
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
if (keycloakVersion === undefined) {
return "15.0.2";
}
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
});
return keycloakVersion;
})();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
});
}

View File

@ -5,11 +5,12 @@ import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm";
import { keycloakVersions } from "./KeycloakVersion";
//@ts-ignore
const propertiesParser = require("properties-parser");
for (const keycloakVersion of ["11.0.3", "15.0.2", "16.1.0"]) {
for (const keycloakVersion of keycloakVersions) {
console.log({ keycloakVersion });
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");

View File

@ -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 };
}

View File

@ -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";
}
}

View File

@ -1,7 +0,0 @@
import { Octokit } from "@octokit/rest";
export function createOctokit(params: { github_token: string }) {
const { github_token } = params;
return new Octokit({ ...(github_token !== "" ? { "auth": github_token } : {}) });
}

View File

@ -2,7 +2,7 @@ import * as fs from "fs";
import * as path from "path";
function getProjectRootRec(dirPath: string): string {
if (fs.existsSync(path.join(dirPath, "tsconfig.json"))) {
if (fs.existsSync(path.join(dirPath, "package.json"))) {
return dirPath;
}
return getProjectRootRec(path.join(dirPath, ".."));

View File

@ -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 };
}

View File

@ -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 };
}

View File

@ -2,10 +2,10 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => {
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
const { message, client } = kcContext;

View File

@ -3,10 +3,10 @@ import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => {
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
assert(kcContext.message !== undefined);

View File

@ -14,7 +14,6 @@ import { LoginUpdatePassword } from "./LoginUpdatePassword";
import { LoginUpdateProfile } from "./LoginUpdateProfile";
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
import { LoginPageExpired } from "./LoginPageExpired";
import { LoginIdpLinkEmail } from "./LoginIdpLinkEmail";
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
switch (kcContext.pageId) {
@ -42,8 +41,6 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase }
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...props }} />;
}

View File

@ -2,33 +2,20 @@ import { useState, memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
//NOTE: Even if we login with email Keycloak expect username and password in
//the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
formElement.submit();
});
const onSubmit = useConstCallback(() => (setIsLoginButtonDisabled(true), true));
return (
<Template
@ -46,40 +33,27 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.L
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return (
<>
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={cx(props.kcInputClass)}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off",
})}
/>
</>
);
})()}
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{!realm.loginWithEmailAllowed
? msg("username")
: !realm.registrationEmailAsUsername
? msg("usernameOrEmail")
: msg("email")}
</label>
<input
tabIndex={1}
id="username"
className={cx(props.kcInputClass)}
name="username"
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off",
})}
/>
</div>
<div className={cx(props.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => {
const { url, idpAlias } = kcContext;
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
const { cx } = useCssAndCx();

View File

@ -1,32 +0,0 @@
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
export const LoginIdpLinkEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail } & KcProps) => {
const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = getMsg(kcContext);
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={
<>
<p id="instruction1" className="instruction">
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
</p>
<p id="instruction2" className="instruction">
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
</p>
<p id="instruction3" className="instruction">
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
</p>
</>
}
/>
);
});

View File

@ -2,7 +2,7 @@ import { useEffect, memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../tools/pathJoin";
import { useCssAndCx } from "tss-react";
@ -12,7 +12,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBas
const { cx } = useCssAndCx();
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
useEffect(() => {
let isCleanedUp = false;

View File

@ -2,12 +2,12 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
export const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => {
const { url } = kcContext;
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
return (
<Template

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => {
const { url, realm, auth } = kcContext;
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { cx } = useCssAndCx();

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
export const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;

View File

@ -2,10 +2,10 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => {
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
const { url } = kcContext;

View File

@ -2,13 +2,13 @@ import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => {
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { cx } = useCssAndCx();

View File

@ -2,7 +2,7 @@ import { useMemo, memo, useEffect, useState, Fragment } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
import type { ReactComponent } from "../tools/ReactComponent";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
@ -11,7 +11,7 @@ import { useFormValidationSlice } from "../useFormValidationSlice";
export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { cx, css } = useCssAndCx();
@ -74,7 +74,7 @@ type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
const { cx, css } = useCssAndCx();
const { advancedMsg } = getMsg(kcContext);
const { advancedMsg } = useKcMessage();
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
@ -95,7 +95,7 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
{
target: { value },
},
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>],
]: [React.ChangeEvent<HTMLInputElement>],
) =>
formValidationReducer({
"action": "update value",
@ -148,50 +148,26 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
{attribute.required && <>*</>}
</div>
<div className={cx(props.kcInputWrapperClass)}>
{(() => {
const { options } = attribute.validators;
if (options !== undefined) {
return (
<select
id={attribute.name}
name={attribute.name}
onChange={onChangeFactory(attribute.name)}
onBlur={onBlurFactory(attribute.name)}
value={value}
>
{options.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<input
type={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "password";
default:
return "text";
}
})()}
id={attribute.name}
name={attribute.name}
value={value}
onChange={onChangeFactory(attribute.name)}
className={cx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
autoComplete={attribute.autocomplete}
onBlur={onBlurFactory(attribute.name)}
/>
);
})()}
<input
type={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "password";
default:
return "text";
}
})()}
id={attribute.name}
name={attribute.name}
value={value}
onChange={onChangeFactory(attribute.name)}
className={cx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
autoComplete={attribute.autocomplete}
onBlur={onBlurFactory(attribute.name)}
/>
{displayableErrors.length !== 0 && (
<span
id={`input-error-${attribute.name}`}

View File

@ -1,9 +1,12 @@
import { useReducer, useEffect, memo } from "react";
import type { ReactNode } from "react";
import { getMsg, getCurrentKcLanguageTag, changeLocale, getTagLabel } from "../i18n";
import type { KcLanguageTag } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { assert } from "../tools/assert";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../tools/pathJoin";
@ -48,19 +51,30 @@ export const Template = memo((props: TemplateProps) => {
console.log("Rendering this page with react using keycloakify");
}, []);
const { msg } = getMsg(kcContext);
const { msg } = useKcMessage();
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [KcLanguageTag]) =>
changeLocale({
kcContext,
kcLanguageTag,
}),
);
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
const onChangeLanguageClickFactory = useCallbackFactory(([languageTag]: [KcLanguageTag]) => setKcLanguageTag(languageTag));
const onTryAnotherWayClick = useConstCallback(() => (document.forms["kc-select-try-another-way-form" as never].submit(), false));
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
useEffect(() => {
if (!realm.internationalizationEnabled) {
return;
}
assert(locale !== undefined);
if (kcLanguageTag === getBestMatchAmongKcLanguageTag(locale.current)) {
return;
}
window.location.href = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag)!.url;
}, [kcLanguageTag]);
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
useEffect(() => {
@ -138,13 +152,13 @@ export const Template = memo((props: TemplateProps) => {
<div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}>
<div className="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link">
{getTagLabel({ "kcLanguageTag": getCurrentKcLanguageTag(kcContext), kcContext })}
{getKcLanguageTagLabel(kcLanguageTag)}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item">
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
{getTagLabel({ "kcLanguageTag": languageTag, kcContext })}
{getKcLanguageTagLabel(languageTag)}
</a>
</li>
))}

View File

@ -1,33 +1,12 @@
import { useReducer, useEffect, memo } from "react";
import { memo } from "react";
import { Template } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { getMsg } from "../i18n";
import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react";
import { kcMessages, getCurrentKcLanguageTag } from "../i18n";
import type { KcLanguageTag } from "../i18n";
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextBase;
downloadTermMarkdown: (params: { currentKcLanguageTag: KcLanguageTag }) => Promise<string>;
}) {
const { kcContext, downloadTermMarkdown } = params;
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
const currentKcLanguageTag = getCurrentKcLanguageTag(kcContext);
downloadTermMarkdown({ currentKcLanguageTag }).then(thermMarkdown => {
kcMessages[currentKcLanguageTag].termsText = thermMarkdown;
forceUpdate();
});
}, []);
}
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => {
const { msg, msgStr } = getMsg(kcContext);
const { msg, msgStr } = useKcMessage();
const { cx } = useCssAndCx();

View File

@ -1,8 +1,9 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { KcLanguageTag } from "../i18n";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n";
import type { MessageKey } from "../i18n/useKcMessage";
import type { LanguageLabel } from "../i18n/KcLanguageTag";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
@ -23,7 +24,6 @@ export type KcContextBase =
| KcContextBase.LoginUpdatePassword
| KcContextBase.LoginUpdateProfile
| KcContextBase.LoginIdpLinkConfirm
| KcContextBase.LoginIdpLinkEmail
| KcContextBase.LoginPageExpired;
export declare namespace KcContextBase {
@ -46,10 +46,13 @@ export declare namespace KcContextBase {
locale?: {
supported: {
url: string;
label: string;
languageTag: KcLanguageTag;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
//label: LanguageLabel;
}[];
currentLanguageTag: KcLanguageTag;
current: LanguageLabel;
};
auth?: {
showUsername: boolean;
@ -212,14 +215,6 @@ export declare namespace KcContextBase {
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";
};
@ -320,7 +315,6 @@ export type Validators = Partial<{
name: string;
shouldBe: "equal" | "different";
};
options: Validators.Options;
}>;
export declare namespace Validators {
@ -337,9 +331,6 @@ export declare namespace Validators {
min?: `${number}`;
max?: `${number}`;
};
export type Options = {
options: string[];
};
}
assert<Equals<KcContextBase["pageId"], PageId>>();

View File

@ -7,9 +7,6 @@ import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendsKcContextBase } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "../tools/pathJoin";
import { pathBasename } from "../tools/pathBasename";
import { resourcesCommonPath } from "./kcContextMocks/urlResourcesPath";
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
@ -96,13 +93,5 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
return { kcContext };
}
const kcContext = getKcContextFromWindow<KcContextExtended>();
if (kcContext !== undefined) {
const { url } = kcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonPath));
}
return { kcContext };
return { "kcContext": getKcContextFromWindow<KcContextExtended>() };
}

View File

@ -1,5 +1,7 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContextBase, Attribute } from "../KcContextBase";
import { getEvtKcLanguage } from "../../i18n/useKcLanguageTag";
import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
import { id } from "tsafe/id";
@ -30,100 +32,81 @@ export const kcContextCommonMock: KcContextBase.Common = {
},
"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",
//"current": null as any
"current": "English",
},
"auth": {
"showUsername": false,
@ -141,6 +124,11 @@ export const kcContextCommonMock: KcContextBase.Common = {
"isAppInitiatedAction": false,
};
Object.defineProperty(kcContextCommonMock.locale!, "current", {
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
"enumerable": true,
});
const loginUrl = {
...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
@ -379,12 +367,4 @@ export const kcContextMocks: KcContextBase[] = [
"pageId": "login-idp-link-confirm.ftl",
"idpAlias": "FranceConnect",
}),
id<KcContextBase.LoginIdpLinkEmail>({
...kcContextCommonMock,
"pageId": "login-idp-link-email.ftl",
"idpAlias": "FranceConnect",
"brokerContext": {
"username": "anUsername",
},
}),
];

View File

@ -2,4 +2,4 @@ import { pathJoin } from "../../tools/pathJoin";
export const subDirOfPublicDirBasename = "keycloak_static";
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "resources");
export const resourcesCommonPath = pathJoin(resourcesPath, "resources_common");
export const resourcesCommonPath = pathJoin(subDirOfPublicDirBasename, "resources_common");

View File

@ -0,0 +1,63 @@
import { objectKeys } from "tsafe/objectKeys";
import { kcMessages } from "./kcMessages/login";
export type KcLanguageTag = keyof typeof kcMessages;
const kcLanguageByTagLabel = {
/* spell-checker: disable */
"es": "Español",
"it": "Italiano",
"fr": "Français",
"ca": "Català",
"en": "English",
"de": "Deutsch",
"no": "Norsk",
"pt-BR": "Português (Brasil)",
"ru": "Русский",
"sk": "Slovenčina",
"ja": "日本語",
"pl": "Polski",
"zh-CN": "中文简体",
"sv": "Svenska",
"lt": "Lietuvių",
"cs": "Čeština",
"nl": "Nederlands",
"tr": "Türkçe",
"da": "Dansk",
"hu": "Magyar",
/* spell-checker: enable */
} as const;
export type LanguageLabel = typeof kcLanguageByTagLabel[keyof typeof kcLanguageByTagLabel];
export function getKcLanguageTagLabel(language: KcLanguageTag): LanguageLabel {
return kcLanguageByTagLabel[language] ?? language;
}
export const kcLanguageTags = objectKeys(kcMessages);
/**
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
* it corresponds to: "fr".
* If there is no reasonable match it's guessed from navigator.language.
* If still no matches "en" is returned.
*/
export function getBestMatchAmongKcLanguageTag(languageLike: string): KcLanguageTag {
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
const kcLanguageTag = kcLanguageTags.find(
language =>
language.toLowerCase().includes(iso2LanguageLike) ||
getKcLanguageTagLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase(),
);
if (kcLanguageTag !== undefined) {
return kcLanguageTag;
}
if (languageLike !== navigator.language) {
return getBestMatchAmongKcLanguageTag(navigator.language);
}
return "en";
}

View File

@ -1,180 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import ReactMarkdown from "react-markdown";
import memoize from "memoizee";
import { kcMessages as kcMessagesBase } from "./generated_kcMessages/15.0.2/login";
import { assert } from "tsafe/assert";
export const kcMessages = {
...kcMessagesBase,
"en": {
...kcMessagesBase["en"],
"termsText": "⏳",
"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": {
...kcMessagesBase["fr"],
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être egale à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entiers",
"notAValidOption": "N'est pas une option valide",
/* spell-checker: enable */
},
};
export type KcLanguageTag = keyof typeof kcMessages;
type KcContextLike = { locale?: { currentLanguageTag: KcLanguageTag } };
export function getCurrentKcLanguageTag(kcContext: KcContextLike) {
return kcContext.locale?.currentLanguageTag ?? "en";
}
export function getTagLabel(params: {
kcContext: {
locale?: {
supported: { languageTag: KcLanguageTag; label: string }[];
};
};
kcLanguageTag: KcLanguageTag;
}): string {
const { kcContext, kcLanguageTag } = params;
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag);
assert(targetSupportedLocale !== undefined, `${kcLanguageTag} need to be enabled in Keycloak admin`);
return targetSupportedLocale.label;
}
export function changeLocale(params: {
kcContext: {
locale?: {
supported: { languageTag: KcLanguageTag; url: string }[];
};
};
kcLanguageTag: KcLanguageTag;
}): never {
const { kcContext, kcLanguageTag } = params;
const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled");
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag);
assert(targetSupportedLocale !== undefined, `${kcLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url;
assert(false, "never");
}
export type MessageKey = keyof typeof kcMessages["en"];
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
if (str === undefined) {
return undefined as any;
}
str = (() => {
const startIndex = str
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
return str;
}
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return str;
})();
return (
doRenderMarkdown ? (
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
{str}
</ReactMarkdown>
) : (
str
)
) as any;
}
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): DoRenderMarkdown extends true ? JSX.Element : string {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
kcLanguageTag,
doRenderMarkdown,
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
/**
* When the language is switched the page is reloaded, this may appear
* as a bug as you might notice that the language successfully switch before
* reload.
* However we need to tell Keycloak that the user have changed the language
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
*
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*
*
* NOTE: This function is memoized, it always returns the same object for a given kcContext)
*
*/
export const getMsg = memoize((kcContext: KcContextLike) => {
const kcLanguageTag = getCurrentKcLanguageTag(kcContext);
return {
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element => resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
};
});

View File

@ -0,0 +1,48 @@
import { kcMessages as kcMessagesBase } from "../generated_kcMessages/15.0.2/login";
import { Evt } from "evt";
import { objectKeys } from "tsafe/objectKeys";
const kcMessages = {
...kcMessagesBase,
"en": {
...kcMessagesBase["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",
},
"fr": {
...kcMessagesBase["fr"],
/* spell-checker: disable */
"shouldBeEqual": "{0} doit être egale à {1}",
"shouldBeDifferent": "{0} doit être différent de {1}",
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
"mustBeAnInteger": "Doit être un nombre entiers",
/* spell-checker: enable */
},
};
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
objectKeys(kcMessages).forEach(kcLanguage =>
Object.defineProperty(
kcMessages[kcLanguage],
key,
(() => {
let value = key === "termsText" ? "⏳" : kcMessages[kcLanguage][key];
return {
"enumerable": true,
"get": () => value,
"set": (newValue: string) => {
value = newValue;
Evt.asPostable(evtTermsUpdated).post();
},
};
})(),
),
),
);
export { kcMessages };

View File

@ -0,0 +1,28 @@
import { createUseGlobalState } from "powerhooks/useGlobalState";
import { getKcContextFromWindow } from "../getKcContext/getKcContextFromWindow";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
import type { StatefulEvt } from "powerhooks";
import { KcLanguageTag } from "./KcLanguageTag";
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
const wrap = createUseGlobalState(
"kcLanguageTag",
() => {
const kcContext = getKcContextFromWindow();
const languageLike = kcContext?.locale?.current ?? (typeof navigator === "undefined" ? undefined : navigator.language);
if (languageLike === undefined) {
return "en";
}
return getBestMatchAmongKcLanguageTag(languageLike);
},
{ "persistance": "localStorage" },
);
export const { useKcLanguageTag } = wrap;
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
return wrap.evtKcLanguageTag;
}

View File

@ -0,0 +1,115 @@
import "minimal-polyfills/Object.fromEntries";
import { useReducer } from "react";
import { useKcLanguageTag } from "./useKcLanguageTag";
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
import { useEvt } from "evt/hooks";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import ReactMarkdown from "react-markdown";
import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo";
export { kcMessages };
export type MessageKey = keyof typeof kcMessages["en"];
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
if (str === undefined) {
return undefined as any;
}
str = (() => {
const startIndex = str
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {
return str;
}
args.forEach((arg, i) => {
if (arg === undefined) {
return;
}
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
});
return str;
})();
return (
doRenderMarkdown ? (
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
{str}
</ReactMarkdown>
) : (
str
)
) as any;
}
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
key: Key;
args: (string | undefined)[];
kcLanguageTag: string;
doRenderMarkdown: DoRenderMarkdown;
}): DoRenderMarkdown extends true ? JSX.Element : string {
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
const match = key.match(/^\$\{([^{]+)\}$/);
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
const out = resolveMsg({
"key": keyUnwrappedFromCurlyBraces,
args,
kcLanguageTag,
doRenderMarkdown,
});
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
}
/**
* When the language is switched the page is reloaded, this may appear
* as a bug as you might notice that the language successfully switch before
* reload.
* However we need to tell Keycloak that the user have changed the language
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
*
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
*
*/
export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag();
const [trigger, forceUpdate] = useReducer((counter: number) => counter + 1, 0);
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
return useGuaranteedMemo(
() => ({
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string =>
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element =>
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
}),
[kcLanguageTag, trigger],
);
}

View File

@ -1,10 +1,18 @@
export * from "./getKcContext";
export * from "./i18n";
export { useDownloadTerms } from "./components/Terms";
export * from "./i18n/KcLanguageTag";
export * from "./i18n/useKcLanguageTag";
export * from "./i18n/useKcMessage";
export * from "./i18n/kcMessages/login";
export * from "./components/KcProps";
export * from "./components/Login";
export * from "./components/Template";
export * from "./components/KcApp";
export * from "./components/Info";
export * from "./components/Error";
export * from "./components/LoginResetPassword";
export * from "./components/LoginVerifyEmail";
export * from "./keycloakJsAdapter";
export * from "./useFormValidationSlice";

View File

@ -1,3 +0,0 @@
export function pathBasename(path: string) {
return path.split("/").reverse()[0];
}

View File

@ -1,30 +1,35 @@
import "./tools/Array.prototype.every";
import { useMemo, useReducer, Fragment } from "react";
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
import { getMsg } from "./i18n";
import type { KcLanguageTag } from "./i18n";
import { useKcMessage } from "./i18n/useKcMessage";
import { useConstCallback } from "powerhooks/useConstCallback";
import { id } from "tsafe/id";
import type { MessageKey } from "./i18n";
import type { MessageKey } from "./i18n/useKcMessage";
import { emailRegexp } from "./tools/emailRegExp";
export type KcContextLike = {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
attributes: { name: string; value?: string; validators: Validators }[];
passwordRequired: boolean;
realm: { registrationEmailAsUsername: boolean };
};
export function useGetErrors(params: {
kcContext: {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
profile: {
attributes: { name: string; value?: string; validators: Validators }[];
};
locale?: { currentLanguageTag: KcLanguageTag };
};
}) {
const { kcContext } = params;
const {
messagesPerField,
profile: { attributes },
} = kcContext;
kcContext: {
messagesPerField,
profile: { attributes },
},
} = params;
const { msg, msgStr, advancedMsg, advancedMsgStr } = getMsg(kcContext);
const { msg, msgStr, advancedMsg, advancedMsgStr } = useKcMessage();
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
const { name, fieldValueByAttributeName } = params;
@ -208,7 +213,7 @@ export function useGetErrors(params: {
break scope;
}
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
const msgArgs = ["invalidEmailMessage"] as const;
errors.push({
validatorName,
@ -271,32 +276,6 @@ export function useGetErrors(params: {
}
}
scope: {
const validatorName = "options";
const validator = validators[validatorName];
if (validator === undefined) {
break scope;
}
if (value === "") {
break scope;
}
if (validator.options.indexOf(value) >= 0) {
break scope;
}
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
errors.push({
validatorName,
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
"errorMessageStr": advancedMsgStr(...msgArgs),
});
}
//TODO: Implement missing validators.
return errors;

View File

@ -8,7 +8,6 @@ generateKeycloakThemeResources({
"themeName": "keycloakify-demo-app",
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
"keycloakThemeEmailDirPath": pathJoin(sampleReactProjectDirPath, "keycloak_theme_email"),
"urlPathname": "/keycloakify-demo-app/",
"urlOrigin": undefined,
"extraPagesId": ["my-custom-page.ftl"],

222
yarn.lock
View File

@ -30,17 +30,6 @@
dependencies:
regenerator-runtime "^0.13.4"
"@emotion/cache@*":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
dependencies:
"@emotion/memoize" "^0.7.4"
"@emotion/sheet" "^1.1.0"
"@emotion/utils" "^1.0.0"
"@emotion/weak-memoize" "^0.2.5"
stylis "4.0.13"
"@emotion/cache@^11.4.0":
version "11.4.0"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0"
@ -91,11 +80,6 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.2.tgz#1d9ffde531714ba28e62dac6a996a8b1089719d0"
integrity sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw==
"@emotion/sheet@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
"@emotion/unitless@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
@ -111,107 +95,6 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@octokit/auth-token@^2.4.4":
version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==
dependencies:
"@octokit/types" "^6.0.3"
"@octokit/core@^3.5.1":
version "3.6.0"
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085"
integrity sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==
dependencies:
"@octokit/auth-token" "^2.4.4"
"@octokit/graphql" "^4.5.8"
"@octokit/request" "^5.6.3"
"@octokit/request-error" "^2.0.5"
"@octokit/types" "^6.0.3"
before-after-hook "^2.2.0"
universal-user-agent "^6.0.0"
"@octokit/endpoint@^6.0.1":
version "6.0.12"
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658"
integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==
dependencies:
"@octokit/types" "^6.0.3"
is-plain-object "^5.0.0"
universal-user-agent "^6.0.0"
"@octokit/graphql@^4.5.8":
version "4.8.0"
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3"
integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==
dependencies:
"@octokit/request" "^5.6.0"
"@octokit/types" "^6.0.3"
universal-user-agent "^6.0.0"
"@octokit/openapi-types@^11.2.0":
version "11.2.0"
resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-11.2.0.tgz#b38d7fc3736d52a1e96b230c1ccd4a58a2f400a6"
integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA==
"@octokit/plugin-paginate-rest@^2.16.8":
version "2.17.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz#32e9c7cab2a374421d3d0de239102287d791bce7"
integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw==
dependencies:
"@octokit/types" "^6.34.0"
"@octokit/plugin-request-log@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85"
integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==
"@octokit/plugin-rest-endpoint-methods@^5.12.0":
version "5.13.0"
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz#8c46109021a3412233f6f50d28786f8e552427ba"
integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA==
dependencies:
"@octokit/types" "^6.34.0"
deprecation "^2.3.1"
"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.1.0.tgz#9e150357831bfc788d13a4fd4b1913d60c74d677"
integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==
dependencies:
"@octokit/types" "^6.0.3"
deprecation "^2.0.0"
once "^1.4.0"
"@octokit/request@^5.6.0", "@octokit/request@^5.6.3":
version "5.6.3"
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0"
integrity sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==
dependencies:
"@octokit/endpoint" "^6.0.1"
"@octokit/request-error" "^2.1.0"
"@octokit/types" "^6.16.1"
is-plain-object "^5.0.0"
node-fetch "^2.6.7"
universal-user-agent "^6.0.0"
"@octokit/rest@^18.12.0":
version "18.12.0"
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.12.0.tgz#f06bc4952fc87130308d810ca9d00e79f6988881"
integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==
dependencies:
"@octokit/core" "^3.5.1"
"@octokit/plugin-paginate-rest" "^2.16.8"
"@octokit/plugin-request-log" "^1.0.4"
"@octokit/plugin-rest-endpoint-methods" "^5.12.0"
"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0":
version "6.34.0"
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.34.0.tgz#c6021333334d1ecfb5d370a8798162ddf1ae8218"
integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw==
dependencies:
"@octokit/openapi-types" "^11.2.0"
"@types/mdast@^3.0.0", "@types/mdast@^3.0.3":
version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -219,15 +102,10 @@
dependencies:
"@types/unist" "*"
"@types/memoizee@^0.4.7":
version "0.4.7"
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.7.tgz#fa0025b5ea1c8acb85fd52a76982c5a096c2c91e"
integrity sha512-EMtvWnRC/W0KYmXxbPJAMg/M1OXkFboSpd/IzkNMDXfoFPE4uom1RHFl8q7m/4R0y9eG1yaoaIPiMHk0vMA0eA==
"@types/node@^17.0.25":
version "17.0.25"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.25.tgz#527051f3c2f77aa52e5dc74e45a3da5fb2301448"
integrity sha512-wANk6fBrUwdpY4isjWrKTufkrXdu1D2YHCot2fD/DfWxF5sMrVSA+KN7ydckvaTCh0HiqX9IVl0L5/ZoXg5M7w==
"@types/node@^10.0.0":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==
"@types/parse-json@^4.0.0":
version "4.0.0"
@ -271,11 +149,6 @@ ansi-colors@^4.1.1:
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==
ansi-escapes@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b"
integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==
ansi-escapes@^4.3.0:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
@ -317,11 +190,6 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
before-after-hook@^2.2.0:
version "2.2.2"
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -420,13 +288,6 @@ cli-cursor@^3.1.0:
dependencies:
restore-cursor "^3.1.0"
cli-select@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/cli-select/-/cli-select-1.1.2.tgz#456dced464b3346ca661b16a0e37fc4b28db4818"
integrity sha512-PSvWb8G0PPmBNDcz/uM2LkZN3Nn5JmhUl465tTfynQAXjKzFpmHbxStM6X/+awKp5DJuAaHMzzMPefT0suGm1w==
dependencies:
ansi-escapes "^3.2.0"
cli-truncate@2.1.0, cli-truncate@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7"
@ -562,11 +423,6 @@ debug@^4.0.0, debug@^4.3.2:
dependencies:
ms "2.1.2"
deprecation@^2.0.0, deprecation@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
dom-serializer@^1.0.1, dom-serializer@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
@ -680,6 +536,15 @@ event-emitter@^0.3.5:
d "1"
es5-ext "~0.10.14"
evt@2.0.0-beta.38:
version "2.0.0-beta.38"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.38.tgz#9886a08889cddba1984a236efd6d352f9d6a6539"
integrity sha512-b35iBAVlDHVHOqgWzjSBJZu3C0GRZ/2cIHfNFuMSWwLT/WJO2n/4x7hg6BpaNJlSRywBAtf8KcU1GUyVfEhVIA==
dependencies:
minimal-polyfills "^2.2.1"
run-exclusive "^2.2.14"
tsafe "^0.4.1"
evt@2.0.0-beta.39:
version "2.0.0-beta.39"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.39.tgz#3c859a83b35940f7eecfb5f148f03b7cbf3fee51"
@ -917,11 +782,6 @@ is-plain-obj@^2.0.0:
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287"
integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==
is-plain-object@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344"
integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==
is-promise@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
@ -1131,13 +991,6 @@ next-tick@~1.0.0:
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
node-fetch@^2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
dependencies:
whatwg-url "^5.0.0"
noms@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859"
@ -1170,7 +1023,7 @@ object-assign@^4.1.1:
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
once@^1.3.0, once@^1.4.0:
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@ -1295,12 +1148,12 @@ please-upgrade-node@^3.2.0:
dependencies:
semver-compare "^1.0.0"
powerhooks@^0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.14.0.tgz#44dd201f470761362a139ae2cb51eaa658ed5e3e"
integrity sha512-jWrRHyqev7Lh3MId7h1mNxs+fgegr8liHN17AaHxMgrXI6KxJ14B3Pe1It4FIRWJ4Z1dxsA734FerFGZ3Vgdkg==
powerhooks@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.11.0.tgz#923749ed405a1189759cd3f16d36bd5efacfd40c"
integrity sha512-I48J5vJqlTRiR3eH6svxiIYLutdedu2YEd3uhCmK+pRg2jcuourVwp1UYORI/EzbKC9vv0X/0Vd/SZ6e07rYtA==
dependencies:
evt "2.0.0-beta.39"
evt "2.0.0-beta.38"
memoizee "^0.4.15"
resize-observer-polyfill "^1.5.1"
tsafe "^0.8.1"
@ -1556,11 +1409,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
stylis@4.0.13:
version "4.0.13"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
stylis@^4.0.3:
version "4.0.10"
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.10.tgz#446512d1097197ab3f02fb3c258358c3f7a14240"
@ -1615,11 +1463,6 @@ to-regex-range@^5.0.1:
dependencies:
is-number "^7.0.0"
tr46@~0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=
trough@^1.0.0:
version "1.0.5"
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
@ -1650,12 +1493,11 @@ tslib@^2.2.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
tss-react@^3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-3.5.2.tgz#1b5db1f4a71fe62c939eed5368ea7809ca2ad0a9"
integrity sha512-IgGizaOhbntrWdh2EISJYnhYEEWXXWf3sn5aa8LYf7e59NCM5mg0PxJuEoLx9pLG5WlJF80zTRN/Y1Jnjw9LYA==
tss-react@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-3.0.0.tgz#b084e1d10d1ea23c925b6a2141c1e91a0c5e9a2d"
integrity sha512-aZ/DZEuUvqki/1TKKBM4OmRx6TI5lcaF4BLMo0D8lyT/5S7zRFaAdVfAlsirHcQNgOAdf5IjLUcEbCYWcY6PJw==
dependencies:
"@emotion/cache" "*"
"@emotion/serialize" "*"
"@emotion/utils" "*"
@ -1725,11 +1567,6 @@ unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universal-user-agent@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-6.0.0.tgz#3381f8503b251c0d9cd21bc1de939ec9df5480ee"
integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==
untildify@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
@ -1758,19 +1595,6 @@ vfile@^4.0.0:
unist-util-stringify-position "^2.0.0"
vfile-message "^2.0.0"
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"
integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=
whatwg-url@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d"
integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0=
dependencies:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
which-pm-runs@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"