Remove eslint and run prettier (changelog ignore)

This commit is contained in:
garronej
2021-10-11 21:35:40 +02:00
parent 9f8218efb7
commit 305ce9e44d
76 changed files with 27255 additions and 22419 deletions

View File

@ -1,7 +0,0 @@
node_modules/
dist/
CHANGELOG.md
.yarn_home/
src/test/apps/
src/test/types/
src/tools/types/

View File

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

View File

@ -9,14 +9,13 @@ on:
jobs: jobs:
test_lint: test_formatting:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts_ci' }}
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v2.3.4
- uses: actions/setup-node@v2.1.3 - uses: actions/setup-node@v2.1.3
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- name: If this step fails run 'npm run lint' and 'npm run format' then commit again. - name: If this step fails run 'yarn format' then commit again.
run: | run: |
PACKAGE_MANAGER=npm PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then if [ -f "./yarn.lock" ]; then
@ -27,7 +26,7 @@ jobs:
test: test:
runs-on: macos-10.15 runs-on: macos-10.15
needs: test_lint needs: test_formatting
strategy: strategy:
matrix: matrix:
node: [ '15', '14', '13' ] node: [ '15', '14', '13' ]

258
README.md
View File

@ -20,17 +20,19 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png"> <img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p> </p>
**NEW in v2** **NEW in v2**
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values. - It's now possible to implement custom `.ftl` pages.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`). - Support for Keycloak plugins that introduce non standard ftl values.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
# Motivations # 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. 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. 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. 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. 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 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. the cheer amount of maintenance that it involves.
@ -47,69 +49,71 @@ Here is `keycloakify` for you 🍸
<i> <a href="https://datalab.sspcloud.fr">With keycloakify:</a> </i> <i> <a href="https://datalab.sspcloud.fr">With keycloakify:</a> </i>
<br> <br>
<img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif"> <img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif">
</p> </p>
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up. **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. 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)
- [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-thesrc-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)
- [About the errors related to `objectToJson` in Keycloak logs.](#about-the-errors-related-to-objecttojson-in-keycloak-logs)
- [Email domain whitelist](#email-domain-whitelist)
- [Motivations](#motivations) # Requirements
- [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)
- [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-thesrc-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)
- [About the errors related to `objectToJson` in Keycloak logs.](#about-the-errors-related-to-objecttojson-in-keycloak-logs)
- [Email domain whitelist](#email-domain-whitelist)
# Requirements Tested with the following Keycloak versions:
Tested with the following Keycloak versions: - [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
- [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)
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore) - Tests ongoing with [14.0.0](https://hub.docker.com/layers/jboss/keycloak/14.0.0/images/sha256-ca713e87ad163da71ab329010de2464a41ff030a25ae0aef15c1c290252f3d7f?context=explore)
- Tests ongoing with [14.0.0](https://hub.docker.com/layers/jboss/keycloak/14.0.0/images/sha256-ca713e87ad163da71ab329010de2464a41ff030a25ae0aef15c1c290252f3d7f?context=explore)
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get 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. (before you customize it) will always be the ones of Keycloak v11.
This tool assumes you are bundling your app with Webpack (tested with 4.44.2) . This tool assumes you are bundling your app with Webpack (tested with 4.44.2) .
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file 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. and a `build/static/` directory generated by webpack.
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432) 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) **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`, `wget`, `unzip` are assumed to be available. - `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `wget`, `unzip` are assumed to be available.
- `docker` must be up and running when running `yarn keycloak`. - `docker` must be up and running when running `yarn keycloak`.
On Windows you'll have to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). On Windows you'll have to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10).
## My framework doesnt seem to be supported, what can I do? ## My framework doesnt seem to be supported, what can I do?
Currently Keycloakify is only compatible with `create-react-app` apps. 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 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. 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). 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 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. separate module. Checkout [ts_ci](https://github.com/garronej/ts_ci), it can help with that.
# How to use # How to use
## Setting up the build tool ## Setting up the build tool
```bash ```bash
@ -117,6 +121,7 @@ yarn add keycloakify
``` ```
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json) [`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
```json ```json
"scripts": { "scripts": {
"keycloak": "yarn build && build-keycloak-theme", "keycloak": "yarn build && build-keycloak-theme",
@ -135,14 +140,15 @@ The first approach is to only customize the style of the default Keycloak login
your own class names. your own class names.
If you have created a new React project specifically to create a Keycloak theme and nothing else then If you have created a new React project specifically to create a Keycloak theme and nothing else then
your index should look something like: your index should look something like:
`src/index.tsx` `src/index.tsx`
```tsx ```tsx
import { App } from "./<wherever>/App"; import { App } from "./<wherever>/App";
import { import {
KcApp, KcApp,
defaultKcProps, defaultKcProps,
getKcContext getKcContext
} from "keycloakify"; } from "keycloakify";
import { css } from "tss-react/@emotion/css"; import { css } from "tss-react/@emotion/css";
@ -152,59 +158,57 @@ const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" }); const myClassName = css({ "color": "red" });
reactDom.render( reactDom.render(
<KcApp <KcApp
kcContext={kcContext} kcContext={kcContext}
{...{ {...{
...defaultKcProps, ...defaultKcProps,
"kcHeaderWrapperClass": myClassName "kcHeaderWrapperClass": myClassName
}} }}
/> />
document.getElementById("root") document.getElementById("root")
); );
``` ```
If you share a unique project for your app and the Keycloak theme, your index should look If you share a unique project for your app and the Keycloak theme, your index should look
more like this: more like this:
`src/index.tsx` `src/index.tsx`
```tsx ```tsx
import { App } from "./<wherever>/App"; import { App } from "./<wherever>/App";
import { import { KcApp, defaultKcProps, getKcContext } from "keycloakify";
KcApp, import { css } from "tss-react/@emotion/css";
defaultKcProps,
getKcContext
} from "keycloakify";
import { css } from "tss-react/@emotion/css";
const { kcContext } = getKcContext(); const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" }); const myClassName = css({ "color": "red" });
reactDom.render( reactDom.render(
// Unless the app is currently being served by Keycloak // Unless the app is currently being served by Keycloak
// kcContext is undefined. // kcContext is undefined.
kcContext !== undefined ? kcContext !== undefined ? (
<KcApp <KcApp
kcContext={kcContext} kcContext={kcContext}
{...{ {...{
...defaultKcProps, ...defaultKcProps,
"kcHeaderWrapperClass": myClassName "kcHeaderWrapperClass": myClassName,
}} }}
/> : />
<App />, // Your actual app ) : (
document.getElementById("root") <App />
), // Your actual app
document.getElementById("root"),
); );
``` ```
<p align="center"> <p align="center">
<i>result:</i></br> <i>result:</i></br>
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png"> <img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
</p> </p>
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx) Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx)
(the [index.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) ) (the [index.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) )
and the result you can expect: and the result you can expect:
<p align="center"> <p align="center">
<i> <a href="https://datalab.sspcloud.fr">Customization using only CSS:</a> </i> <i> <a href="https://datalab.sspcloud.fr">Customization using only CSS:</a> </i>
@ -214,36 +218,39 @@ and the result you can expect:
### Advanced pages configuration ### Advanced pages configuration
If you want to go beyond only customizing the CSS you can re-implement some of the If you want to go beyond only customizing the CSS you can re-implement some of the
pages or even add new ones. 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 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/app/components/KcApp). If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/app/components/KcApp).
The web app is in production [here](https://datalab.sspcloud.fr). The web app is in production [here](https://datalab.sspcloud.fr).
Main takeaways are: 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 - You must declare your custom pages in the package.json. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/package.json#L17-L22)
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) You must declare theses page in the type argument of the getter
- (TS only) If you use Keycloak plugins that defines non standard `.ftl` values 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)
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) - (TS only) If you use Keycloak plugins that defines non standard `.ftl` values
that define `authorizedMailDomains` in `register.ftl`) you should (Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13) that define `authorizedMailDomains` in `register.ftl`) you should
- You should provide sample data for all the non standard value if you want to be able declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13)
to debug the page outside of keycloak. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L28-L43) - 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: WARNING: If you chose to go this way use:
```json ```json
"dependencies": { "dependencies": {
"keycloakify": "~X.Y.Z" "keycloakify": "~X.Y.Z"
} }
``` ```
in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app. in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app.
### Hot reload ### Hot reload
Rebuild the theme each time you make a change to see the result is not practical. 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`: If you want to test your login screens outside of Keycloak you can mock a given `kcContext`:
```tsx ```tsx
import { import {
@ -257,9 +264,9 @@ const { kcContext } = getKcContext({
}); });
reactDom.render( reactDom.render(
<KcApp <KcApp
kcContext={kcContextMocks.kcLoginContext} kcContext={kcContextMocks.kcLoginContext}
{...defaultKcProps} {...defaultKcProps}
/> />
document.getElementById("root") document.getElementById("root")
); );
@ -274,15 +281,15 @@ Checkout [this concrete example](https://github.com/garronej/keycloakify-demo-ap
By default the theme generated is standalone. Meaning that when your users 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. 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 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 to first browse unauthenticated before logging in, you will get a significant
performance boost if you jump through those hoops: 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) - 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)
- 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) - 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. - 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 - 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. 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. - 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) Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-app#about-keycloakify)
@ -308,51 +315,51 @@ If you need to customize pages that are not supported yet or if you need to impl
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate [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). the building and publishing of the theme (the .jar file).
# Limitations # Limitations
## `process.env.PUBLIC_URL` not supported. ## `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). 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). (This isn't recommended anyway).
## `@font-face` importing fonts from the `src/` dir ## `@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-) 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. this limitation doesn't apply, you can import fonts however you see fit.
### Example of setup that **won't** work ### Example of setup that **won't** work
- We have a `fonts/` directory in `src/` - 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. - 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 ### Possible workarounds
- Use [`--external-assets`](#enable-loading-in-a-blink-of-a-eye-of-login-pages-). - 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 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` - 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`. directory and to place your `@font-face` statements in the `public/index.html`.
Example [here](https://github.com/InseeFrLab/onyxia-ui/blob/0e3a04610cfe872ca71dad59e05ced8f785dee4b/public/index.html#L6-L51). Example [here](https://github.com/InseeFrLab/onyxia-ui/blob/0e3a04610cfe872ca71dad59e05ced8f785dee4b/public/index.html#L6-L51).
- 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). - 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) # Implement context persistence (optional)
If, before logging in, a user has selected a specific language 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 you don't want it to be reset to default when the user gets redirected to
the login or register pages. the login or register pages.
Same goes for the dark mode, you don't want, if the user had it enabled Same goes for the dark mode, you don't want, if the user had it enabled
to show the login page with light themes. to show the login page with light themes.
The problem is that you are probably using `localStorage` to persist theses values across 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 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`. app you won't be able to carry over states using `localStorage`.
The only reliable solution is to inject parameters into the URL before The only reliable solution is to inject parameters into the URL before
redirecting to Keycloak. We integrate with redirecting to Keycloak. We integrate with
[`keycloak-js`](https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc), [`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 by providing you a way to tell `keycloak-js` that you would like to inject
some search parameters before redirecting. some search parameters before redirecting.
The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`). The method also works with [`@react-keycloak/web`](https://www.npmjs.com/package/@react-keycloak/web) (use the `initOptions`).
@ -368,23 +375,24 @@ Note that the states are automatically restored on the other side by `powerhooks
```typescript ```typescript
import keycloak_js from "keycloak-js"; import keycloak_js from "keycloak-js";
import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState"; import { injectGlobalStatesInSearchParams } from "powerhooks/useGlobalState";
import { createKeycloakAdapter } from "keycloakify"; import { createKeycloakAdapter } from "keycloakify";
//... //...
const keycloakInstance = keycloak_js({ const keycloakInstance = keycloak_js({
"url": "http://keycloak-server/auth", "url": "http://keycloak-server/auth",
"realm": "myrealm", "realm": "myrealm",
"clientId": "myapp" "clientId": "myapp",
}); });
keycloakInstance.init({ keycloakInstance.init({
"onLoad": 'check-sso', "onLoad": "check-sso",
"silentCheckSsoRedirectUri": window.location.origin + "/silent-check-sso.html", "silentCheckSsoRedirectUri":
window.location.origin + "/silent-check-sso.html",
"adapter": createKeycloakAdapter({ "adapter": createKeycloakAdapter({
"transformUrlBeforeRedirect": injectGlobalStatesInSearchParams, "transformUrlBeforeRedirect": injectGlobalStatesInSearchParams,
keycloakInstance keycloakInstance,
}) }),
}); });
//... //...
@ -396,12 +404,13 @@ flash of the blank html before the js bundle have been evaluated
# Kickstart video # Kickstart video
*NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded* _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) [![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8)
# About the errors related to `objectToJson` in Keycloak logs. # About the errors related to `objectToJson` in Keycloak logs.
The logs of your keycloak server will always show this kind of errors every time a client request a page:
The logs of your keycloak server will always show this kind of errors every time a client request a page:
```log ```log
FTL stack trace ("~" means nesting-related): FTL stack trace ("~" means nesting-related):
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson" at line 70, column 21] - Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson" at line 70, column 21]
@ -410,13 +419,14 @@ FTL stack trace ("~" means nesting-related):
- Reached through: @compress [in template "login.ftl" in macro "objectToJson" at line 36, column 5] - Reached through: @compress [in template "login.ftl" in macro "objectToJson" at line 36, column 5]
- Reached through: @objectToJson object=(.data_model) de... [in template "login.ftl" at line 163, column 43] - Reached through: @objectToJson object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
``` ```
Theses are expected and can be safely ignored.
To [converts the `.ftl` values into a JavaScript object](https://github.com/InseeFrLab/keycloakify/blob/main/src/bin/build-keycloak-theme/generateFtl/common.ftl) Theses are expected and can be safely ignored.
To [converts the `.ftl` values into a JavaScript object](https://github.com/InseeFrLab/keycloakify/blob/main/src/bin/build-keycloak-theme/generateFtl/common.ftl)
without making assumptions on the `.data_model` we have to do things that throws. without making assumptions on the `.data_model` we have to do things that throws.
It's all-right though because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs. It's all-right though because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs.
# Email domain whitelist # Email domain whitelist
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) 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. and `kcRegisterContext["authorizedMailDomains"]` to validate on.

View File

@ -16,8 +16,6 @@
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/", "copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
"generate-messages": "node dist/bin/generate-i18n-messages.js", "generate-messages": "node dist/bin/generate-i18n-messages.js",
"link_in_test_app": "node dist/bin/link_in_test_app.js", "link_in_test_app": "node dist/bin/link_in_test_app.js",
"lint:check": "eslint . --ext .ts,.tsx",
"lint": "yarn lint:check --fix",
"_format": "prettier '**/*.{ts,tsx,json,md}'", "_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write", "format": "yarn _format --write",
"format:check": "yarn _format --list-different", "format:check": "yarn _format --list-different",
@ -28,9 +26,6 @@
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js" "download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [
"eslint --fix"
],
"*.{ts,tsx,json,md}": [ "*.{ts,tsx,json,md}": [
"prettier --write" "prettier --write"
] ]
@ -68,12 +63,9 @@
"react": "^17.0.1", "react": "^17.0.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"typescript": "^4.2.3", "typescript": "^4.2.3",
"@typescript-eslint/eslint-plugin": "^4.24.0",
"@typescript-eslint/parser": "^4.24.0",
"eslint": "^7.26.0",
"eslint-config-prettier": "^8.3.0",
"husky": "^4.3.8", "husky": "^4.3.8",
"lint-staged": "^11.0.0" "lint-staged": "^11.0.0",
"prettier": "^2.3.0"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.4.1", "@emotion/react": "^11.4.1",

View File

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

View File

@ -1,8 +1,15 @@
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources"; import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
import { generateJavaStackFiles } from "./generateJavaStackFiles"; import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path"; import {
join as pathJoin,
relative as pathRelative,
basename as pathBasename,
} from "path";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles"; import {
generateDebugFiles,
containerLaunchScriptBasename,
} from "./generateDebugFiles";
import { URL } from "url"; import { URL } from "url";
type ParsedPackageJson = { type ParsedPackageJson = {
@ -13,21 +20,34 @@ type ParsedPackageJson = {
const reactProjectDirPath = process.cwd(); const reactProjectDirPath = process.cwd();
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets"; const doUseExternalAssets =
process.argv[2]?.toLowerCase() === "--external-assets";
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json")); const parsedPackageJson: ParsedPackageJson = require(pathJoin(
reactProjectDirPath,
"package.json",
));
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak"); export const keycloakThemeBuildingDirPath = pathJoin(
reactProjectDirPath,
"build_keycloak",
);
function sanitizeThemeName(name: string) { function sanitizeThemeName(name: string) {
return name.replace(/^@(.*)/, '$1').split('/').join('-'); return name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
} }
export function main() { export function main() {
console.log("🔏 Building the keycloak theme...⌚"); console.log("🔏 Building the keycloak theme...⌚");
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? []; const extraPagesId: string[] =
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? []; (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
const extraThemeProperties: string[] =
(parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ??
[];
const themeName = sanitizeThemeName(parsedPackageJson.name); const themeName = sanitizeThemeName(parsedPackageJson.name);
generateKeycloakThemeResources({ generateKeycloakThemeResources({
@ -35,105 +55,111 @@ export function main() {
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"), "reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
themeName, themeName,
...(() => { ...(() => {
const url = (() => { const url = (() => {
const { homepage } = parsedPackageJson; const { homepage } = parsedPackageJson;
return homepage === undefined ? return homepage === undefined ? undefined : new URL(homepage);
undefined :
new URL(homepage);
})(); })();
return { return {
"urlPathname": "urlPathname":
url === undefined ? url === undefined
"/" : ? "/"
url.pathname.replace(/([^/])$/, "$1/"), : url.pathname.replace(/([^/])$/, "$1/"),
"urlOrigin": !doUseExternalAssets ? undefined : (() => { "urlOrigin": !doUseExternalAssets
? undefined
if (url === undefined) { : (() => {
console.error("ERROR: You must specify 'homepage' in your package.json"); if (url === undefined) {
process.exit(-1); console.error(
} "ERROR: You must specify 'homepage' in your package.json",
);
return url.origin; process.exit(-1);
}
})()
return url.origin;
})(),
}; };
})(), })(),
extraPagesId, extraPagesId,
extraThemeProperties, extraThemeProperties,
//We have to leave it at that otherwise we break our default theme. //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 common //Problem is that we can't guarantee that the the old resources common
//will still be available on the newer keycloak version. //will still be available on the newer keycloak version.
"keycloakVersion": "11.0.3" "keycloakVersion": "11.0.3",
}); });
const { jarFilePath } = generateJavaStackFiles({ const { jarFilePath } = generateJavaStackFiles({
version: parsedPackageJson.version, version: parsedPackageJson.version,
themeName, themeName,
homepage: parsedPackageJson.homepage, homepage: parsedPackageJson.homepage,
keycloakThemeBuildingDirPath keycloakThemeBuildingDirPath,
}); });
child_process.execSync( child_process.execSync("mvn package", {
"mvn package", "cwd": keycloakThemeBuildingDirPath,
{ "cwd": keycloakThemeBuildingDirPath } });
);
generateDebugFiles({ generateDebugFiles({
keycloakThemeBuildingDirPath, keycloakThemeBuildingDirPath,
themeName, themeName,
"keycloakVersion": "15.0.2" "keycloakVersion": "15.0.2",
}); });
console.log([ console.log(
'', [
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`, "",
`It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image.`, `✅ Your keycloak theme has been generated and bundled into ./${pathRelative(
'', reactProjectDirPath,
'Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:', jarFilePath,
'', )} 🚀`,
'value.yaml: ', `It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image.`,
' extraInitContainers: |', "",
' - name: realm-ext-provider', "Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
' image: curlimages/curl', "",
' imagePullPolicy: IfNotPresent', "value.yaml: ",
' command:', " extraInitContainers: |",
' - sh', " - name: realm-ext-provider",
' args:', " image: curlimages/curl",
' - -c', " imagePullPolicy: IfNotPresent",
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`, " command:",
' volumeMounts:', " - sh",
' - name: extensions', " args:",
' mountPath: /extensions', " - -c",
' ', ` - curl -L -f -S -o /extensions/${pathBasename(
' extraVolumeMounts: |', jarFilePath,
' - name: extensions', )} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
' mountPath: /opt/jboss/keycloak/standalone/deployments', " volumeMounts:",
' extraEnv: |', " - name: extensions",
' - name: KEYCLOAK_USER', " mountPath: /extensions",
' value: admin', " ",
' - name: KEYCLOAK_PASSWORD', " extraVolumeMounts: |",
' value: xxxxxxxxx', " - name: extensions",
' - name: JAVA_OPTS', " mountPath: /opt/jboss/keycloak/standalone/deployments",
' value: -Dkeycloak.profile=preview', " extraEnv: |",
'', " - name: KEYCLOAK_USER",
'', " value: admin",
'To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:', " - name: KEYCLOAK_PASSWORD",
'', " value: xxxxxxxxx",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename))} 👈`, " - name: JAVA_OPTS",
'', " value: -Dkeycloak.profile=preview",
'To enable the theme within keycloak log into the admin console ( 👉 http://localhost:8080 username: admin, password: admin 👈), create a realm (called "myrealm" for example),', "",
`go to your realm settings, click on the theme tab then select ${themeName}.`, "",
`More details: https://www.keycloak.org/getting-started/getting-started-docker`, "To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:",
'', "",
'Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈', `👉 $ ./${pathRelative(
'', reactProjectDirPath,
].join("\n")); pathJoin(
keycloakThemeBuildingDirPath,
containerLaunchScriptBasename,
),
)} 👈`,
"",
'To enable the theme within keycloak log into the admin console ( 👉 http://localhost:8080 username: admin, password: admin 👈), create a realm (called "myrealm" for example),',
`go to your realm settings, click on the theme tab then select ${themeName}.`,
`More details: https://www.keycloak.org/getting-started/getting-started-docker`,
"",
"Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈",
"",
].join("\n"),
);
} }

View File

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

View File

@ -1,18 +1,15 @@
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname, } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh"; export const containerLaunchScriptBasename =
"start_keycloak_testing_container.sh";
/** Files for being able to run a hot reload keycloak container */ /** Files for being able to run a hot reload keycloak container */
export function generateDebugFiles( export function generateDebugFiles(params: {
params: { keycloakVersion: "11.0.3" | "15.0.2";
keycloakVersion: "11.0.3" | "15.0.2"; themeName: string;
themeName: string; keycloakThemeBuildingDirPath: string;
keycloakThemeBuildingDirPath: string; }) {
}
) {
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params; const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
fs.writeFileSync( fs.writeFileSync(
@ -29,8 +26,8 @@ export function generateDebugFiles(
"", "",
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]', 'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
].join("\n"), ].join("\n"),
"utf8" "utf8",
) ),
); );
const dockerImage = `${themeName}/keycloak-hot-reload`; const dockerImage = `${themeName}/keycloak-hot-reload`;
@ -54,42 +51,53 @@ export function generateDebugFiles(
" -e KEYCLOAK_USER=admin \\", " -e KEYCLOAK_USER=admin \\",
" -e KEYCLOAK_PASSWORD=admin \\", " -e KEYCLOAK_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\", " -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v ${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName) ` -v ${pathJoin(
}:/opt/jboss/keycloak/themes/${themeName}:rw \\`, keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName,
)}:/opt/jboss/keycloak/themes/${themeName}:rw \\`,
` -it ${dockerImage}:latest`, ` -it ${dockerImage}:latest`,
"" "",
].join("\n"), ].join("\n"),
"utf8" "utf8",
), ),
{ "mode": 0o755 } { "mode": 0o755 },
); );
const standaloneHaFilePath = pathJoin(keycloakThemeBuildingDirPath, "configuration", `standalone-ha.xml`); const standaloneHaFilePath = pathJoin(
keycloakThemeBuildingDirPath,
"configuration",
`standalone-ha.xml`,
);
try { fs.mkdirSync(pathDirname(standaloneHaFilePath)); } catch { } try {
fs.mkdirSync(pathDirname(standaloneHaFilePath));
} catch {}
fs.writeFileSync( fs.writeFileSync(
standaloneHaFilePath, standaloneHaFilePath,
fs.readFileSync( fs
pathJoin( .readFileSync(
__dirname, pathJoin(__dirname, `standalone-ha_${keycloakVersion}.xml`),
`standalone-ha_${keycloakVersion}.xml`
) )
)
.toString("utf8") .toString("utf8")
.replace( .replace(
new RegExp([ new RegExp(
"<staticMaxAge>2592000</staticMaxAge>", [
"<cacheThemes>true</cacheThemes>", "<staticMaxAge>2592000</staticMaxAge>",
"<cacheTemplates>true</cacheTemplates>" "<cacheThemes>true</cacheThemes>",
].join("\\s*"), "g" "<cacheTemplates>true</cacheTemplates>",
].join("\\s*"),
"g",
), ),
[ [
"<staticMaxAge>-1</staticMaxAge>", "<staticMaxAge>-1</staticMaxAge>",
"<cacheThemes>false</cacheThemes>", "<cacheThemes>false</cacheThemes>",
"<cacheTemplates>false</cacheTemplates>" "<cacheTemplates>false</cacheTemplates>",
].join("\n") ].join("\n"),
) ),
); );
}
}

View File

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

View File

@ -2,7 +2,7 @@ import cheerio from "cheerio";
import { import {
replaceImportsFromStaticInJsCode, replaceImportsFromStaticInJsCode,
replaceImportsInInlineCssCode, replaceImportsInInlineCssCode,
generateCssCodeToDefineGlobals generateCssCodeToDefineGlobals,
} from "../replaceImportFromStatic"; } from "../replaceImportFromStatic";
import fs from "fs"; import fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
@ -10,62 +10,62 @@ import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName"; import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
export const pageIds = [ export const pageIds = [
"login.ftl", "register.ftl", "register-user-profile.ftl", "login.ftl",
"info.ftl", "error.ftl", "login-reset-password.ftl", "register.ftl",
"login-verify-email.ftl", "terms.ftl", "login-otp.ftl", "register-user-profile.ftl",
"login-update-profile.ftl", "login-idp-link-confirm.ftl" "info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-idp-link-confirm.ftl",
] as const; ] as const;
export type PageId = typeof pageIds[number]; export type PageId = typeof pageIds[number];
function loadAdjacentFile(fileBasename: string) { function loadAdjacentFile(fileBasename: string) {
return fs.readFileSync(pathJoin(__dirname, fileBasename)) return fs.readFileSync(pathJoin(__dirname, fileBasename)).toString("utf8");
.toString("utf8"); }
};
export function generateFtlFilesCodeFactory(params: {
export function generateFtlFilesCodeFactory( cssGlobalsToDefine: Record<string, string>;
params: { indexHtmlCode: string;
cssGlobalsToDefine: Record<string, string>; urlPathname: string;
indexHtmlCode: string; urlOrigin: undefined | string;
urlPathname: string; }) {
urlOrigin: undefined | string; const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } =
} params;
) {
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
$("script:not([src])").each((...[, element]) => { $("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!, "jsCode": $(element).html()!,
urlOrigin urlOrigin,
}); });
$(element).text(fixedJsCode); $(element).text(fixedJsCode);
}); });
$("style").each((...[, element]) => { $("style").each((...[, element]) => {
const { fixedCssCode } = replaceImportsInInlineCssCode({ const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!, "cssCode": $(element).html()!,
"urlPathname": params.urlPathname, "urlPathname": params.urlPathname,
urlOrigin urlOrigin,
}); });
$(element).text(fixedCssCode); $(element).text(fixedCssCode);
}); });
([ (
["link", "href"], [
["script", "src"], ["link", "href"],
] as const).forEach(([selector, attrName]) => ["script", "src"],
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => { $(selector).each((...[, element]) => {
const href = $(element).attr(attrName); const href = $(element).attr(attrName);
if (href === undefined) { if (href === undefined) {
@ -74,94 +74,90 @@ export function generateFtlFilesCodeFactory(
$(element).attr( $(element).attr(
attrName, attrName,
urlOrigin !== undefined ? urlOrigin !== undefined
href.replace(/^\//, `${urlOrigin}/`) : ? href.replace(/^\//, `${urlOrigin}/`)
href.replace( : href.replace(
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`), new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
"${url.resourcesPath}/build/" "${url.resourcesPath}/build/",
) ),
); );
}),
})
); );
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later. //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlPlaceholders = { const ftlPlaceholders = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl") '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile(
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1], "common.ftl",
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->': ).match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
[ "<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
'<#if scripts??>', "<#if scripts??>",
' <#list scripts as script>', " <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>', ' <script src="${script}" type="text/javascript"></script>',
' </#list>', " </#list>",
'</#if>' "</#if>",
].join("\n") ].join("\n"),
}; };
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->"; const pageSpecificCodePlaceholder =
"<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
$("head").prepend( $("head").prepend(
[ [
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [ ...(Object.keys(cssGlobalsToDefine).length === 0
'', ? []
'<style>', : [
generateCssCodeToDefineGlobals({ "",
cssGlobalsToDefine, "<style>",
urlPathname generateCssCodeToDefineGlobals({
}).cssCodeToPrependInHead, cssGlobalsToDefine,
'</style>', urlPathname,
'' }).cssCodeToPrependInHead,
]), "</style>",
"",
]),
"<script>", "<script>",
loadAdjacentFile("Object.deepAssign.js"), loadAdjacentFile("Object.deepAssign.js"),
"</script>", "</script>",
'<script>', "<script>",
` window.${ftlValuesGlobalName}= Object.assign(`, ` window.${ftlValuesGlobalName}= Object.assign(`,
` {},`, ` {},`,
` ${objectKeys(ftlPlaceholders)[0]}`, ` ${objectKeys(ftlPlaceholders)[0]}`,
' );', " );",
'</script>', "</script>",
'', "",
pageSpecificCodePlaceholder, pageSpecificCodePlaceholder,
'', "",
objectKeys(ftlPlaceholders)[1] objectKeys(ftlPlaceholders)[1],
].join("\n"), ].join("\n"),
); );
const partiallyFixedIndexHtmlCode = $.html(); const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode( function generateFtlFilesCode(params: { pageId: string }): {
params: { ftlCode: string;
pageId: string; } {
}
): { ftlCode: string; } {
const { pageId } = params; const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode); const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html() let ftlCode = $.html().replace(
.replace( pageSpecificCodePlaceholder,
pageSpecificCodePlaceholder, [
[ "<script>",
'<script>', ` Object.deepAssign(`,
` Object.deepAssign(`, ` window.${ftlValuesGlobalName},`,
` window.${ftlValuesGlobalName},`, ` { "pageId": "${pageId}" }`,
` { "pageId": "${pageId}" }`, " );",
' );', "</script>",
'</script>' ].join("\n"),
].join("\n") );
);
objectKeys(ftlPlaceholders) objectKeys(ftlPlaceholders).forEach(
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id])); id => (ftlCode = ftlCode.replace(id, ftlPlaceholders[id])),
);
return { ftlCode }; return { ftlCode };
} }
return { generateFtlFilesCode }; return { generateFtlFilesCode };
}
}

View File

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

View File

@ -1,39 +1,33 @@
import * as url from "url"; import * as url from "url";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
export function generateJavaStackFiles(params: {
export function generateJavaStackFiles( version: string;
params: { themeName: string;
version: string; homepage?: string;
themeName: string; keycloakThemeBuildingDirPath: string;
homepage?: string; }): { jarFilePath: string } {
keycloakThemeBuildingDirPath: string; const { themeName, version, homepage, keycloakThemeBuildingDirPath } =
} params;
): { jarFilePath: string; } {
const {
themeName,
version,
homepage,
keycloakThemeBuildingDirPath
} = params;
{ {
const { pomFileCode } = (function generatePomFileCode(): {
const { pomFileCode } = (function generatePomFileCode(): { pomFileCode: string; } { pomFileCode: string;
} {
const groupId = (() => { const groupId = (() => {
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`; const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
return (!homepage ? return (
fallbackGroupId : (!homepage
url.parse(homepage).host?.replace(/:[0-9]+$/,"")?.split(".").reverse().join(".") ?? fallbackGroupId ? fallbackGroupId
) + ".keycloak"; : url
.parse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(); })();
const artefactId = `${themeName}-keycloak-theme`; const artefactId = `${themeName}-keycloak-theme`;
@ -49,51 +43,57 @@ export function generateJavaStackFiles(
` <version>${version}</version>`, ` <version>${version}</version>`,
` <name>${artefactId}</name>`, ` <name>${artefactId}</name>`,
` <description />`, ` <description />`,
`</project>` `</project>`,
].join("\n"); ].join("\n");
return { pomFileCode }; return { pomFileCode };
})(); })();
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), pathJoin(keycloakThemeBuildingDirPath, "pom.xml"),
Buffer.from(pomFileCode, "utf8") Buffer.from(pomFileCode, "utf8"),
); );
} }
{ {
const themeManifestFilePath = pathJoin( const themeManifestFilePath = pathJoin(
keycloakThemeBuildingDirPath, "src", "main", keycloakThemeBuildingDirPath,
"resources", "META-INF", "keycloak-themes.json" "src",
"main",
"resources",
"META-INF",
"keycloak-themes.json",
); );
try { try {
fs.mkdirSync(pathDirname(themeManifestFilePath)); fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
} catch { }
fs.writeFileSync( fs.writeFileSync(
themeManifestFilePath, themeManifestFilePath,
Buffer.from( Buffer.from(
JSON.stringify({ JSON.stringify(
"themes": [ {
{ "themes": [
"name": themeName, {
"types": ["login"] "name": themeName,
} "types": ["login"],
] },
}, null, 2), ],
"utf8" },
) null,
2,
),
"utf8",
),
); );
} }
return { "jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`) }; return {
"jarFilePath": pathJoin(
keycloakThemeBuildingDirPath,
"target",
`${themeName}-${version}.jar`,
),
};
} }

View File

@ -1,178 +1,197 @@
import { transformCodebase } from "../tools/transformCodebase"; import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { import {
replaceImportsInCssCode, replaceImportsInCssCode,
replaceImportsFromStaticInJsCode replaceImportsFromStaticInJsCode,
} from "./replaceImportFromStatic"; } from "./replaceImportFromStatic";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl"; import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import * as child_process from "child_process"; import * as child_process from "child_process";
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/getKcContext/kcContextMocks/urlResourcesPath"; import {
resourcesCommonPath,
resourcesPath,
subDirOfPublicDirBasename,
} from "../../lib/getKcContext/kcContextMocks/urlResourcesPath";
import { isInside } from "../tools/isInside"; import { isInside } from "../tools/isInside";
export function generateKeycloakThemeResources(params: {
export function generateKeycloakThemeResources( themeName: string;
params: { reactAppBuildDirPath: string;
themeName: string; keycloakThemeBuildingDirPath: string;
reactAppBuildDirPath: string; urlPathname: string;
keycloakThemeBuildingDirPath: string; //If urlOrigin is not undefined then it means --externals-assets
urlPathname: string; urlOrigin: undefined | string;
//If urlOrigin is not undefined then it means --externals-assets extraPagesId: string[];
urlOrigin: undefined | string; extraThemeProperties: string[];
extraPagesId: string[]; keycloakVersion: "11.0.3" | "15.0.2";
extraThemeProperties: string[]; }) {
keycloakVersion: "11.0.3" | "15.0.2" const {
} themeName,
) { reactAppBuildDirPath,
keycloakThemeBuildingDirPath,
const { urlPathname,
themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlOrigin,
urlPathname, urlOrigin, extraPagesId, extraThemeProperties, extraPagesId,
keycloakVersion extraThemeProperties,
keycloakVersion,
} = params; } = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login"); const themeDirPath = pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName,
"login",
);
let allCssGlobalsToDefine: Record<string, string> = {}; let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({ transformCodebase({
"destDirPath": "destDirPath":
urlOrigin === undefined ? urlOrigin === undefined
pathJoin(themeDirPath, "resources", "build") : ? pathJoin(themeDirPath, "resources", "build")
reactAppBuildDirPath, : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath, "srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => { "transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/ //NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if ( if (
urlOrigin === undefined && urlOrigin === undefined &&
isInside({ isInside({
"dirPath": pathJoin(reactAppBuildDirPath, subDirOfPublicDirBasename), "dirPath": pathJoin(
filePath reactAppBuildDirPath,
subDirOfPublicDirBasename,
),
filePath,
}) })
) { ) {
return undefined; return undefined;
} }
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) { if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine, fixedCssCode } =
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode( replaceImportsInCssCode({
{ "cssCode": sourceCode.toString("utf8") } "cssCode": sourceCode.toString("utf8"),
); });
allCssGlobalsToDefine = { allCssGlobalsToDefine = {
...allCssGlobalsToDefine, ...allCssGlobalsToDefine,
...cssGlobalsToDefine ...cssGlobalsToDefine,
}; };
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") }; return {
"modifiedSourceCode": Buffer.from(fixedCssCode, "utf8"),
};
} }
if (/\.js?$/i.test(filePath)) { if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"), "jsCode": sourceCode.toString("utf8"),
urlOrigin urlOrigin,
}); });
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") }; return {
"modifiedSourceCode": Buffer.from(fixedJsCode, "utf8"),
};
} }
return urlOrigin === undefined ? return urlOrigin === undefined
{ "modifiedSourceCode": sourceCode } : ? { "modifiedSourceCode": sourceCode }
undefined; : undefined;
},
}
}); });
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({ const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine, "cssGlobalsToDefine": allCssGlobalsToDefine,
"indexHtmlCode": fs.readFileSync( "indexHtmlCode": fs
pathJoin(reactAppBuildDirPath, "index.html") .readFileSync(pathJoin(reactAppBuildDirPath, "index.html"))
).toString("utf8"), .toString("utf8"),
urlPathname, urlPathname,
urlOrigin urlOrigin,
}); });
[...pageIds, ...extraPagesId].forEach(pageId => { [...pageIds, ...extraPagesId].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId }); const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true }); fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeDirPath, pageId), pathJoin(themeDirPath, pageId),
Buffer.from(ftlCode, "utf8") Buffer.from(ftlCode, "utf8"),
); );
}); });
{ {
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd"); const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
downloadBuiltinKeycloakTheme({ downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
}); });
const themeResourcesDirPath = pathJoin(themeDirPath, "resources"); const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"), "srcDirPath": pathJoin(
"destDirPath": themeResourcesDirPath tmpDirPath,
"keycloak",
"login",
"resources",
),
"destDirPath": themeResourcesDirPath,
}); });
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public"); const reactAppPublicDirPath = pathJoin(
reactAppBuildDirPath,
"..",
"public",
);
transformCodebase({ transformCodebase({
"srcDirPath": themeResourcesDirPath, "srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin( "destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
reactAppPublicDirPath,
resourcesPath
)
}); });
transformCodebase({ transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"), "srcDirPath": pathJoin(
"destDirPath": pathJoin( tmpDirPath,
reactAppPublicDirPath, "keycloak",
resourcesCommonPath "common",
) "resources",
),
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesCommonPath),
}); });
const keycloakResourcesWithinPublicDirPath = const keycloakResourcesWithinPublicDirPath = pathJoin(
pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename); reactAppPublicDirPath,
subDirOfPublicDirBasename,
);
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"), pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from([ Buffer.from(
"This is just a test folder that helps develop", [
"the login and register page without having to yarn build" "This is just a test folder that helps develop",
].join(" ")) "the login and register page without having to yarn build",
].join(" "),
),
); );
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"),
Buffer.from("*", "utf8") Buffer.from("*", "utf8"),
); );
child_process.execSync(`rm -r ${tmpDirPath}`); child_process.execSync(`rm -r ${tmpDirPath}`);
} }
fs.writeFileSync( fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"), pathJoin(themeDirPath, "theme.properties"),
Buffer.from( Buffer.from(
"parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")),
"utf8" "utf8",
) ),
); );
} }

View File

@ -4,7 +4,5 @@ export * from "./build-keycloak-theme";
import { main } from "./build-keycloak-theme"; import { main } from "./build-keycloak-theme";
if (require.main === module) { if (require.main === module) {
main();
main(); }
}

View File

@ -1,14 +1,10 @@
import * as crypto from "crypto"; import * as crypto from "crypto";
import { ftlValuesGlobalName } from "./ftlValuesGlobalName"; import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
export function replaceImportsFromStaticInJsCode( export function replaceImportsFromStaticInJsCode(params: {
params: { jsCode: string;
jsCode: string; urlOrigin: undefined | string;
urlOrigin: undefined | string; }): { fixedJsCode: string } {
}
): { fixedJsCode: string; } {
/* /*
NOTE: NOTE:
@ -23,114 +19,104 @@ export function replaceImportsFromStaticInJsCode(
const { jsCode, urlOrigin } = params; const { jsCode, urlOrigin } = params;
const fixedJsCode = const fixedJsCode = jsCode
jsCode .replace(/([a-z]+\.[a-z]+)\+"static\//g, (...[, group]) =>
.replace( urlOrigin === undefined
/([a-z]+\.[a-z]+)\+"static\//g, ? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
(...[, group]) => : `("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`,
urlOrigin === undefined ? )
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` : .replace(
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/` /".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/,
) (...[, group1, group2, group3]) =>
.replace( urlOrigin === undefined
/".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/, ? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
(...[, group1, group2, group3]) => : `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
urlOrigin === undefined ? );
`".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},` :
`".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`
);
return { fixedJsCode }; return { fixedJsCode };
} }
export function replaceImportsInInlineCssCode( export function replaceImportsInInlineCssCode(params: {
params: { cssCode: string;
cssCode: string; urlPathname: string;
urlPathname: string; urlOrigin: undefined | string;
urlOrigin: undefined | string; }): { fixedCssCode: string } {
}
): { fixedCssCode: string; } {
const { cssCode, urlPathname, urlOrigin } = params; const { cssCode, urlPathname, urlOrigin } = params;
const fixedCssCode = cssCode.replace( const fixedCssCode = cssCode.replace(
urlPathname === "/" ? urlPathname === "/"
/url\(\/([^/][^)]+)\)/g : ? /url\(\/([^/][^)]+)\)/g
new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"), : new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"),
(...[, group]) => `url(${urlOrigin === undefined ? (...[, group]) =>
"${url.resourcesPath}/build/" + group : `url(${
params.urlOrigin + urlPathname + group})` urlOrigin === undefined
? "${url.resourcesPath}/build/" + group
: params.urlOrigin + urlPathname + group
})`,
); );
return { fixedCssCode }; return { fixedCssCode };
} }
export function replaceImportsInCssCode( export function replaceImportsInCssCode(params: { cssCode: string }): {
params: {
cssCode: string;
}
): {
fixedCssCode: string; fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>; cssGlobalsToDefine: Record<string, string>;
} { } {
const { cssCode } = params; const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {}; const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? []) new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? []).forEach(
.forEach(match => match =>
cssGlobalsToDefine[ (cssGlobalsToDefine[
"url" + crypto "url" +
.createHash("sha256") crypto
.update(match) .createHash("sha256")
.digest("hex") .update(match)
.substring(0, 15) .digest("hex")
] = match .substring(0, 15)
); ] = match),
);
let fixedCssCode = cssCode; let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach( Object.keys(cssGlobalsToDefine).forEach(
cssVariableName => cssVariableName =>
//NOTE: split/join pattern ~ replace all //NOTE: split/join pattern ~ replace all
fixedCssCode = (fixedCssCode = fixedCssCode
fixedCssCode.split(cssGlobalsToDefine[cssVariableName]) .split(cssGlobalsToDefine[cssVariableName])
.join(`var(--${cssVariableName})`) .join(`var(--${cssVariableName})`)),
); );
return { fixedCssCode, cssGlobalsToDefine }; return { fixedCssCode, cssGlobalsToDefine };
} }
export function generateCssCodeToDefineGlobals( export function generateCssCodeToDefineGlobals(params: {
params: { cssGlobalsToDefine: Record<string, string>;
cssGlobalsToDefine: Record<string, string>; urlPathname: string;
urlPathname: string; }): {
}
): {
cssCodeToPrependInHead: string; cssCodeToPrependInHead: string;
} { } {
const { cssGlobalsToDefine, urlPathname } = params; const { cssGlobalsToDefine, urlPathname } = params;
return { return {
"cssCodeToPrependInHead": [ "cssCodeToPrependInHead": [
":root {", ":root {",
...Object.keys(cssGlobalsToDefine) ...Object.keys(cssGlobalsToDefine)
.map(cssVariableName => [ .map(cssVariableName =>
`--${cssVariableName}:`, [
cssGlobalsToDefine[cssVariableName] `--${cssVariableName}:`,
.replace(new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"), "url(${url.resourcesPath}/build/") cssGlobalsToDefine[cssVariableName].replace(
].join(" ")) new RegExp(
`url\\(${urlPathname.replace(/\//g, "\\/")}`,
"g",
),
"url(${url.resourcesPath}/build/",
),
].join(" "),
)
.map(line => ` ${line};`), .map(line => ` ${line};`),
"}" "}",
].join("\n") ].join("\n"),
}; };
} }

View File

@ -2,52 +2,49 @@
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme"; import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip" import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import type { KeycloakVersion } from "./KeycloakVersion"; import type { KeycloakVersion } from "./KeycloakVersion";
export function downloadBuiltinKeycloakTheme( export function downloadBuiltinKeycloakTheme(params: {
params: { keycloakVersion: KeycloakVersion;
keycloakVersion: KeycloakVersion; destDirPath: string;
destDirPath: string; }) {
}
) {
const { keycloakVersion, destDirPath } = params; const { keycloakVersion, destDirPath } = params;
for (const ext of ["", "-community"]) { for (const ext of ["", "-community"]) {
downloadAndUnzip({ downloadAndUnzip({
"destDirPath": destDirPath, "destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`, "url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme` "pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
}); });
} }
} }
if (require.main === module) { if (require.main === module) {
const keycloakVersion = (() => { const keycloakVersion = (() => {
const keycloakVersion = process.argv[2] as KeycloakVersion | undefined;
const keycloakVersion = process.argv[2] as (KeycloakVersion | undefined);
if (keycloakVersion === undefined) { if (keycloakVersion === undefined) {
return "15.0.2"; return "15.0.2";
} }
return keycloakVersion; return keycloakVersion;
})(); })();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme"); const destDirPath = pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
);
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`); console.log(
`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`,
);
downloadBuiltinKeycloakTheme({ downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
destDirPath destDirPath,
}); });
} }

View File

@ -11,7 +11,6 @@ import { keycloakVersions } from "./KeycloakVersion";
const propertiesParser = require("properties-parser"); const propertiesParser = require("properties-parser");
for (const keycloakVersion of keycloakVersions) { for (const keycloakVersion of keycloakVersions) {
console.log({ keycloakVersion }); console.log({ keycloakVersion });
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44"); const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
@ -20,20 +19,21 @@ for (const keycloakVersion of keycloakVersions) {
downloadBuiltinKeycloakTheme({ downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
}); });
type Dictionary = { [idiomId: string]: string }; type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {}; const record: { [typeOfPage: string]: { [language: string]: Dictionary } } =
{};
{ {
const baseThemeDirPath = pathJoin(tmpDirPath, "base"); const baseThemeDirPath = pathJoin(tmpDirPath, "base");
crawl(baseThemeDirPath).forEach(filePath => { crawl(baseThemeDirPath).forEach(filePath => {
const match = filePath.match(
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/); /^([^/]+)\/messages\/messages_([^.]+)\.properties$/,
);
if (match === null) { if (match === null) {
return; return;
@ -45,43 +45,58 @@ for (const keycloakVersion of keycloakVersions) {
Object.fromEntries( Object.fromEntries(
Object.entries( Object.entries(
propertiesParser.parse( propertiesParser.parse(
fs.readFileSync( fs
pathJoin(baseThemeDirPath, filePath) .readFileSync(
) pathJoin(baseThemeDirPath, filePath),
.toString("utf8") )
) .toString("utf8"),
).map(([key, value]: any) => [key, value.replace(/''/g, "'")]) ),
).map(([key, value]: any) => [
key,
value.replace(/''/g, "'"),
]),
); );
}); });
} }
rm_r(tmpDirPath); rm_r(tmpDirPath);
const targetDirPath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_kcMessages", keycloakVersion); const targetDirPath = pathJoin(
getProjectRoot(),
"src",
"lib",
"i18n",
"generated_kcMessages",
keycloakVersion,
);
fs.mkdirSync(targetDirPath, { "recursive": true }); fs.mkdirSync(targetDirPath, { "recursive": true });
Object.keys(record).forEach(pageType => { Object.keys(record).forEach(pageType => {
const filePath = pathJoin(targetDirPath, `${pageType}.ts`); const filePath = pathJoin(targetDirPath, `${pageType}.ts`);
fs.writeFileSync( fs.writeFileSync(
filePath, filePath,
Buffer.from( Buffer.from(
[ [
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`, `//This code was automatically generated by running ${pathRelative(
'//PLEASE DO NOT EDIT MANUALLY', getProjectRoot(),
'', __filename,
'/* spell-checker: disable */', )}`,
`export const kcMessages= ${JSON.stringify(record[pageType], null, 2)};`, "//PLEASE DO NOT EDIT MANUALLY",
'/* spell-checker: enable */' "",
].join("\n"), "utf8") "/* spell-checker: disable */",
`export const kcMessages= ${JSON.stringify(
record[pageType],
null,
2,
)};`,
"/* spell-checker: enable */",
].join("\n"),
"utf8",
),
); );
console.log(`${filePath} wrote`); console.log(`${filePath} wrote`);
}); });
} }

View File

@ -1,4 +1,3 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs"; import * as fs from "fs";
@ -12,7 +11,9 @@ fs.writeFileSync(
(() => { (() => {
const packageJsonParsed = JSON.parse( const packageJsonParsed = JSON.parse(
fs fs
.readFileSync(pathJoin(keycloakifyDirPath, "package.json")) .readFileSync(
pathJoin(keycloakifyDirPath, "package.json"),
)
.toString("utf8"), .toString("utf8"),
); );
@ -31,7 +32,13 @@ fs.writeFileSync(
const commonThirdPartyDeps = (() => { const commonThirdPartyDeps = (() => {
const namespaceModuleNames = ["@emotion"]; const namespaceModuleNames = ["@emotion"];
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"]; const standaloneModuleNames = [
"react",
"@types/react",
"powerhooks",
"tss-react",
"evt",
];
return [ return [
...namespaceModuleNames ...namespaceModuleNames
@ -69,7 +76,9 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
...(targetModuleName !== undefined ? [targetModuleName] : []), ...(targetModuleName !== undefined ? [targetModuleName] : []),
].join(" "); ].join(" ");
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`); console.log(
`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`,
);
execSync(cmd, { execSync(cmd, {
cwd, cwd,
@ -128,4 +137,4 @@ testAppNames.forEach(testAppName =>
"cwd": getTestAppPath(testAppName), "cwd": getTestAppPath(testAppName),
"targetModuleName": "keycloakify", "targetModuleName": "keycloakify",
}), }),
); );

View File

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

View File

@ -1,4 +1,3 @@
import { basename as pathBasename, join as pathJoin } from "path"; import { basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process"; import { execSync } from "child_process";
import fs from "fs"; import fs from "fs";
@ -6,14 +5,11 @@ import { transformCodebase } from "../tools/transformCodebase";
import { rm_rf, rm, rm_r } from "./rm"; import { rm_rf, rm, rm_r } from "./rm";
/** assert url ends with .zip */ /** assert url ends with .zip */
export function downloadAndUnzip( export function downloadAndUnzip(params: {
params: { url: string;
url: string; destDirPath: string;
destDirPath: string; pathOfDirToExtractInArchive?: string;
pathOfDirToExtractInArchive?: string; }) {
}
) {
const { url, destDirPath, pathOfDirToExtractInArchive } = params; const { url, destDirPath, pathOfDirToExtractInArchive } = params;
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx"); const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
@ -25,23 +21,23 @@ export function downloadAndUnzip(
execSync(`wget ${url}`, { "cwd": tmpDirPath }); execSync(`wget ${url}`, { "cwd": tmpDirPath });
execSync( execSync(
`unzip ${pathBasename(url) `unzip ${pathBasename(url)}${
}${pathOfDirToExtractInArchive === undefined ? pathOfDirToExtractInArchive === undefined
"" : ` "${pathOfDirToExtractInArchive}/*"` ? ""
: ` "${pathOfDirToExtractInArchive}/*"`
}`, }`,
{ "cwd": tmpDirPath } { "cwd": tmpDirPath },
); );
rm(pathBasename(url), { "cwd": tmpDirPath }); rm(pathBasename(url), { "cwd": tmpDirPath });
transformCodebase({ transformCodebase({
"srcDirPath": pathOfDirToExtractInArchive === undefined ? "srcDirPath":
tmpDirPath : pathOfDirToExtractInArchive === undefined
pathJoin(tmpDirPath, pathOfDirToExtractInArchive) ? tmpDirPath
, : pathJoin(tmpDirPath, pathOfDirToExtractInArchive),
destDirPath, destDirPath,
}); });
rm_r(tmpDirPath); rm_r(tmpDirPath);
}
}

View File

@ -16,4 +16,4 @@ export function getProjectRoot(): string {
} }
return (result = getProjectRootRec(__dirname)); return (result = getProjectRootRec(__dirname));
} }

View File

@ -1,8 +1,11 @@
import { getProjectRoot } from "./getProjectRoot";
import { join as pathJoin } from "path";
import child_process from "child_process";
import { getProjectRoot } from "./getProjectRoot"; Object.entries<string>(
import { join as pathJoin } from "path"; require(pathJoin(getProjectRoot(), "package.json"))["bin"],
import child_process from "child_process"; ).forEach(([, scriptPath]) =>
child_process.execSync(`chmod +x ${scriptPath}`, {
Object.entries<string>(require(pathJoin(getProjectRoot(), "package.json"))["bin"]) "cwd": getProjectRoot(),
.forEach(([, scriptPath]) => child_process.execSync(`chmod +x ${scriptPath}`, { "cwd": getProjectRoot() })); }),
);

View File

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

View File

@ -1,42 +1,38 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
function rmInternal( function rmInternal(params: {
params: { pathToRemove: string;
pathToRemove: string; args: string | undefined;
args: string | undefined; cwd: string | undefined;
cwd: string | undefined; }) {
} const { pathToRemove, args, cwd } = params;
) {
const { pathToRemove, args, cwd } = params; execSync(
`rm ${args ? `-${args} ` : ""}${pathToRemove.replace(/ /g, "\\ ")}`,
execSync( cwd !== undefined ? { cwd } : undefined,
`rm ${args ? `-${args} ` : ""}${pathToRemove.replace(/\ /g, "\\ ")}`, );
cwd !== undefined ? { cwd } : undefined
);
} }
export function rm(pathToRemove: string, options?: { cwd: string; }) { export function rm(pathToRemove: string, options?: { cwd: string }) {
rmInternal({ rmInternal({
pathToRemove, pathToRemove,
"args": undefined, "args": undefined,
"cwd": options?.cwd, "cwd": options?.cwd,
}); });
} }
export function rm_r(pathToRemove: string, options?: { cwd: string; }) { export function rm_r(pathToRemove: string, options?: { cwd: string }) {
rmInternal({ rmInternal({
pathToRemove, pathToRemove,
"args": "r", "args": "r",
"cwd": options?.cwd, "cwd": options?.cwd,
}); });
} }
export function rm_rf(pathToRemove: string, options?: { cwd: string; }) { export function rm_rf(pathToRemove: string, options?: { cwd: string }) {
rmInternal({ rmInternal({
pathToRemove, pathToRemove,
"args": "rf", "args": "rf",
"cwd": options?.cwd, "cwd": options?.cwd,
}); });
} }

View File

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

View File

@ -4,33 +4,31 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error; } & KcProps) => { export const Error = memo(
({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => {
const { msg } = useKcMessage(); const { msg } = useKcMessage();
const { message, client } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{
client !== undefined && client.baseUrl !== undefined &&
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
}
</div>
}
/>
);
});
const { message, client } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{client !== undefined && client.baseUrl !== undefined && (
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
)}
</div>
}
/>
);
},
);

View File

@ -1,4 +1,3 @@
import { memo } from "react"; import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -6,68 +5,75 @@ import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info; } & KcProps) => { export const Info = memo(
({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => {
const { msg } = useKcMessage();
const { msg } = useKcMessage(); assert(kcContext.message !== undefined);
assert(kcContext.message !== undefined); const {
messageHeader,
const { message,
messageHeader, requiredActions,
message, skipLink,
requiredActions, pageRedirectUri,
skipLink, actionUri,
pageRedirectUri, client,
actionUri, } = kcContext;
client
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={
messageHeader !== undefined ?
<>{messageHeader}</>
:
<>{message.summary}</>
}
formNode={
<div id="kc-info-message">
<p className="instruction">{message.summary}
{
requiredActions !== undefined &&
<b>
{
requiredActions
.map(requiredAction => msg(`requiredAction.${requiredAction}` as const))
.join(",")
}
</b>
}
</p>
{
!skipLink &&
pageRedirectUri !== undefined ?
<p><a href={pageRedirectUri}>{(msg("backToApplication"))}</a></p>
:
actionUri !== undefined ?
<p><a href={actionUri}>{msg("proceedWithAction")}</a></p>
:
client.baseUrl !== undefined &&
<p><a href={client.baseUrl}>{msg("backToApplication")}</a></p>
}
</div>
}
/>
);
});
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={
messageHeader !== undefined ? (
<>{messageHeader}</>
) : (
<>{message.summary}</>
)
}
formNode={
<div id="kc-info-message">
<p className="instruction">
{message.summary}
{requiredActions !== undefined && (
<b>
{requiredActions
.map(requiredAction =>
msg(
`requiredAction.${requiredAction}` as const,
),
)
.join(",")}
</b>
)}
</p>
{!skipLink && pageRedirectUri !== undefined ? (
<p>
<a href={pageRedirectUri}>
{msg("backToApplication")}
</a>
</p>
) : actionUri !== undefined ? (
<p>
<a href={actionUri}>
{msg("proceedWithAction")}
</a>
</p>
) : (
client.baseUrl !== undefined && (
<p>
<a href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
)
)}
</div>
}
/>
);
},
);

View File

@ -1,4 +1,3 @@
import { memo } from "react"; import { memo } from "react";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -14,18 +13,31 @@ import { LoginOtp } from "./LoginOtp";
import { LoginUpdateProfile } from "./LoginUpdateProfile"; import { LoginUpdateProfile } from "./LoginUpdateProfile";
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm"; import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase; } & KcProps) => { export const KcApp = memo(
switch (kcContext.pageId) { ({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
case "login.ftl": return <Login {...{ kcContext, ...props }} />; switch (kcContext.pageId) {
case "register.ftl": return <Register {...{ kcContext, ...props }} />; case "login.ftl":
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...props }} />; return <Login {...{ kcContext, ...props }} />;
case "info.ftl": return <Info {...{ kcContext, ...props }} />; case "register.ftl":
case "error.ftl": return <Error {...{ kcContext, ...props }} />; return <Register {...{ kcContext, ...props }} />;
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />; case "register-user-profile.ftl":
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />; return <RegisterUserProfile {...{ kcContext, ...props }} />;
case "terms.ftl": return <Terms {...{ kcContext, ...props }} />; case "info.ftl":
case "login-otp.ftl": return <LoginOtp {...{ kcContext, ...props }} />; return <Info {...{ kcContext, ...props }} />;
case "login-update-profile.ftl": return <LoginUpdateProfile {...{ kcContext, ...props }} />; case "error.ftl":
case "login-idp-link-confirm.ftl": return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />; return <Error {...{ kcContext, ...props }} />;
} case "login-reset-password.ftl":
}); return <LoginResetPassword {...{ kcContext, ...props }} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, ...props }} />;
case "terms.ftl":
return <Terms {...{ kcContext, ...props }} />;
case "login-otp.ftl":
return <LoginOtp {...{ kcContext, ...props }} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
}
},
);

View File

@ -1,39 +1,39 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined"; import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
/** Class names can be provided as an array or separated by whitespace */ /** Class names can be provided as an array or separated by whitespace */
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; }; export type KcPropsGeneric<CssClasses extends string> = {
[key in CssClasses]: readonly string[] | string | undefined;
};
export type KcTemplateClassKey = export type KcTemplateClassKey =
"stylesCommon" | | "stylesCommon"
"styles" | | "styles"
"scripts" | | "scripts"
"kcHtmlClass" | | "kcHtmlClass"
"kcLoginClass" | | "kcLoginClass"
"kcHeaderClass" | | "kcHeaderClass"
"kcHeaderWrapperClass" | | "kcHeaderWrapperClass"
"kcFormCardClass" | | "kcFormCardClass"
"kcFormCardAccountClass" | | "kcFormCardAccountClass"
"kcFormHeaderClass" | | "kcFormHeaderClass"
"kcLocaleWrapperClass" | | "kcLocaleWrapperClass"
"kcContentWrapperClass" | | "kcContentWrapperClass"
"kcLabelWrapperClass" | | "kcLabelWrapperClass"
"kcContentWrapperClass" | | "kcContentWrapperClass"
"kcLabelWrapperClass" | | "kcLabelWrapperClass"
"kcFormGroupClass" | | "kcFormGroupClass"
"kcResetFlowIcon" | | "kcResetFlowIcon"
"kcResetFlowIcon" | | "kcResetFlowIcon"
"kcFeedbackSuccessIcon" | | "kcFeedbackSuccessIcon"
"kcFeedbackWarningIcon" | | "kcFeedbackWarningIcon"
"kcFeedbackErrorIcon" | | "kcFeedbackErrorIcon"
"kcFeedbackInfoIcon" | | "kcFeedbackInfoIcon"
"kcContentWrapperClass" | | "kcContentWrapperClass"
"kcFormSocialAccountContentClass" | | "kcFormSocialAccountContentClass"
"kcFormSocialAccountClass" | | "kcFormSocialAccountClass"
"kcSignUpClass" | | "kcSignUpClass"
"kcInfoAreaWrapperClass" | "kcInfoAreaWrapperClass";
;
export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>; export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>;
@ -41,7 +41,7 @@ export const defaultKcTemplateProps = {
"stylesCommon": [ "stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css" "lib/zocial/zocial.css",
], ],
"styles": ["css/login.css"], "styles": ["css/login.css"],
"scripts": [], "scripts": [],
@ -64,69 +64,69 @@ export const defaultKcTemplateProps = {
"kcFormGroupClass": ["form-group"], "kcFormGroupClass": ["form-group"],
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"], "kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcSignUpClass": ["login-pf-signup"], "kcSignUpClass": ["login-pf-signup"],
"kcInfoAreaWrapperClass": [] "kcInfoAreaWrapperClass": [],
} as const; } as const;
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>(); assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
/** Tu use if you don't want any default */ /** Tu use if you don't want any default */
export const allClearKcTemplateProps = export const allClearKcTemplateProps = allPropertiesValuesToUndefined(
allPropertiesValuesToUndefined(defaultKcTemplateProps); defaultKcTemplateProps,
);
assert<typeof allClearKcTemplateProps extends KcTemplateProps ? true: false>(); assert<typeof allClearKcTemplateProps extends KcTemplateProps ? true : false>();
export type KcProps = KcPropsGeneric< export type KcProps = KcPropsGeneric<
KcTemplateClassKey | | KcTemplateClassKey
"kcLogoLink" | | "kcLogoLink"
"kcLogoClass" | | "kcLogoClass"
"kcContainerClass" | | "kcContainerClass"
"kcContentClass" | | "kcContentClass"
"kcFeedbackAreaClass" | | "kcFeedbackAreaClass"
"kcLocaleClass" | | "kcLocaleClass"
"kcAlertIconClasserror" | | "kcAlertIconClasserror"
"kcFormAreaClass" | | "kcFormAreaClass"
"kcFormSocialAccountListClass" | | "kcFormSocialAccountListClass"
"kcFormSocialAccountDoubleListClass" | | "kcFormSocialAccountDoubleListClass"
"kcFormSocialAccountListLinkClass" | | "kcFormSocialAccountListLinkClass"
"kcWebAuthnKeyIcon" | | "kcWebAuthnKeyIcon"
"kcFormClass" | | "kcFormClass"
"kcFormGroupErrorClass" | | "kcFormGroupErrorClass"
"kcLabelClass" | | "kcLabelClass"
"kcInputClass" | | "kcInputClass"
"kcInputErrorMessageClass" | | "kcInputErrorMessageClass"
"kcInputWrapperClass" | | "kcInputWrapperClass"
"kcFormOptionsClass" | | "kcFormOptionsClass"
"kcFormButtonsClass" | | "kcFormButtonsClass"
"kcFormSettingClass" | | "kcFormSettingClass"
"kcTextareaClass" | | "kcTextareaClass"
"kcInfoAreaClass" | | "kcInfoAreaClass"
"kcFormGroupHeader" | | "kcFormGroupHeader"
"kcButtonClass" | | "kcButtonClass"
"kcButtonPrimaryClass" | | "kcButtonPrimaryClass"
"kcButtonDefaultClass" | | "kcButtonDefaultClass"
"kcButtonLargeClass" | | "kcButtonLargeClass"
"kcButtonBlockClass" | | "kcButtonBlockClass"
"kcInputLargeClass" | | "kcInputLargeClass"
"kcSrOnlyClass" | | "kcSrOnlyClass"
"kcSelectAuthListClass" | | "kcSelectAuthListClass"
"kcSelectAuthListItemClass" | | "kcSelectAuthListItemClass"
"kcSelectAuthListItemInfoClass" | | "kcSelectAuthListItemInfoClass"
"kcSelectAuthListItemLeftClass" | | "kcSelectAuthListItemLeftClass"
"kcSelectAuthListItemBodyClass" | | "kcSelectAuthListItemBodyClass"
"kcSelectAuthListItemDescriptionClass" | | "kcSelectAuthListItemDescriptionClass"
"kcSelectAuthListItemHeadingClass" | | "kcSelectAuthListItemHeadingClass"
"kcSelectAuthListItemHelpTextClass" | | "kcSelectAuthListItemHelpTextClass"
"kcAuthenticatorDefaultClass" | | "kcAuthenticatorDefaultClass"
"kcAuthenticatorPasswordClass" | | "kcAuthenticatorPasswordClass"
"kcAuthenticatorOTPClass" | | "kcAuthenticatorOTPClass"
"kcAuthenticatorWebAuthnClass" | | "kcAuthenticatorWebAuthnClass"
"kcAuthenticatorWebAuthnPasswordlessClass" | | "kcAuthenticatorWebAuthnPasswordlessClass"
"kcSelectOTPListClass" | | "kcSelectOTPListClass"
"kcSelectOTPListItemClass" | | "kcSelectOTPListItemClass"
"kcAuthenticatorOtpCircleClass" | | "kcAuthenticatorOtpCircleClass"
"kcSelectOTPItemHeadingClass" | | "kcSelectOTPItemHeadingClass"
"kcFormOptionsWrapperClass" | "kcFormOptionsWrapperClass"
>; >;
export const defaultKcProps = { export const defaultKcProps = {
@ -134,13 +134,31 @@ export const defaultKcProps = {
"kcLogoLink": "http://www.keycloak.org", "kcLogoLink": "http://www.keycloak.org",
"kcLogoClass": "login-pf-brand", "kcLogoClass": "login-pf-brand",
"kcContainerClass": "container-fluid", "kcContainerClass": "container-fluid",
"kcContentClass": ["col-sm-8", "col-sm-offset-2", "col-md-6", "col-md-offset-3", "col-lg-6", "col-lg-offset-3"], "kcContentClass": [
"col-sm-8",
"col-sm-offset-2",
"col-md-6",
"col-md-offset-3",
"col-lg-6",
"col-lg-offset-3",
],
"kcFeedbackAreaClass": ["col-md-12"], "kcFeedbackAreaClass": ["col-md-12"],
"kcLocaleClass": ["col-xs-12", "col-sm-1"], "kcLocaleClass": ["col-xs-12", "col-sm-1"],
"kcAlertIconClasserror": ["pficon", "pficon-error-circle-o"], "kcAlertIconClasserror": ["pficon", "pficon-error-circle-o"],
"kcFormAreaClass": ["col-sm-10", "col-sm-offset-1", "col-md-8", "col-md-offset-2", "col-lg-8", "col-lg-offset-2"], "kcFormAreaClass": [
"kcFormSocialAccountListClass": ["login-pf-social", "list-unstyled", "login-pf-social-all"], "col-sm-10",
"col-sm-offset-1",
"col-md-8",
"col-md-offset-2",
"col-lg-8",
"col-lg-offset-2",
],
"kcFormSocialAccountListClass": [
"login-pf-social",
"list-unstyled",
"login-pf-social-all",
],
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"], "kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"], "kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"], "kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
@ -149,14 +167,25 @@ export const defaultKcProps = {
"kcFormGroupErrorClass": ["has-error"], "kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"], "kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"], "kcInputClass": ["form-control"],
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"], "kcInputErrorMessageClass": [
"pf-c-form__helper-text",
"pf-m-error",
"required",
"kc-feedback-text",
],
"kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"], "kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"], "kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"], "kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormSettingClass": ["login-pf-settings"], "kcFormSettingClass": ["login-pf-settings"],
"kcTextareaClass": ["form-control"], "kcTextareaClass": ["form-control"],
"kcInfoAreaClass": ["col-xs-12", "col-sm-4", "col-md-4", "col-lg-5", "details"], "kcInfoAreaClass": [
"col-xs-12",
"col-sm-4",
"col-md-4",
"col-lg-5",
"details",
],
// user-profile grouping // user-profile grouping
"kcFormGroupHeader": ["pf-c-form__group"], "kcFormGroupHeader": ["pf-c-form__group"],
@ -191,21 +220,28 @@ export const defaultKcProps = {
"kcAuthenticatorPasswordClass": ["fa", "fa-unlock list-view-pf-icon-lg"], "kcAuthenticatorPasswordClass": ["fa", "fa-unlock list-view-pf-icon-lg"],
"kcAuthenticatorOTPClass": ["fa", "fa-mobile", "list-view-pf-icon-lg"], "kcAuthenticatorOTPClass": ["fa", "fa-mobile", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnClass": ["fa", "fa-key", "list-view-pf-icon-lg"], "kcAuthenticatorWebAuthnClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnPasswordlessClass": ["fa", "fa-key", "list-view-pf-icon-lg"], "kcAuthenticatorWebAuthnPasswordlessClass": [
"fa",
"fa-key",
"list-view-pf-icon-lg",
],
//css classes for the OTP Login Form //css classes for the OTP Login Form
"kcSelectOTPListClass": ["card-pf", "card-pf-view", "card-pf-view-select", "card-pf-view-single-select"], "kcSelectOTPListClass": [
"card-pf",
"card-pf-view",
"card-pf-view-select",
"card-pf-view-single-select",
],
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"], "kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"], "kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"], "kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
"kcFormOptionsWrapperClass": [] "kcFormOptionsWrapperClass": [],
} as const; } as const;
assert<typeof defaultKcProps extends KcProps ? true : false>(); assert<typeof defaultKcProps extends KcProps ? true : false>();
/** Tu use if you don't want any default */ /** Tu use if you don't want any default */
export const allClearKcProps = export const allClearKcProps = allPropertiesValuesToUndefined(defaultKcProps);
allPropertiesValuesToUndefined(defaultKcProps);
assert<typeof allClearKcProps extends KcProps ? true : false>(); assert<typeof allClearKcProps extends KcProps ? true : false>();

View File

@ -1,4 +1,3 @@
import { useState, memo } from "react"; import { useState, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -7,151 +6,241 @@ import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login; } & KcProps) => { export const Login = memo(
({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
const {
social,
realm,
url,
usernameEditDisabled,
login,
auth,
registrationDisabled,
} = kcContext;
const { const { msg, msgStr } = useKcMessage();
social, realm, url,
usernameEditDisabled, login,
auth, registrationDisabled
} = kcContext;
const { msg, msgStr } = useKcMessage(); const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); const [isLoginButtonDisabled, setIsLoginButtonDisabled] =
useState(false);
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const onSubmit = useConstCallback(
() => (setIsLoginButtonDisabled(true), true),
);
const onSubmit = useConstCallback(() => return (
(setIsLoginButtonDisabled(true), true) <Template
); {...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
return ( displayInfo={social.displayInfo}
<Template displayWide={realm.password && social.providers !== undefined}
{...{ kcContext, ...props }} headerNode={msg("doLogIn")}
doFetchDefaultThemeResources={true} formNode={
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div
id="kc-form"
className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}
>
<div <div
id="kc-form-wrapper" id="kc-form"
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])} className={cx(
>
{
realm.password && realm.password &&
( social.providers !== undefined &&
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post"> props.kcContentWrapperClass,
)}
>
<div
id="kc-form-wrapper"
className={cx(
realm.password &&
social.providers && [
props.kcFormSocialAccountContentClass,
props.kcFormSocialAccountClass,
],
)}
>
{realm.password && (
<form
id="kc-form-login"
onSubmit={onSubmit}
action={url.loginAction}
method="post"
>
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcFormGroupClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}> <label
{ htmlFor="username"
!realm.loginWithEmailAllowed ? className={cx(props.kcLabelClass)}
msg("username") >
: {!realm.loginWithEmailAllowed
( ? msg("username")
!realm.registrationEmailAsUsername ? : !realm.registrationEmailAsUsername
msg("usernameOrEmail") : ? msg("usernameOrEmail")
msg("email") : msg("email")}
)
}
</label> </label>
<input <input
tabIndex={1} tabIndex={1}
id="username" id="username"
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
name="username" name="username"
defaultValue={login.username ?? ''} defaultValue={login.username ?? ""}
type="text" type="text"
{...(usernameEditDisabled ? { "disabled": true } : { "autoFocus": true, "autoComplete": "off" })} {...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off",
})}
/> />
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}> <label
htmlFor="password"
className={cx(props.kcLabelClass)}
>
{msg("password")} {msg("password")}
</label> </label>
<input tabIndex={2} id="password" className={cx(props.kcInputClass)} name="password" type="password" autoComplete="off" /> <input
tabIndex={2}
id="password"
className={cx(props.kcInputClass)}
name="password"
type="password"
autoComplete="off"
/>
</div> </div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}> <div
className={cx(
props.kcFormGroupClass,
props.kcFormSettingClass,
)}
>
<div id="kc-form-options"> <div id="kc-form-options">
{ {realm.rememberMe &&
( !usernameEditDisabled && (
realm.rememberMe && <div className="checkbox">
!usernameEditDisabled <label>
) && <input
<div className="checkbox"> tabIndex={3}
<label> id="rememberMe"
<input tabIndex={3} id="rememberMe" name="rememberMe" type="checkbox" {...(login.rememberMe ? { "checked": true } : {})} /> name="rememberMe"
{msg("rememberMe")} type="checkbox"
</label> {...(login.rememberMe
</div> ? {
} "checked":
true,
}
: {})}
/>
{msg("rememberMe")}
</label>
</div>
)}
</div> </div>
<div className={cx(props.kcFormOptionsWrapperClass)}> <div
{ className={cx(
realm.resetPasswordAllowed && props.kcFormOptionsWrapperClass,
)}
>
{realm.resetPasswordAllowed && (
<span> <span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{msg("doForgotPassword")}</a> <a
tabIndex={5}
href={
url.loginResetCredentialsUrl
}
>
{msg(
"doForgotPassword",
)}
</a>
</span> </span>
} )}
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}> <div
id="kc-form-buttons"
className={cx(props.kcFormGroupClass)}
>
<input <input
type="hidden" type="hidden"
id="id-hidden-input" id="id-hidden-input"
name="credentialId" name="credentialId"
{...(auth?.selectedCredential !== undefined ? { "value": auth.selectedCredential } : {})} {...(auth?.selectedCredential !==
undefined
? {
"value":
auth.selectedCredential,
}
: {})}
/> />
<input <input
tabIndex={4} tabIndex={4}
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} name="login" id="kc-login" type="submit" className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")} value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled} disabled={isLoginButtonDisabled}
/> />
</div> </div>
</form> </form>
) )}
} </div>
</div> {realm.password && social.providers !== undefined && (
{ <div
(realm.password && social.providers !== undefined) && id="kc-social-providers"
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}> className={cx(
<ul className={cx(props.kcFormSocialAccountListClass, social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass)}> props.kcFormSocialAccountContentClass,
{ props.kcFormSocialAccountClass,
social.providers.map(p => )}
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}> >
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}> <ul
className={cx(
props.kcFormSocialAccountListClass,
social.providers.length > 4 &&
props.kcFormSocialAccountDoubleListClass,
)}
>
{social.providers.map(p => (
<li
key={p.providerId}
className={cx(
props.kcFormSocialAccountListLinkClass,
)}
>
<a
href={p.loginUrl}
id={`zocial-${p.alias}`}
className={cx(
"zocial",
p.providerId,
)}
>
<span>{p.displayName}</span> <span>{p.displayName}</span>
</a> </a>
</li> </li>
) ))}
} </ul>
</ul> </div>
</div> )}
} </div>
</div> }
} infoNode={
infoNode={
(
realm.password && realm.password &&
realm.registrationAllowed && realm.registrationAllowed &&
!registrationDisabled !registrationDisabled && (
) && <div id="kc-registration">
<div id="kc-registration"> <span>
<span> {msg("noAccount")}
{msg("noAccount")} <a tabIndex={6} href={url.registrationUrl}>
<a tabIndex={6} href={url.registrationUrl}> {msg("doRegister")}
{msg("doRegister")} </a>
</a> </span>
</span> </div>
</div> )
} }
/> />
); );
}); },
);

View File

@ -1,4 +1,3 @@
import { memo } from "react"; import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -6,56 +5,61 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; } & KcProps) => { export const LoginIdpLinkConfirm = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => {
const { url, idpAlias } = kcContext;
const { url, idpAlias } = kcContext; const { msg } = useKcMessage();
const { msg } = useKcMessage();
const { cx } = useCssAndCx();
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("confirmLinkIdpTitle")}
formNode={
<form id="kc-register-form" action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="submitAction"
id="updateProfile"
value="updateProfile"
>
{msg("confirmLinkIdpReviewProfile")}
</button>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="submitAction"
id="linkAccount"
value="linkAccount"
>
{msg("confirmLinkIdpContinue", idpAlias)}
</button>
</div>
</form>
}
/>
);
});
const { cx } = useCssAndCx();
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("confirmLinkIdpTitle")}
formNode={
<form
id="kc-register-form"
action={url.loginAction}
method="post"
>
<div className={cx(props.kcFormGroupClass)}>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
name="submitAction"
id="updateProfile"
value="updateProfile"
>
{msg("confirmLinkIdpReviewProfile")}
</button>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
name="submitAction"
id="linkAccount"
value="linkAccount"
>
{msg("confirmLinkIdpContinue", idpAlias)}
</button>
</div>
</form>
}
/>
);
},
);

View File

@ -1,5 +1,3 @@
import { useEffect, memo } from "react"; import { useEffect, memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -9,140 +7,169 @@ import { appendHead } from "../tools/appendHead";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp; } & KcProps) => { export const LoginOtp = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.LoginOtp } & KcProps) => {
const { otpLogin, url } = kcContext;
const { otpLogin, url } = kcContext; const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); const { msg, msgStr } = useKcMessage();
const { msg, msgStr } = useKcMessage();
useEffect(
() => {
useEffect(() => {
let isCleanedUp = false; let isCleanedUp = false;
appendHead({ appendHead({
"type": "javascript", "type": "javascript",
"src": pathJoin( "src": pathJoin(
kcContext.url.resourcesCommonPath, kcContext.url.resourcesCommonPath,
"node_modules/jquery/dist/jquery.min.js" "node_modules/jquery/dist/jquery.min.js",
) ),
}).then(() => { }).then(() => {
if (isCleanedUp) return; if (isCleanedUp) return;
evaluateInlineScript(); evaluateInlineScript();
}); });
return () => { isCleanedUp = true }; return () => {
isCleanedUp = true;
};
}, []);
}, return (
[] <Template
); {...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
return ( headerNode={msg("doLogIn")}
<Template formNode={
{...{ kcContext, ...props }} <form
doFetchDefaultThemeResources={true} id="kc-otp-login-form"
headerNode={msg("doLogIn")} className={cx(props.kcFormClass)}
formNode={ action={url.loginAction}
method="post"
<form >
id="kc-otp-login-form" {otpLogin.userOtpCredentials.length > 1 && (
className={cx(props.kcFormClass)} <div className={cx(props.kcFormGroupClass)}>
action={url.loginAction} <div className={cx(props.kcInputWrapperClass)}>
method="post" {otpLogin.userOtpCredentials.map(
> otpCredential => (
{ <div
otpLogin.userOtpCredentials.length > 1 && key={otpCredential.id}
<div className={cx(props.kcFormGroupClass)}> className={cx(
<div className={cx(props.kcInputWrapperClass)}> props.kcSelectOTPListClass,
{ )}
otpLogin.userOtpCredentials.map(otpCredential => >
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}> <input
<input type="hidden" value="${otpCredential.id}" /> type="hidden"
<div className={cx(props.kcSelectOTPListItemClass)}> value="${otpCredential.id}"
<span className={cx(props.kcAuthenticatorOtpCircleClass)} /> />
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}> <div
{otpCredential.userLabel} className={cx(
</h2> props.kcSelectOTPListItemClass,
)}
>
<span
className={cx(
props.kcAuthenticatorOtpCircleClass,
)}
/>
<h2
className={cx(
props.kcSelectOTPItemHeadingClass,
)}
>
{
otpCredential.userLabel
}
</h2>
</div>
</div> </div>
</div> ),
) )}
} </div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label
htmlFor="otp"
className={cx(props.kcLabelClass)}
>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
id="otp"
name="otp"
autoComplete="off"
type="text"
className={cx(props.kcInputClass)}
autoFocus
/>
</div> </div>
</div> </div>
}
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}> <div
<label htmlFor="otp" className={cx(props.kcLabelClass)}> id="kc-form-options"
{msg("loginOtpOneTime")} className={cx(props.kcFormOptionsClass)}
</label> >
<div
className={cx(
props.kcFormOptionsWrapperClass,
)}
/>
</div>
<div
id="kc-form-buttons"
className={cx(props.kcFormButtonsClass)}
>
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div> </div>
</form>
<div className={cx(props.kcInputWrapperClass)}> }
<input />
id="otp" );
name="otp" },
autoComplete="off" );
type="text"
className={cx(props.kcInputClass)}
autoFocus
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
</form >
}
/>
);
});
declare const $: any; declare const $: any;
function evaluateInlineScript() { function evaluateInlineScript() {
$(document).ready(function () { $(document).ready(function () {
// Card Single Select // Card Single Select
$('.card-pf-view-single-select').click(function (this: any) { $(".card-pf-view-single-select").click(function (this: any) {
if ($(this).hasClass('active')) { $(this).removeClass('active'); $(this).children().removeAttr('name'); } if ($(this).hasClass("active")) {
else { $(this).removeClass("active");
$('.card-pf-view-single-select').removeClass('active'); $(this).children().removeAttr("name");
$('.card-pf-view-single-select').children().removeAttr('name'); } else {
$(this).addClass('active'); $(this).children().attr('name', 'selectedCredentialId'); $(".card-pf-view-single-select").removeClass("active");
$(".card-pf-view-single-select").children().removeAttr("name");
$(this).addClass("active");
$(this).children().attr("name", "selectedCredentialId");
} }
}); });
var defaultCred = $('.card-pf-view-single-select')[0]; var defaultCred = $(".card-pf-view-single-select")[0];
if (defaultCred) { if (defaultCred) {
defaultCred.click(); defaultCred.click();
} }
}); });
}
}

View File

@ -1,4 +1,3 @@
import { memo } from "react"; import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -6,78 +5,101 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword; } & KcProps) => { export const LoginResetPassword = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => {
const { url, realm, auth } = kcContext;
const { const { msg, msgStr } = useKcMessage();
url,
realm,
auth
} = kcContext;
const { msg, msgStr } = useKcMessage(); const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); return (
<Template
return ( {...{ kcContext, ...props }}
<Template doFetchDefaultThemeResources={true}
{...{ kcContext, ...props }} displayMessage={false}
doFetchDefaultThemeResources={true} headerNode={msg("emailForgotTitle")}
displayMessage={false} formNode={
headerNode={msg("emailForgotTitle")} <form
formNode={ id="kc-reset-password-form"
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> className={cx(props.kcFormClass)}
<div className={cx(props.kcFormGroupClass)}> action={url.loginAction}
<div className={cx(props.kcLabelWrapperClass)}> method="post"
<label htmlFor="username" className={cx(props.kcLabelClass)}> >
{ <div className={cx(props.kcFormGroupClass)}>
!realm.loginWithEmailAllowed ? <div className={cx(props.kcLabelWrapperClass)}>
msg("username") <label
: htmlFor="username"
!realm.registrationEmailAsUsername ? className={cx(props.kcLabelClass)}
msg("usernameOrEmail") : >
msg("email") {!realm.loginWithEmailAllowed
} ? msg("username")
</label> : !realm.registrationEmailAsUsername
</div> ? msg("usernameOrEmail")
<div className={cx(props.kcInputWrapperClass)}> : msg("email")}
<input </label>
type="text" </div>
id="username" <div className={cx(props.kcInputWrapperClass)}>
name="username" <input
className={cx(props.kcInputClass)} type="text"
autoFocus id="username"
defaultValue={ name="username"
auth !== undefined && auth.showUsername ? className={cx(props.kcInputClass)}
auth.attemptedUsername : undefined autoFocus
} defaultValue={
/> auth !== undefined && auth.showUsername
</div> ? auth.attemptedUsername
</div> : undefined
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}> }
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> />
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div> </div>
</div> </div>
<div
className={cx(
props.kcFormGroupClass,
props.kcFormSettingClass,
)}
>
<div
id="kc-form-options"
className={cx(props.kcFormOptionsClass)}
>
<div
className={cx(
props.kcFormOptionsWrapperClass,
)}
>
<span>
<a href={url.loginUrl}>
{msg("backToLogin")}
</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div
<input id="kc-form-buttons"
className={cx( className={cx(props.kcFormButtonsClass)}
props.kcButtonClass, props.kcButtonPrimaryClass, >
props.kcButtonBlockClass, props.kcButtonLargeClass <input
)} className={cx(
type="submit" props.kcButtonClass,
value={msgStr("doSubmit")} props.kcButtonPrimaryClass,
/> props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
type="submit"
value={msgStr("doSubmit")}
/>
</div>
</div> </div>
</div> </form>
</form> }
} infoNode={msg("emailInstruction")}
infoNode={msg("emailInstruction")} />
/> );
); },
}); );

View File

@ -5,129 +5,200 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; } & KcProps) => { export const LoginUpdateProfile = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => {
const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); const { msg, msgStr } = useKcMessage();
const { msg, msgStr } = useKcMessage(); const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext; return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
headerNode={msg("loginProfileTitle")}
formNode={
<form
id="kc-update-profile-form"
className={cx(props.kcFormClass)}
action={url.loginAction}
method="post"
>
{user.editUsernameAllowed && (
<div
className={cx(
props.kcFormGroupClass,
messagesPerField.printIfExists(
"username",
props.kcFormGroupErrorClass,
),
)}
>
<div className={cx(props.kcLabelWrapperClass)}>
<label
htmlFor="username"
className={cx(props.kcLabelClass)}
>
{msg("username")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="username"
name="username"
defaultValue={user.username ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
)}
return ( <div
<Template className={cx(
{...{ kcContext, ...props }} props.kcFormGroupClass,
doFetchDefaultThemeResources={true} messagesPerField.printIfExists(
headerNode={msg("loginProfileTitle")} "email",
formNode={ props.kcFormGroupErrorClass,
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> ),
{user.editUsernameAllowed && )}
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}> >
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}> <label
{msg("username")} htmlFor="email"
</label> className={cx(props.kcLabelClass)}
</div> >
<div className={cx(props.kcInputWrapperClass)}> {msg("email")}
<input </label>
type="text" </div>
id="username" <div className={cx(props.kcInputWrapperClass)}>
name="username" <input
defaultValue={user.username ?? ""} type="text"
className={cx(props.kcInputClass)} id="email"
/> name="email"
</div> defaultValue={user.email ?? ""}
</div> className={cx(props.kcInputClass)}
} />
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}> <div
<div className={cx(props.kcLabelWrapperClass)}> className={cx(
<label htmlFor="email" className={cx(props.kcLabelClass)}> props.kcFormGroupClass,
{msg("email")} messagesPerField.printIfExists(
</label> "firstName",
</div> props.kcFormGroupErrorClass,
<div className={cx(props.kcInputWrapperClass)}> ),
<input )}
type="text" >
id="email" <div className={cx(props.kcLabelWrapperClass)}>
name="email" <label
defaultValue={user.email ?? ""} htmlFor="firstName"
className={cx(props.kcInputClass)} className={cx(props.kcLabelClass)}
/> >
</div> {msg("firstName")}
</div> </label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="firstName"
name="firstName"
defaultValue={user.firstName ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}> <div
<div className={cx(props.kcLabelWrapperClass)}> className={cx(
<label htmlFor="firstName" className={cx(props.kcLabelClass)}> props.kcFormGroupClass,
{msg("firstName")} messagesPerField.printIfExists(
</label> "lastName",
</div> props.kcFormGroupErrorClass,
<div className={cx(props.kcInputWrapperClass)}> ),
<input )}
type="text" >
id="firstName" <div className={cx(props.kcLabelWrapperClass)}>
name="firstName" <label
defaultValue={user.firstName ?? ""} htmlFor="lastName"
className={cx(props.kcInputClass)} className={cx(props.kcLabelClass)}
/> >
</div> {msg("lastName")}
</div> </label>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}> <div className={cx(props.kcInputWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass)}> <input
<label htmlFor="lastName" className={cx(props.kcLabelClass)}> type="text"
{msg("lastName")} id="lastName"
</label> name="lastName"
</div> defaultValue={user.lastName ?? ""}
<div className={cx(props.kcInputWrapperClass)}> className={cx(props.kcInputClass)}
<input />
type="text" </div>
id="lastName" </div>
name="lastName"
defaultValue={user.lastName ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
{
isAppInitiatedAction ?
<>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
<button
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
:
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
}
</div>
</div>
</form>
}
/>
);
});
<div className={cx(props.kcFormGroupClass)}>
<div
id="kc-form-options"
className={cx(props.kcFormOptionsClass)}
>
<div
className={cx(
props.kcFormOptionsWrapperClass,
)}
/>
</div>
<div
id="kc-form-buttons"
className={cx(props.kcFormButtonsClass)}
>
{isAppInitiatedAction ? (
<>
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass,
)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
<button
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass,
)}
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
)}
</div>
</div>
</form>
}
/>
);
},
);

View File

@ -1,39 +1,37 @@
import { memo } from "react"; import { memo } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; } & KcProps) => { export const LoginVerifyEmail = memo(
({
const { msg } = useKcMessage(); kcContext,
...props
const { }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => {
url const { msg } = useKcMessage();
} = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
<>
<p className="instruction">
{msg("emailVerifyInstruction1")}
</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<a href={url.loginAction}>{msg("doClickHere")}</a>
{msg("emailVerifyInstruction3")}
</p>
</>
}
/>
);
});
const { url } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
<>
<p className="instruction">
{msg("emailVerifyInstruction1")}
</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<a href={url.loginAction}>{msg("doClickHere")}</a>
{msg("emailVerifyInstruction3")}
</p>
</>
}
/>
);
},
);

View File

@ -5,122 +5,279 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register; } & KcProps) => { export const Register = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.Register } & KcProps) => {
const {
url,
messagesPerField,
register,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey,
} = kcContext;
const { const { msg, msgStr } = useKcMessage();
url,
messagesPerField,
register,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey
} = kcContext;
const { msg, msgStr } = useKcMessage(); const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); return (
<Template
return ( {...{ kcContext, ...props }}
<Template doFetchDefaultThemeResources={true}
{...{ kcContext, ...props }} headerNode={msg("registerTitle")}
doFetchDefaultThemeResources={true} formNode={
headerNode={msg("registerTitle")} <form
formNode={ id="kc-register-form"
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post"> className={cx(props.kcFormClass)}
action={url.registrationAction}
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}> method="post"
<div className={cx(props.kcLabelWrapperClass)}> >
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{msg("firstName")}</label> <div
</div> className={cx(
<div className={cx(props.kcInputWrapperClass)}> props.kcFormGroupClass,
<input type="text" id="firstName" className={cx(props.kcInputClass)} name="firstName" messagesPerField.printIfExists(
defaultValue={register.formData.firstName ?? ""} "firstName",
/> props.kcFormGroupErrorClass,
</div> ),
</div> )}
>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>{msg("lastName")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="lastName" className={cx(props.kcInputClass)} name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('email', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}>{msg("email")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="email" className={cx(props.kcInputClass)} name="email"
defaultValue={register.formData.email ?? ""} autoComplete="email"
/>
</div>
</div>
{
!realm.registrationEmailAsUsername &&
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('username', props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>{msg("username")}</label> <label
htmlFor="firstName"
className={cx(props.kcLabelClass)}
>
{msg("firstName")}
</label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="username" className={cx(props.kcInputClass)} name="username" <input
defaultValue={register.formData.username ?? ""} autoComplete="username" /> type="text"
id="firstName"
className={cx(props.kcInputClass)}
name="firstName"
defaultValue={
register.formData.firstName ?? ""
}
/>
</div> </div>
</div > </div>
} <div
{ className={cx(
passwordRequired && props.kcFormGroupClass,
<> messagesPerField.printIfExists(
"lastName",
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}> props.kcFormGroupErrorClass,
<div className={cx(props.kcLabelWrapperClass)}> ),
<label htmlFor="password" className={cx(props.kcLabelClass)}>{msg("password")}</label> )}
</div> >
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<input type="password" id="password" className={cx(props.kcInputClass)} name="password" autoComplete="new-password" /> <label
</div> htmlFor="lastName"
className={cx(props.kcLabelClass)}
>
{msg("lastName")}
</label>
</div> </div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>{msg("passwordConfirm")}</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
</div>
</div>
</>
}
{
recaptchaRequired &&
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div> <input
</div> type="text"
</div> id="lastName"
} className={cx(props.kcInputClass)}
<div className={cx(props.kcFormGroupClass)}> name="lastName"
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> defaultValue={
<div className={cx(props.kcFormOptionsWrapperClass)}> register.formData.lastName ?? ""
<span><a href={url.loginUrl}>{msg("backToLogin")}</a></span> }
/>
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div
<input className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} type="submit" className={cx(
value={msgStr("doRegister")} /> props.kcFormGroupClass,
messagesPerField.printIfExists(
"email",
props.kcFormGroupErrorClass,
),
)}
>
<div className={cx(props.kcLabelWrapperClass)}>
<label
htmlFor="email"
className={cx(props.kcLabelClass)}
>
{msg("email")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="email"
className={cx(props.kcInputClass)}
name="email"
defaultValue={register.formData.email ?? ""}
autoComplete="email"
/>
</div>
</div> </div>
</div> {!realm.registrationEmailAsUsername && (
</form > <div
} className={cx(
/> props.kcFormGroupClass,
); messagesPerField.printIfExists(
}); "username",
props.kcFormGroupErrorClass,
),
)}
>
<div className={cx(props.kcLabelWrapperClass)}>
<label
htmlFor="username"
className={cx(props.kcLabelClass)}
>
{msg("username")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="username"
className={cx(props.kcInputClass)}
name="username"
defaultValue={
register.formData.username ?? ""
}
autoComplete="username"
/>
</div>
</div>
)}
{passwordRequired && (
<>
<div
className={cx(
props.kcFormGroupClass,
messagesPerField.printIfExists(
"password",
props.kcFormGroupErrorClass,
),
)}
>
<div
className={cx(
props.kcLabelWrapperClass,
)}
>
<label
htmlFor="password"
className={cx(props.kcLabelClass)}
>
{msg("password")}
</label>
</div>
<div
className={cx(
props.kcInputWrapperClass,
)}
>
<input
type="password"
id="password"
className={cx(props.kcInputClass)}
name="password"
autoComplete="new-password"
/>
</div>
</div>
<div
className={cx(
props.kcFormGroupClass,
messagesPerField.printIfExists(
"password-confirm",
props.kcFormGroupErrorClass,
),
)}
>
<div
className={cx(
props.kcLabelWrapperClass,
)}
>
<label
htmlFor="password-confirm"
className={cx(props.kcLabelClass)}
>
{msg("passwordConfirm")}
</label>
</div>
<div
className={cx(
props.kcInputWrapperClass,
)}
>
<input
type="password"
id="password-confirm"
className={cx(props.kcInputClass)}
name="password-confirm"
/>
</div>
</div>
</>
)}
{recaptchaRequired && (
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}>
<div
className="g-recaptcha"
data-size="compact"
data-sitekey={recaptchaSiteKey}
></div>
</div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div
id="kc-form-options"
className={cx(props.kcFormOptionsClass)}
>
<div
className={cx(
props.kcFormOptionsWrapperClass,
)}
>
<span>
<a href={url.loginUrl}>
{msg("backToLogin")}
</a>
</span>
</div>
</div>
<div
id="kc-form-buttons"
className={cx(props.kcFormButtonsClass)}
>
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
)}
type="submit"
value={msgStr("doRegister")}
/>
</div>
</div>
</form>
}
/>
);
},
);

View File

@ -1,4 +1,3 @@
import { memo, Fragment } from "react"; import { memo, Fragment } from "react";
import { Template } from "./Template"; import { Template } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
@ -7,220 +6,349 @@ import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
import type { ReactComponent } from "../tools/ReactComponent"; import type { ReactComponent } from "../tools/ReactComponent";
export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.RegisterUserProfile; } & KcProps) => { export const RegisterUserProfile = memo(
({
kcContext,
...props
}: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
const {
url,
messagesPerField,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey,
} = kcContext;
const { const { msg, msgStr } = useKcMessage();
url,
messagesPerField,
realm,
passwordRequired,
recaptchaRequired,
recaptchaSiteKey
} = kcContext;
const { msg, msgStr } = useKcMessage(); const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); return (
<Template
{...{ kcContext, ...props }}
displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true}
doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")}
formNode={
<form
id="kc-register-form"
className={cx(props.kcFormClass)}
action={url.registrationAction}
method="post"
>
<UserProfileFormFields
kcContext={kcContext}
{...props}
AfterField={({ attribute }) =>
/*render password fields just under the username or email (if used as username)*/
(passwordRequired &&
(attribute.name == "username" ||
(attribute.name == "email" &&
realm.registrationEmailAsUsername)) && (
<>
<div
className={cx(
props.kcFormGroupClass,
)}
>
<div
className={cx(
props.kcLabelWrapperClass,
)}
>
<label
htmlFor="password"
className={cx(
props.kcLabelClass,
)}
>
{msg("password")}
</label>{" "}
*
</div>
<div
className={cx(
props.kcInputWrapperClass,
)}
>
<input
type="password"
id="password"
className={cx(
props.kcInputClass,
)}
name="password"
autoComplete="new-password"
aria-invalid={
messagesPerField.existsError(
"password",
) ||
messagesPerField.existsError(
"password-confirm",
)
}
/>
{messagesPerField.existsError(
"password",
) && (
<span
id="input-error-password"
className={cx(
props.kcInputErrorMessageClass,
)}
aria-live="polite"
>
{messagesPerField.get(
"password",
)}
</span>
)}
</div>
</div>
<div
className={cx(
props.kcFormGroupClass,
)}
>
<div
className={cx(
props.kcLabelWrapperClass,
)}
>
<label
htmlFor="password-confirm"
className={cx(
props.kcLabelClass,
)}
>
{msg("passwordConfirm")}
</label>{" "}
*
</div>
<div
className={cx(
props.kcInputWrapperClass,
)}
>
<input
type="password"
id="password-confirm"
className={cx(
props.kcInputClass,
)}
name="password-confirm"
aria-invalid={messagesPerField.existsError(
"password-confirm",
)}
/>
{messagesPerField.existsError(
"password-confirm",
) && (
<span
id="input-error-password-confirm"
className={cx(
props.kcInputErrorMessageClass,
)}
aria-live="polite"
>
{messagesPerField.get(
"password-confirm",
)}
</span>
)}
</div>
</div>
</>
)) ||
null
}
/>
{recaptchaRequired && (
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}>
<div
className="g-recaptcha"
data-size="compact"
data-sitekey={recaptchaSiteKey}
/>
</div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div
id="kc-form-options"
className={cx(props.kcFormOptionsClass)}
>
<div
className={cx(
props.kcFormOptionsWrapperClass,
)}
>
<span>
<a href={url.loginUrl}>
{msg("backToLogin")}
</a>
</span>
</div>
</div>
return ( <div
<Template id="kc-form-buttons"
{...{ kcContext, ...props }} className={cx(props.kcFormButtonsClass)}
displayMessage={messagesPerField.exists("global")} >
displayRequiredFields={true} <input
doFetchDefaultThemeResources={true} className={cx(
headerNode={msg("registerTitle")} props.kcButtonClass,
formNode={ props.kcButtonPrimaryClass,
<form props.kcButtonBlockClass,
id="kc-register-form" props.kcButtonLargeClass,
className={cx(props.kcFormClass)} )}
action={url.registrationAction} type="submit"
method="post" value={msgStr("doRegister")}
> />
<UserProfileFormFields </div>
kcContext={kcContext} </div>
{...props} </form>
AfterField={({ attribute }) => }
/*render password fields just under the username or email (if used as username)*/ />
passwordRequired && (attribute.name == "username" || (attribute.name == "email" && realm.registrationEmailAsUsername)) && );
<> },
<div className={cx(props.kcFormGroupClass)}> );
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>
{msg("password")}
</label> *
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password" className={cx(props.kcInputClass)} name="password"
autoComplete="new-password"
aria-invalid={
messagesPerField.existsError("password") ||
messagesPerField.existsError("password-confirm")
}
/>
{
messagesPerField.existsError("password") &&
<span id="input-error-password" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get('password')}
</span>
}
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm"
className={cx(props.kcLabelClass)}>
{msg("passwordConfirm")}
</label> *
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)}
name="password-confirm"
aria-invalid={messagesPerField.existsError("password-confirm")}
/>
{
messagesPerField.existsError("password-confirm") &&
<span id="input-error-password-confirm" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get('password-confirm')}
</span>
}
</div>
</div>
</> || null
}
/>
{
recaptchaRequired &&
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div>
</div>
}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span><a href={url.loginUrl}>{msg("backToLogin")}</a></span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
type="submit" value={msgStr("doRegister")}
/>
</div>
</div>
</form >
}
/>
);
});
const UserProfileFormFields = memo( const UserProfileFormFields = memo(
( ({
{ kcContext,
kcContext, BeforeField = () => null,
BeforeField = () => null, AfterField = () => null,
AfterField = () => null, ...props
...props }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
}: Partial<
{ kcContext: KcContextBase.RegisterUserProfile; } & Record<
KcProps & "BeforeField" | "AfterField",
Partial<Record< ReactComponent<{
"BeforeField" | "AfterField", attribute: KcContextBase.RegisterUserProfile["profile"]["attributes"][number];
ReactComponent<{ attribute: KcContextBase.RegisterUserProfile["profile"]["attributes"][number]; }> }>
>> >
) => { >) => {
const { messagesPerField } = kcContext;
const { messagesPerField } = kcContext; const { cx } = useCssAndCx();
const { cx } = useCssAndCx(); const { advancedMsg } = useKcMessage();
const { advancedMsg } = useKcMessage(); let currentGroup = "";
let currentGroup = ""; return (
<>
{kcContext.profile.attributes.map((attribute, i) => {
const {
group = "",
groupDisplayHeader = "",
groupDisplayDescription = "",
} = attribute;
return ( if (group === currentGroup) return null;
<>
{kcContext.profile.attributes.map((attribute, i) => {
const { currentGroup = group;
group = "",
groupDisplayHeader = "",
groupDisplayDescription = ""
} = attribute;
if (group === currentGroup) return null; return (
<Fragment key={i}>
currentGroup = group; {group !== "" && (
<div className={cx(props.kcFormGroupClass)}>
return ( <div
<Fragment key={i}> className={cx(
{group !== "" && props.kcContentWrapperClass,
<div className={cx(props.kcFormGroupClass)}> )}
<div className={cx(props.kcContentWrapperClass)}> >
<label <label
id={`header-${group}`} id={`header-${group}`}
className={cx(props.kcFormGroupHeader)} className={cx(
> props.kcFormGroupHeader,
{groupDisplayHeader !== "" && advancedMsg(groupDisplayHeader) || currentGroup} )}
</label> >
</div> {(groupDisplayHeader !== "" &&
{groupDisplayDescription !== "" && advancedMsg(
<div className={cx(props.kcLabelWrapperClass)}> groupDisplayHeader,
<label )) ||
id={`description-${group}`} currentGroup}
className={`${cx(props.kcLabelClass)}`} </label>
> </div>
{advancedMsg(groupDisplayDescription) ?? ""} {groupDisplayDescription !== "" && (
</label> <div
</div> className={cx(
} props.kcLabelWrapperClass,
</div>} )}
<BeforeField attribute={attribute} /> >
<div className={cx(props.kcFormGroupClass)}> <label
<div className={cx(props.kcLabelWrapperClass)}> id={`description-${group}`}
<label className={`${cx(
htmlFor={attribute.name} props.kcLabelClass,
className={cx(props.kcLabelClass)} )}`}
> >
{advancedMsg(attribute.displayName ?? "")} {advancedMsg(
</label> groupDisplayDescription,
{attribute.required && <>*</>} ) ?? ""}
</div> </label>
<div className={cx(props.kcInputWrapperClass)}> </div>
<input )}
type="text" </div>
id={attribute.name} )}
name={attribute.name} <BeforeField attribute={attribute} />
value={attribute.value ?? ""} <div className={cx(props.kcFormGroupClass)}>
className={cx(props.kcInputClass)} <div className={cx(props.kcLabelWrapperClass)}>
aria-invalid={messagesPerField.existsError(attribute.name)} <label
disabled={attribute.readOnly} htmlFor={attribute.name}
{...(attribute.autocomplete === undefined ? {} : { className={cx(props.kcLabelClass)}
"autoComplete": attribute.autocomplete >
})} {advancedMsg(
/> attribute.displayName ?? "",
{ )}
kcContext.messagesPerField.existsError(attribute.name) && </label>
<span {attribute.required && <>*</>}
id={`input-error-${attribute.name}`} </div>
className={cx(props.kcInputErrorMessageClass)} <div className={cx(props.kcInputWrapperClass)}>
aria-live="polite" <input
> type="text"
{messagesPerField.get(attribute.name)} id={attribute.name}
</span> name={attribute.name}
} value={attribute.value ?? ""}
</div > className={cx(props.kcInputClass)}
</div > aria-invalid={messagesPerField.existsError(
<AfterField attribute={attribute} /> attribute.name,
</Fragment> )}
); disabled={attribute.readOnly}
})} {...(attribute.autocomplete ===
</> undefined
); ? {}
: {
} "autoComplete":
); attribute.autocomplete,
})}
/>
{kcContext.messagesPerField.existsError(
attribute.name,
) && (
<span
id={`input-error-${attribute.name}`}
className={cx(
props.kcInputErrorMessageClass,
)}
aria-live="polite"
>
{messagesPerField.get(
attribute.name,
)}
</span>
)}
</div>
</div>
<AfterField attribute={attribute} />
</Fragment>
);
})}
</>
);
},
);

View File

@ -1,4 +1,3 @@
import { useReducer, useEffect, memo } from "react"; import { useReducer, useEffect, memo } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
@ -29,10 +28,9 @@ export type TemplateProps = {
* to avoid pulling the default theme assets. * to avoid pulling the default theme assets.
*/ */
doFetchDefaultThemeResources: boolean; doFetchDefaultThemeResources: boolean;
} & { kcContext: KcContextBase; } & KcTemplateProps; } & { kcContext: KcContextBase } & KcTemplateProps;
export const Template = memo((props: TemplateProps) => { export const Template = memo((props: TemplateProps) => {
const { const {
displayInfo = false, displayInfo = false,
displayMessage = true, displayMessage = true,
@ -44,34 +42,34 @@ export const Template = memo((props: TemplateProps) => {
formNode, formNode,
infoNode = null, infoNode = null,
kcContext, kcContext,
doFetchDefaultThemeResources doFetchDefaultThemeResources,
} = props; } = props;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
useEffect(() => { console.log("Rendering this page with react using keycloakify") }, []); useEffect(() => {
console.log("Rendering this page with react using keycloakify");
}, []);
const { msg } = useKcMessage(); const { msg } = useKcMessage();
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag(); const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
const onChangeLanguageClickFactory = useCallbackFactory( const onChangeLanguageClickFactory = useCallbackFactory(
([languageTag]: [KcLanguageTag]) => ([languageTag]: [KcLanguageTag]) => setKcLanguageTag(languageTag),
setKcLanguageTag(languageTag)
); );
const onTryAnotherWayClick = useConstCallback(() => const onTryAnotherWayClick = useConstCallback(
(document.forms["kc-select-try-another-way-form" as never].submit(), false) () => (
document.forms["kc-select-try-another-way-form" as never].submit(),
false
),
); );
const { const { realm, locale, auth, url, message, isAppInitiatedAction } =
realm, locale, auth, kcContext;
url, message, isAppInitiatedAction
} = kcContext;
useEffect(() => { useEffect(() => {
if (!realm.internationalizationEnabled) { if (!realm.internationalizationEnabled) {
return; return;
} }
@ -82,15 +80,14 @@ export const Template = memo((props: TemplateProps) => {
return; return;
} }
window.location.href = window.location.href = locale.supported.find(
locale.supported.find(({ languageTag }) => languageTag === kcLanguageTag)!.url; ({ languageTag }) => languageTag === kcLanguageTag,
)!.url;
}, [kcLanguageTag]); }, [kcLanguageTag]);
const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false); const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
useEffect(() => { useEffect(() => {
if (!doFetchDefaultThemeResources) { if (!doFetchDefaultThemeResources) {
setExtraCssLoaded(); setExtraCssLoaded();
return; return;
@ -104,50 +101,49 @@ export const Template = memo((props: TemplateProps) => {
Promise.all( Promise.all(
[ [
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)), ...toArr(props.stylesCommon).map(relativePath =>
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath)) pathJoin(url.resourcesCommonPath, relativePath),
].map(href => appendHead({ ),
"type": "css", ...toArr(props.styles).map(relativePath =>
href pathJoin(url.resourcesPath, relativePath),
}))).then(() => { ),
].map(href =>
appendHead({
"type": "css",
href,
}),
),
).then(() => {
if (isUnmounted) {
return;
}
if (isUnmounted) { setExtraCssLoaded();
return; });
}
setExtraCssLoaded(); toArr(props.scripts).forEach(relativePath =>
appendHead({
});
toArr(props.scripts).forEach(
relativePath => appendHead({
"type": "javascript", "type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath) "src": pathJoin(url.resourcesPath, relativePath),
}) }),
); );
if (props.kcHtmlClass !== undefined) { if (props.kcHtmlClass !== undefined) {
const htmlClassList = const htmlClassList =
document.getElementsByTagName("html")[0] document.getElementsByTagName("html")[0].classList;
.classList;
const tokens = cx(props.kcHtmlClass).split(" ") const tokens = cx(props.kcHtmlClass).split(" ");
htmlClassList.add(...tokens); htmlClassList.add(...tokens);
cleanups.push(() => htmlClassList.remove(...tokens)); cleanups.push(() => htmlClassList.remove(...tokens));
} }
return () => { return () => {
isUnmounted = true; isUnmounted = true;
cleanups.forEach(f => f()); cleanups.forEach(f => f());
}; };
}, [props.kcHtmlClass]); }, [props.kcHtmlClass]);
if (!isExtraCssLoaded) { if (!isExtraCssLoaded) {
@ -156,163 +152,260 @@ export const Template = memo((props: TemplateProps) => {
return ( return (
<div className={cx(props.kcLoginClass)}> <div className={cx(props.kcLoginClass)}>
<div id="kc-header" className={cx(props.kcHeaderClass)}> <div id="kc-header" className={cx(props.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(props.kcHeaderWrapperClass)}> <div
id="kc-header-wrapper"
className={cx(props.kcHeaderWrapperClass)}
>
{msg("loginTitleHtml", realm.displayNameHtml)} {msg("loginTitleHtml", realm.displayNameHtml)}
</div> </div>
</div> </div>
<div className={cx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}> <div
className={cx(
props.kcFormCardClass,
displayWide && props.kcFormCardAccountClass,
)}
>
<header className={cx(props.kcFormHeaderClass)}> <header className={cx(props.kcFormHeaderClass)}>
{ {realm.internationalizationEnabled &&
( (assert(locale !== undefined), true) &&
realm.internationalizationEnabled && locale.supported.length > 1 && (
(assert(locale !== undefined), true) && <div id="kc-locale">
locale.supported.length > 1 <div
) && id="kc-locale-wrapper"
<div id="kc-locale"> className={cx(props.kcLocaleWrapperClass)}
<div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}> >
<div className="kc-dropdown" id="kc-locale-dropdown"> <div
<a href="#" id="kc-current-locale-link"> className="kc-dropdown"
{getKcLanguageTagLabel(kcLanguageTag)} id="kc-locale-dropdown"
</a> >
<ul> <a href="#" id="kc-current-locale-link">
{ {getKcLanguageTagLabel(
locale.supported.map( kcLanguageTag,
({ languageTag }) => )}
<li key={languageTag} className="kc-dropdown-item"> </a>
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}> <ul>
{getKcLanguageTagLabel(languageTag)} {locale.supported.map(
({ languageTag }) => (
<li
key={languageTag}
className="kc-dropdown-item"
>
<a
href="#"
onClick={onChangeLanguageClickFactory(
languageTag,
)}
>
{getKcLanguageTagLabel(
languageTag,
)}
</a> </a>
</li> </li>
) ),
} )}
</ul> </ul>
</div>
</div>
</div>
)}
{!(
auth !== undefined &&
auth.showUsername &&
!auth.showResetCredentials
) ? (
displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div
className={cx(
props.kcLabelWrapperClass,
"subtitle",
)}
>
<span className="subtitle">
<span className="required">*</span>
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
<h1 id="kc-page-title">{headerNode}</h1>
</div>
</div>
) : (
<h1 id="kc-page-title">{headerNode}</h1>
)
) : displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div
className={cx(
props.kcLabelWrapperClass,
"subtitle",
)}
>
<span className="subtitle">
<span className="required">*</span>{" "}
{msg("requiredFields")}
</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">
{auth?.attemptedUsername}
</label>
<a
id="reset-login"
href={url.loginRestartFlowUrl}
>
<div className="kc-login-tooltip">
<i
className={cx(
props.kcResetFlowIcon,
)}
></i>
<span className="kc-tooltip-text">
{msg("restartLoginTooltip")}
</span>
</div>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>
) : (
} <>
{ {showUsernameNode}
!( <div className={cx(props.kcFormGroupClass)}>
auth !== undefined && <div id="kc-username">
auth.showUsername && <label id="kc-attempted-username">
!auth.showResetCredentials {auth?.attemptedUsername}
) ? </label>
( <a
displayRequiredFields ? id="reset-login"
( href={url.loginRestartFlowUrl}
>
<div className={cx(props.kcContentWrapperClass)}> <div className="kc-login-tooltip">
<div className={cx(props.kcLabelWrapperClass, "subtitle")}> <i
<span className="subtitle"> className={cx(
<span className="required">*</span> props.kcResetFlowIcon,
{msg("requiredFields")} )}
</span> ></i>
</div> <span className="kc-tooltip-text">
<div className="col-md-10"> {msg("restartLoginTooltip")}
<h1 id="kc-page-title">{headerNode}</h1> </span>
</div>
</div> </div>
</a>
) </div>
: </div>
( </>
)}
<h1 id="kc-page-title">{headerNode}</h1>
)
) : (
displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"><span className="required">*</span> {msg("requiredFields")}</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</div>
</div>
) : (
<>
{showUsernameNode}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip">
<i className={cx(props.kcResetFlowIcon)}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div>
</a>
</div>
</div>
</>
)
)
}
</header> </header>
<div id="kc-content"> <div id="kc-content">
<div id="kc-content-wrapper"> <div id="kc-content-wrapper">
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{ {displayMessage &&
( message !== undefined &&
displayMessage && (message.type !== "warning" ||
message !== undefined && !isAppInitiatedAction) && (
( <div
message.type !== "warning" || className={cx(
!isAppInitiatedAction "alert",
) `alert-${message.type}`,
) && )}
<div className={cx("alert", `alert-${message.type}`)}> >
{message.type === "success" && <span className={cx(props.kcFeedbackSuccessIcon)}></span>} {message.type === "success" && (
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>} <span
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>} className={cx(
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>} props.kcFeedbackSuccessIcon,
<span )}
className="kc-feedback-text" ></span>
dangerouslySetInnerHTML={{ "__html": message.summary }} )}
/> {message.type === "warning" && (
</div> <span
} className={cx(
props.kcFeedbackWarningIcon,
)}
></span>
)}
{message.type === "error" && (
<span
className={cx(
props.kcFeedbackErrorIcon,
)}
></span>
)}
{message.type === "info" && (
<span
className={cx(
props.kcFeedbackInfoIcon,
)}
></span>
)}
<span
className="kc-feedback-text"
dangerouslySetInnerHTML={{
"__html": message.summary,
}}
/>
</div>
)}
{formNode} {formNode}
{ {auth !== undefined &&
( auth.showTryAnotherWayLink &&
auth !== undefined && showAnotherWayIfPresent && (
auth.showTryAnotherWayLink && <form
showAnotherWayIfPresent id="kc-select-try-another-way-form"
) && action={url.loginAction}
method="post"
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && props.kcContentWrapperClass)} > className={cx(
<div className={cx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])} > displayWide &&
<div className={cx(props.kcFormGroupClass)}> props.kcContentWrapperClass,
<input type="hidden" name="tryAnotherWay" value="on" /> )}
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{msg("doTryAnotherWay")}</a> >
<div
className={cx(
displayWide && [
props.kcFormSocialAccountContentClass,
props.kcFormSocialAccountClass,
],
)}
>
<div
className={cx(
props.kcFormGroupClass,
)}
>
<input
type="hidden"
name="tryAnotherWay"
value="on"
/>
<a
href="#"
id="try-another-way"
onClick={onTryAnotherWayClick}
>
{msg("doTryAnotherWay")}
</a>
</div>
</div> </div>
</div > </form>
</form> )}
} {displayInfo && (
{ <div
displayInfo && id="kc-info"
className={cx(props.kcSignUpClass)}
<div id="kc-info" className={cx(props.kcSignUpClass)}> >
<div id="kc-info-wrapper" className={cx(props.kcInfoAreaWrapperClass)}> <div
id="kc-info-wrapper"
className={cx(props.kcInfoAreaWrapperClass)}
>
{infoNode} {infoNode}
</div> </div>
</div> </div>
} )}
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,56 +5,57 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms; } & KcProps) => { export const Terms = memo(
({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => {
const { msg, msgStr } = useKcMessage();
const { msg, msgStr } = useKcMessage(); const { cx } = useCssAndCx();
const { cx } = useCssAndCx();
const { url } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("termsTitle")}
formNode={
<>
<div id="kc-terms-text">
{msg("termsText")}
</div>
<form className="form-actions" action={url.loginAction} method="POST">
<input
className={cx(
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass
)}
name="accept"
id="kc-accept"
type="submit"
value={msgStr("doAccept")}
/>
<input
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass
)}
name="cancel"
id="kc-decline"
type="submit"
value={msgStr("doDecline")}
/>
</form>
<div className="clearfix" />
</>
}
/>
);
});
const { url } = kcContext;
return (
<Template
{...{ kcContext, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("termsTitle")}
formNode={
<>
<div id="kc-terms-text">{msg("termsText")}</div>
<form
className="form-actions"
action={url.loginAction}
method="POST"
>
<input
className={cx(
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass,
)}
name="accept"
id="kc-accept"
type="submit"
value={msgStr("doAccept")}
/>
<input
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass,
)}
name="cancel"
id="kc-decline"
type="submit"
value={msgStr("doDecline")}
/>
</form>
<div className="clearfix" />
</>
}
/>
);
},
);

View File

@ -1,4 +1,3 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl"; import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { KcLanguageTag } from "../i18n/KcLanguageTag"; import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -6,21 +5,29 @@ import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/useKcMessage"; import type { MessageKey } from "../i18n/useKcMessage";
import type { LanguageLabel } from "../i18n/KcLanguageTag"; import type { LanguageLabel } from "../i18n/KcLanguageTag";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = type ExtractAfterStartingWith<
StrEnum extends `${Prefix}${infer U}` ? U : never; Prefix extends string,
StrEnum,
> = StrEnum extends `${Prefix}${infer U}` ? U : never;
/** Take theses type definition with a grain of salt. /** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages. * Some values might be undefined on some pages.
* (ex: url.loginAction is undefined on error.ftl) * (ex: url.loginAction is undefined on error.ftl)
*/ */
export type KcContextBase = export type KcContextBase =
KcContextBase.Login | KcContextBase.Register | KcContextBase.RegisterUserProfile | KcContextBase.Info | | KcContextBase.Login
KcContextBase.Error | KcContextBase.LoginResetPassword | KcContextBase.LoginVerifyEmail | | KcContextBase.Register
KcContextBase.Terms | KcContextBase.LoginOtp | KcContextBase.LoginUpdateProfile | | KcContextBase.RegisterUserProfile
KcContextBase.LoginIdpLinkConfirm; | KcContextBase.Info
| KcContextBase.Error
| KcContextBase.LoginResetPassword
| KcContextBase.LoginVerifyEmail
| KcContextBase.Terms
| KcContextBase.LoginOtp
| KcContextBase.LoginUpdateProfile
| KcContextBase.LoginIdpLinkConfirm;
export declare namespace KcContextBase { export declare namespace KcContextBase {
export type Common = { export type Common = {
url: { url: {
loginAction: string; loginAction: string;
@ -46,7 +53,7 @@ export declare namespace KcContextBase {
//label: LanguageLabel; //label: LanguageLabel;
}[]; }[];
current: LanguageLabel; current: LanguageLabel;
}, };
auth?: { auth?: {
showUsername: boolean; showUsername: boolean;
showResetCredentials: boolean; showResetCredentials: boolean;
@ -61,7 +68,7 @@ export declare namespace KcContextBase {
client: { client: {
clientId: string; clientId: string;
name?: string; name?: string;
} };
isAppInitiatedAction: boolean; isAppInitiatedAction: boolean;
messagesPerField: { messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined; printIfExists: <T>(fieldName: string, x: T) => T | undefined;
@ -100,7 +107,7 @@ export declare namespace KcContextBase {
alias: string; alias: string;
providerId: string; providerId: string;
displayName: string; displayName: string;
}[] }[];
}; };
}; };
@ -118,7 +125,7 @@ export declare namespace KcContextBase {
alias: string; alias: string;
providerId: string; providerId: string;
displayName: string; displayName: string;
}[] }[];
}; };
}; };
@ -131,38 +138,39 @@ export declare namespace KcContextBase {
lastName?: string; lastName?: string;
email?: string; email?: string;
username?: string; username?: string;
} };
}; };
}; };
export type RegisterUserProfile = RegisterCommon & { export type RegisterUserProfile = RegisterCommon & {
pageId: "register-user-profile.ftl"; pageId: "register-user-profile.ftl";
profile: { profile: {
context: "REGISTRATION_PROFILE"; context: "REGISTRATION_PROFILE";
attributes: Attribute[]; attributes: Attribute[];
attributesByName: Record<string, Attribute>; attributesByName: Record<string, Attribute>;
} };
}; };
export type Info = Common & { export type Info = Common & {
pageId: "info.ftl"; pageId: "info.ftl";
messageHeader?: string; messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKey>[]; requiredActions?: ExtractAfterStartingWith<
"requiredAction.",
MessageKey
>[];
skipLink: boolean; skipLink: boolean;
pageRedirectUri?: string; pageRedirectUri?: string;
actionUri?: string; actionUri?: string;
client: { client: {
baseUrl?: string; baseUrl?: string;
} };
}; };
export type Error = Common & { export type Error = Common & {
pageId: "error.ftl"; pageId: "error.ftl";
client?: { client?: {
baseUrl?: string; baseUrl?: string;
}, };
message: NonNullable<Common["message"]>; message: NonNullable<Common["message"]>;
}; };
@ -170,7 +178,7 @@ export declare namespace KcContextBase {
pageId: "login-reset-password.ftl"; pageId: "login-reset-password.ftl";
realm: { realm: {
loginWithEmailAllowed: boolean; loginWithEmailAllowed: boolean;
} };
}; };
export type LoginVerifyEmail = Common & { export type LoginVerifyEmail = Common & {
@ -184,8 +192,8 @@ export declare namespace KcContextBase {
export type LoginOtp = Common & { export type LoginOtp = Common & {
pageId: "login-otp.ftl"; pageId: "login-otp.ftl";
otpLogin: { otpLogin: {
userOtpCredentials: { id: string; userLabel: string; }[]; userOtpCredentials: { id: string; userLabel: string }[];
} };
}; };
export type LoginUpdateProfile = Common & { export type LoginUpdateProfile = Common & {
@ -203,7 +211,6 @@ export declare namespace KcContextBase {
pageId: "login-idp-link-confirm.ftl"; pageId: "login-idp-link-confirm.ftl";
idpAlias: string; idpAlias: string;
}; };
} }
export type Attribute = { export type Attribute = {
@ -218,7 +225,7 @@ export type Attribute = {
autocomplete?: string; autocomplete?: string;
validators: Validators; validators: Validators;
annotations: Record<string, string>; annotations: Record<string, string>;
groupAnnotations: Record<string, string> groupAnnotations: Record<string, string>;
}; };
export type Validators = Partial<{ export type Validators = Partial<{
@ -226,42 +233,40 @@ export type Validators = Partial<{
double: Validators.DoIgnoreEmpty & Validators.Range; double: Validators.DoIgnoreEmpty & Validators.Range;
integer: Validators.DoIgnoreEmpty & Validators.Range; integer: Validators.DoIgnoreEmpty & Validators.Range;
email: Validators.DoIgnoreEmpty; email: Validators.DoIgnoreEmpty;
'up-immutable-attribute': {}; "up-immutable-attribute": {};
'up-attribute-required-by-metadata-value': {}; "up-attribute-required-by-metadata-value": {};
'up-username-has-value': {}; "up-username-has-value": {};
'up-duplicate-username': {}; "up-duplicate-username": {};
'up-username-mutation': {}; "up-username-mutation": {};
'up-email-exists-as-username': {}; "up-email-exists-as-username": {};
'up-blank-attribute-value': Validators.ErrorMessage & { "up-blank-attribute-value": Validators.ErrorMessage & {
'fail-on-null': boolean; "fail-on-null": boolean;
}; };
'up-duplicate-email': {}; "up-duplicate-email": {};
'local-date': Validators.DoIgnoreEmpty; "local-date": Validators.DoIgnoreEmpty;
pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string; }; pattern: Validators.DoIgnoreEmpty &
'person-name-prohibited-characters': Validators.DoIgnoreEmpty & Validators.ErrorMessage; Validators.ErrorMessage & { pattern: string };
"person-name-prohibited-characters": Validators.DoIgnoreEmpty &
Validators.ErrorMessage;
uri: Validators.DoIgnoreEmpty; uri: Validators.DoIgnoreEmpty;
'username-prohibited-characters': Validators.DoIgnoreEmpty & Validators.ErrorMessage; "username-prohibited-characters": Validators.DoIgnoreEmpty &
Validators.ErrorMessage;
}>; }>;
export declare namespace Validators { export declare namespace Validators {
export type DoIgnoreEmpty = { export type DoIgnoreEmpty = {
'ignore.empty.value'?: boolean; "ignore.empty.value"?: boolean;
}; };
export type ErrorMessage = { export type ErrorMessage = {
'error-message'?: string; "error-message"?: string;
} };
export type Range = { export type Range = {
/** "0", "1", "2"... yeah I know, don't tell me */ /** "0", "1", "2"... yeah I know, don't tell me */
min?: string; min?: string;
max?: string; max?: string;
}; };
} }
assert<Equals<KcContextBase["pageId"], PageId>>(); assert<Equals<KcContextBase["pageId"], PageId>>();

View File

@ -1,4 +1,3 @@
import type { KcContextBase } from "./KcContextBase"; import type { KcContextBase } from "./KcContextBase";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks"; import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName"; import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
@ -6,81 +5,71 @@ import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import type { DeepPartial } from "../tools/DeepPartial"; import type { DeepPartial } from "../tools/DeepPartial";
import { deepAssign } from "../tools/deepAssign"; import { deepAssign } from "../tools/deepAssign";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> =
[KcContextExtended] extends [never]
? KcContextBase
: AndByDiscriminatingKey<
"pageId",
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
export type ExtendsKcContextBase< export function getKcContext<
KcContextExtended extends { pageId: string; } KcContextExtended extends { pageId: string } = never,
> = >(params?: {
[KcContextExtended] extends [never] ? mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
KcContextBase : mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
AndByDiscriminatingKey< }): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } {
"pageId", const { mockPageId, mockData } = params ?? {};
KcContextExtended & KcContextBase.Common,
KcContextBase
>;
export function getKcContext<KcContextExtended extends { pageId: string; } = never>( if (mockPageId !== undefined) {
params?: { //TODO maybe trow if no mock fo custom page
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
}
): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined; } {
const { const kcContextDefaultMock = kcContextMocks.find(
mockPageId, ({ pageId }) => pageId === mockPageId,
mockData );
} = params ?? {};
if (mockPageId !== undefined) { const partialKcContextCustomMock = mockData?.find(
({ pageId }) => pageId === mockPageId,
);
//TODO maybe trow if no mock fo custom page if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`,
].join("\n"),
);
}
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId); const kcContext: any = {};
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId); deepAssign({
"target": kcContext,
"source":
kcContextDefaultMock !== undefined
? kcContextDefaultMock
: { "pageId": mockPageId, ...kcContextCommonMock },
});
if ( if (partialKcContextCustomMock !== undefined) {
kcContextDefaultMock === undefined && deepAssign({
partialKcContextCustomMock === undefined "target": kcContext,
) { "source": partialKcContextCustomMock,
});
}
console.warn([ return { kcContext };
`WARNING: You declared the non build in page ${mockPageId} but you didn't `, }
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n"));
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ?
kcContextDefaultMock :
{ "pageId": mockPageId, ...kcContextCommonMock, }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
return {
"kcContext":
typeof window === "undefined" ?
undefined :
(window as any)[ftlValuesGlobalName]
};
return {
"kcContext":
typeof window === "undefined"
? undefined
: (window as any)[ftlValuesGlobalName],
};
} }

View File

@ -1,2 +1,2 @@
export type { KcContextBase } from "./KcContextBase"; export type { KcContextBase } from "./KcContextBase";
export { getKcContext } from "./getKcContext"; export { getKcContext } from "./getKcContext";

View File

@ -1 +1 @@
export * from "./kcContextMocks"; export * from "./kcContextMocks";

View File

@ -1,4 +1,3 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
import type { KcContextBase, Attribute } from "../KcContextBase"; import type { KcContextBase, Attribute } from "../KcContextBase";
import { getEvtKcLanguage } from "../../i18n/useKcLanguageTag"; import { getEvtKcLanguage } from "../../i18n/useKcLanguageTag";
@ -11,427 +10,387 @@ import { join as pathJoin } from "path";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/"; const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContextBase.Common = { export const kcContextCommonMock: KcContextBase.Common = {
"url": { "url": {
"loginAction": "#", "loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, resourcesPath), "resourcesPath": pathJoin(PUBLIC_URL, resourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath), "resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg", "loginRestartFlowUrl":
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg", "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
}, "loginUrl":
"realm": { "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
"displayName": "myrealm", },
"displayNameHtml": "myrealm", "realm": {
"internationalizationEnabled": true, "displayName": "myrealm",
"registrationEmailAsUsername": true, "displayNameHtml": "myrealm",
}, "internationalizationEnabled": true,
"messagesPerField": { "registrationEmailAsUsername": true,
"printIfExists": (...[, x]) => x, },
"existsError": () => true, "messagesPerField": {
"get": key => `Fake error for ${key}`, "printIfExists": (...[, x]) => x,
"exists": () => true "existsError": () => true,
}, "get": key => `Fake error for ${key}`,
"locale": { "exists": () => true,
"supported": [ },
{ "locale": {
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de", "supported": [
"languageTag": "de" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
{ "languageTag": "de",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no", },
"languageTag": "no" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
{ "languageTag": "no",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru", },
"languageTag": "ru" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
{ "languageTag": "ru",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv", },
"languageTag": "sv" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
{ "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", },
"languageTag": "pt-BR" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
{ "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", },
"languageTag": "lt" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
{ "languageTag": "lt",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en", },
"languageTag": "en" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
{ "languageTag": "en",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it", },
"languageTag": "it" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
{ "languageTag": "it",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr", },
"languageTag": "fr" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
{ "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", },
"languageTag": "zh-CN" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
{ "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", },
"languageTag": "es" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
{ "languageTag": "es",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs", },
"languageTag": "cs" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
{ "languageTag": "cs",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja", },
"languageTag": "ja" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
{ "languageTag": "ja",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk", },
"languageTag": "sk" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
{ "languageTag": "sk",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl", },
"languageTag": "pl" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
{ "languageTag": "pl",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca", },
"languageTag": "ca" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
{ "languageTag": "ca",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl", },
"languageTag": "nl" {
}, "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
{ "languageTag": "nl",
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr", },
"languageTag": "tr" {
} "url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
], "languageTag": "tr",
//"current": null as any },
"current": "English" ],
}, //"current": null as any
"auth": { "current": "English",
"showUsername": false, },
"showResetCredentials": false, "auth": {
"showTryAnotherWayLink": false "showUsername": false,
}, "showResetCredentials": false,
"client": { "showTryAnotherWayLink": false,
"clientId": "myApp" },
}, "client": {
"scripts": [], "clientId": "myApp",
"message": { },
"type": "success", "scripts": [],
"summary": "This is a test message" "message": {
}, "type": "success",
"isAppInitiatedAction": false, "summary": "This is a test message",
},
"isAppInitiatedAction": false,
}; };
Object.defineProperty(kcContextCommonMock.locale!, "current", {
Object.defineProperty( "get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
kcContextCommonMock.locale!, "enumerable": true,
"current", });
{
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
"enumerable": true
}
);
const loginUrl = { const loginUrl = {
...kcContextCommonMock.url, ...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg", "loginResetCredentialsUrl":
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg" "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl":
"/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
}; };
export const kcContextMocks: KcContextBase[] = [ export const kcContextMocks: KcContextBase[] = [
id<KcContextBase.Login>({ id<KcContextBase.Login>({
...kcContextCommonMock, ...kcContextCommonMock,
"pageId": "login.ftl", "pageId": "login.ftl",
"url": loginUrl, "url": loginUrl,
"realm": { "realm": {
...kcContextCommonMock.realm, ...kcContextCommonMock.realm,
"loginWithEmailAllowed": true, "loginWithEmailAllowed": true,
"rememberMe": true, "rememberMe": true,
"password": true, "password": true,
"resetPasswordAllowed": true, "resetPasswordAllowed": true,
"registrationAllowed": true "registrationAllowed": true,
}, },
"auth": kcContextCommonMock.auth!, "auth": kcContextCommonMock.auth!,
"social": { "social": {
"displayInfo": true "displayInfo": true,
}, },
"usernameEditDisabled": false, "usernameEditDisabled": false,
"login": { "login": {
"rememberMe": false "rememberMe": false,
}, },
"registrationDisabled": false, "registrationDisabled": false,
}),
...(() => {
const registerCommon: KcContextBase.RegisterCommon = {
...kcContextCommonMock,
"url": {
...loginUrl,
"registrationAction":
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0",
},
"scripts": [],
"isAppInitiatedAction": false,
"passwordRequired": true,
"recaptchaRequired": false,
"social": {
"displayInfo": true,
},
};
}), return [
...(() => { id<KcContextBase.Register>({
"pageId": "register.ftl",
...registerCommon,
"register": {
"formData": {},
},
}),
id<KcContextBase.RegisterUserProfile>({
"pageId": "register-user-profile.ftl",
...registerCommon,
const registerCommon: KcContextBase.RegisterCommon = { "profile": {
...kcContextCommonMock, "context": "REGISTRATION_PROFILE" as const,
"url": { ...(() => {
...loginUrl, const attributes: Attribute[] = [
"registrationAction": "http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0" {
}, "validators": {
"scripts": [], "username-prohibited-characters": {
"isAppInitiatedAction": false, "ignore.empty.value": true,
"passwordRequired": true, },
"recaptchaRequired": false, "up-username-has-value": {},
"social": { "length": {
"displayInfo": true "ignore.empty.value": true,
}, "min": "3",
}; "max": "255",
},
"up-duplicate-username": {},
"up-username-mutation": {},
},
"displayName": "${username}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "username",
"readOnly": false,
"name": "username",
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true,
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false,
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true,
},
},
"displayName": "${email}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email",
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true,
},
"person-name-prohibited-characters": {
"ignore.empty.value": true,
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value":
{},
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName",
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true,
},
"person-name-prohibited-characters": {
"ignore.empty.value": true,
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value":
{},
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName",
},
{
"validators": {
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "9",
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value":
{},
"email": {
"ignore.empty.value": true,
},
},
"displayName": "${foo}",
"annotations": {
"this_is_second_key":
"this_is_second_value",
"this_is_first_key": "this_is_first_value",
},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "foo",
},
];
return [ return {
id<KcContextBase.Register>({ attributes,
"pageId": "register.ftl", "attributesByName": Object.fromEntries(
...registerCommon, attributes.map(attribute => [
"register": { attribute.name,
"formData": {} attribute,
}, ]),
}), ) as any,
id<KcContextBase.RegisterUserProfile>({ } as any;
"pageId": "register-user-profile.ftl", })(),
...registerCommon, },
}),
];
"profile": { })(),
"context": "REGISTRATION_PROFILE" as const, id<KcContextBase.Info>({
...(() => { ...kcContextCommonMock,
"pageId": "info.ftl",
const attributes: Attribute[] = [ "messageHeader": "<Message header>",
{ "requiredActions": undefined,
"validators": { "skipLink": false,
"username-prohibited-characters": { "actionUri": "#",
"ignore.empty.value": true "client": {
}, "clientId": "myApp",
"up-username-has-value": { "baseUrl": "#",
},
}, }),
"length": { id<KcContextBase.Error>({
"ignore.empty.value": true, ...kcContextCommonMock,
"min": "3", "pageId": "error.ftl",
"max": "255" "client": {
}, "clientId": "myApp",
"up-duplicate-username": { "baseUrl": "#",
},
}, "message": {
"up-username-mutation": { "type": "error",
"summary": "This is the error message",
} },
}, }),
"displayName": "${username}", id<KcContextBase.LoginResetPassword>({
"annotations": { ...kcContextCommonMock,
"pageId": "login-reset-password.ftl",
}, "realm": {
"required": true, ...kcContextCommonMock.realm,
"groupAnnotations": { "loginWithEmailAllowed": false,
},
}, }),
"autocomplete": "username", id<KcContextBase.LoginVerifyEmail>({
"readOnly": false, ...kcContextCommonMock,
"name": "username" "pageId": "login-verify-email.ftl",
}, }),
{ id<KcContextBase.Terms>({
"validators": { ...kcContextCommonMock,
"up-email-exists-as-username": { "pageId": "terms.ftl",
}),
}, id<KcContextBase.LoginOtp>({
"length": { ...kcContextCommonMock,
"max": "255", "pageId": "login-otp.ftl",
"ignore.empty.value": true "otpLogin": {
}, "userOtpCredentials": [
"up-blank-attribute-value": { {
"error-message": "missingEmailMessage", "id": "id1",
"fail-on-null": false "userLabel": "label1",
}, },
"up-duplicate-email": { {
"id": "id2",
}, "userLabel": "label2",
"email": { },
"ignore.empty.value": true ],
} },
}, }),
"displayName": "${email}", id<KcContextBase.LoginUpdateProfile>({
"annotations": { ...kcContextCommonMock,
"pageId": "login-update-profile.ftl",
}, "user": {
"required": true, "editUsernameAllowed": true,
"groupAnnotations": { "username": "anUsername",
"email": "foo@example.com",
}, "firstName": "aFirstName",
"autocomplete": "email", "lastName": "aLastName",
"readOnly": false, },
"name": "email" }),
}, id<KcContextBase.LoginIdpLinkConfirm>({
{ ...kcContextCommonMock,
"validators": { "pageId": "login-idp-link-confirm.ftl",
"length": { "idpAlias": "FranceConnect",
"max": "255", }),
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {
},
"up-attribute-required-by-metadata-value": {
}
},
"displayName": "${firstName}",
"annotations": {
},
"required": true,
"groupAnnotations": {
},
"readOnly": false,
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {
},
"required": true,
"groupAnnotations": {
},
"readOnly": false,
"name": "lastName"
},
{
"validators": {
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "9"
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {},
"email": {
"ignore.empty.value": true
}
},
"displayName": "${foo}",
"annotations": {
"this_is_second_key": "this_is_second_value",
"this_is_first_key": "this_is_first_value"
},
"required": true,
"groupAnnotations": {
},
"readOnly": false,
"name": "foo"
}
];
return {
attributes,
"attributesByName": Object.fromEntries(
attributes.map(attribute => [attribute.name, attribute])
) as any
} as any;
})()
}
})
];
})(),
id<KcContextBase.Info>({
...kcContextCommonMock,
"pageId": "info.ftl",
"messageHeader": "<Message header>",
"requiredActions": undefined,
"skipLink": false,
"actionUri": "#",
"client": {
"clientId": "myApp",
"baseUrl": "#"
}
}),
id<KcContextBase.Error>({
...kcContextCommonMock,
"pageId": "error.ftl",
"client": {
"clientId": "myApp",
"baseUrl": "#"
},
"message": {
"type": "error",
"summary": "This is the error message"
}
}),
id<KcContextBase.LoginResetPassword>({
...kcContextCommonMock,
"pageId": "login-reset-password.ftl",
"realm": {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": false
}
}),
id<KcContextBase.LoginVerifyEmail>({
...kcContextCommonMock,
"pageId": "login-verify-email.ftl"
}),
id<KcContextBase.Terms>({
...kcContextCommonMock,
"pageId": "terms.ftl"
}),
id<KcContextBase.LoginOtp>({
...kcContextCommonMock,
"pageId": "login-otp.ftl",
"otpLogin": {
"userOtpCredentials": [
{
"id": "id1",
"userLabel": "label1"
},
{
"id": "id2",
"userLabel": "label2"
}
]
}
}),
id<KcContextBase.LoginUpdateProfile>({
...kcContextCommonMock,
"pageId": "login-update-profile.ftl",
"user": {
"editUsernameAllowed": true,
"username": "anUsername",
"email": "foo@example.com",
"firstName": "aFirstName",
"lastName": "aLastName"
}
}),
id<KcContextBase.LoginIdpLinkConfirm>({
...kcContextCommonMock,
"pageId": "login-idp-link-confirm.ftl",
"idpAlias": "FranceConnect"
})
]; ];

View File

@ -1,6 +1,8 @@
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
export const subDirOfPublicDirBasename = "keycloak_static"; export const subDirOfPublicDirBasename = "keycloak_static";
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "/resources"); export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "/resources");
export const resourcesCommonPath = pathJoin(subDirOfPublicDirBasename, "/resources_common"); export const resourcesCommonPath = pathJoin(
subDirOfPublicDirBasename,
"/resources_common",
);

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,243 +2,437 @@
//PLEASE DO NOT EDIT MANUALLY //PLEASE DO NOT EDIT MANUALLY
/* spell-checker: disable */ /* spell-checker: disable */
export const kcMessages= { export const kcMessages = {
"ca": { "ca": {
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.", "invalidPasswordHistoryMessage":
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.", "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
"invalidPasswordMinLengthMessage": "Contrasenya incorrecta: longitud mínima {0}.", "invalidPasswordMinDigitsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.", "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.", "invalidPasswordMinLengthMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.", "Contrasenya incorrecta: longitud mínima {0}.",
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.", "invalidPasswordMinLowerCaseCharsMessage":
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular." "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.",
}, "invalidPasswordMinSpecialCharsMessage":
"de": { "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
"invalidPasswordMinLengthMessage": "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.", "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
"invalidPasswordMinDigitsMessage": "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.", "invalidPasswordNotUsernameMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.", "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
"invalidPasswordMinSpecialCharsMessage": "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.", "invalidPasswordRegexPatternMessage":
"invalidPasswordNotUsernameMessage": "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.", "Contrasenya incorrecta: no compleix l'expressió regular.",
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.", },
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.", "de": {
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.", "invalidPasswordMinLengthMessage":
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht." "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.",
}, "invalidPasswordMinLowerCaseCharsMessage":
"en": { "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.",
"invalidPasswordMinLengthMessage": "Invalid password: minimum length {0}.", "invalidPasswordMinDigitsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Invalid password: must contain at least {0} lower case characters.", "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.",
"invalidPasswordMinDigitsMessage": "Invalid password: must contain at least {0} numerical digits.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Invalid password: must contain at least {0} upper case characters.", "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.",
"invalidPasswordMinSpecialCharsMessage": "Invalid password: must contain at least {0} special characters.", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordNotUsernameMessage": "Invalid password: must not be equal to the username.", "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.",
"invalidPasswordRegexPatternMessage": "Invalid password: fails to match regex pattern(s).", "invalidPasswordNotUsernameMessage":
"invalidPasswordHistoryMessage": "Invalid password: must not be equal to any of last {0} passwords.", "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.",
"invalidPasswordBlacklistedMessage": "Invalid password: password is blacklisted.", "invalidPasswordRegexPatternMessage":
"invalidPasswordGenericMessage": "Invalid password: new password does not match password policies.", "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
"ldapErrorInvalidCustomFilter": "Custom configured LDAP filter does not start with \"(\" or does not end with \")\".", "invalidPasswordHistoryMessage":
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout must be a number", "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
"ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number", "invalidPasswordBlacklistedMessage":
"ldapErrorMissingClientId": "Client ID needs to be provided in config when Realm Roles Mapping is not used.", "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Not possible to preserve group inheritance and use UID membership type together.", "invalidPasswordGenericMessage":
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Can not set write only when LDAP provider mode is not WRITABLE", "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht.",
"ldapErrorCantWriteOnlyAndReadOnly": "Can not set write-only and read-only together", },
"ldapErrorCantEnableStartTlsAndConnectionPooling": "Can not enable both StartTLS and connection pooling.", "en": {
"ldapErrorCantEnableUnsyncedAndImportOff": "Can not disable Importing users when LDAP provider mode is UNSYNCED", "invalidPasswordMinLengthMessage":
"ldapErrorMissingGroupsPathGroup": "Groups path group does not exist - please create the group on specified path first", "Invalid password: minimum length {0}.",
"clientRedirectURIsFragmentError": "Redirect URIs must not contain an URI fragment", "invalidPasswordMinLowerCaseCharsMessage":
"clientRootURLFragmentError": "Root URL must not contain an URL fragment", "Invalid password: must contain at least {0} lower case characters.",
"clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme", "invalidPasswordMinDigitsMessage":
"clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme", "Invalid password: must contain at least {0} numerical digits.",
"clientRedirectURIsIllegalSchemeError": "A redirect URI uses an illegal scheme", "invalidPasswordMinUpperCaseCharsMessage":
"clientBaseURLInvalid": "Base URL is not a valid URL", "Invalid password: must contain at least {0} upper case characters.",
"clientRootURLInvalid": "Root URL is not a valid URL", "invalidPasswordMinSpecialCharsMessage":
"clientRedirectURIsInvalid": "A redirect URI is not a valid URI", "Invalid password: must contain at least {0} special characters.",
"pairwiseMalformedClientRedirectURI": "Client contained an invalid redirect URI.", "invalidPasswordNotUsernameMessage":
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs must contain a valid host component.", "Invalid password: must not be equal to the username.",
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.", "invalidPasswordRegexPatternMessage":
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.", "Invalid password: fails to match regex pattern(s).",
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.", "invalidPasswordHistoryMessage":
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI." "Invalid password: must not be equal to any of last {0} passwords.",
}, "invalidPasswordBlacklistedMessage":
"es": { "Invalid password: password is blacklisted.",
"invalidPasswordMinLengthMessage": "Contraseña incorrecta: longitud mínima {0}.", "invalidPasswordGenericMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.", "Invalid password: new password does not match password policies.",
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.", "ldapErrorInvalidCustomFilter":
"invalidPasswordMinUpperCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.", 'Custom configured LDAP filter does not start with "(" or does not end with ")".',
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.", "ldapErrorConnectionTimeoutNotNumber":
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.", "Connection Timeout must be a number",
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.", "ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas." "ldapErrorMissingClientId":
}, "Client ID needs to be provided in config when Realm Roles Mapping is not used.",
"fr": { "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"invalidPasswordMinLengthMessage": "Mot de passe invalide : longueur minimale requise de {0}.", "Not possible to preserve group inheritance and use UID membership type together.",
"invalidPasswordMinLowerCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.", "ldapErrorCantWriteOnlyForReadOnlyLdap":
"invalidPasswordMinDigitsMessage": "Mot de passe invalide : doit contenir au moins {0} chiffre(s).", "Can not set write only when LDAP provider mode is not WRITABLE",
"invalidPasswordMinUpperCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.", "ldapErrorCantWriteOnlyAndReadOnly":
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.", "Can not set write-only and read-only together",
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.", "ldapErrorCantEnableStartTlsAndConnectionPooling":
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.", "Can not enable both StartTLS and connection pooling.",
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe." "ldapErrorCantEnableUnsyncedAndImportOff":
}, "Can not disable Importing users when LDAP provider mode is UNSYNCED",
"it": {}, "ldapErrorMissingGroupsPathGroup":
"ja": { "Groups path group does not exist - please create the group on specified path first",
"invalidPasswordMinLengthMessage": "無効なパスワード: 最小{0}の長さが必要です。", "clientRedirectURIsFragmentError":
"invalidPasswordMinLowerCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。", "Redirect URIs must not contain an URI fragment",
"invalidPasswordMinDigitsMessage": "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。", "clientRootURLFragmentError":
"invalidPasswordMinUpperCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。", "Root URL must not contain an URL fragment",
"invalidPasswordMinSpecialCharsMessage": "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。", "clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme",
"invalidPasswordNotUsernameMessage": "無効なパスワード: ユーザー名と同じパスワードは禁止されています。", "clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme",
"invalidPasswordRegexPatternMessage": "無効なパスワード: 正規表現パターンと一致しません。", "clientRedirectURIsIllegalSchemeError":
"invalidPasswordHistoryMessage": "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。", "A redirect URI uses an illegal scheme",
"invalidPasswordBlacklistedMessage": "無効なパスワード: パスワードがブラックリストに含まれています。", "clientBaseURLInvalid": "Base URL is not a valid URL",
"invalidPasswordGenericMessage": "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。", "clientRootURLInvalid": "Root URL is not a valid URL",
"ldapErrorInvalidCustomFilter": "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。", "clientRedirectURIsInvalid": "A redirect URI is not a valid URI",
"ldapErrorConnectionTimeoutNotNumber": "接続タイムアウトは数字でなければなりません", "pairwiseMalformedClientRedirectURI":
"ldapErrorReadTimeoutNotNumber": "読み取りタイムアウトは数字でなければなりません", "Client contained an invalid redirect URI.",
"ldapErrorMissingClientId": "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。", "pairwiseClientRedirectURIsMissingHost":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。", "Client redirect URIs must contain a valid host component.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。", "pairwiseClientRedirectURIsMultipleHosts":
"ldapErrorCantWriteOnlyAndReadOnly": "write-onlyとread-onlyを一緒に設定することはできません。", "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"ldapErrorCantEnableStartTlsAndConnectionPooling": "StartTLSと接続プーリングの両方を有効にできません。", "pairwiseMalformedSectorIdentifierURI":
"clientRedirectURIsFragmentError": "リダイレクトURIにURIフラグメントを含めることはできません。", "Malformed Sector Identifier URI.",
"clientRootURLFragmentError": "ルートURLにURLフラグメントを含めることはできません。", "pairwiseFailedToGetRedirectURIs":
"pairwiseMalformedClientRedirectURI": "クライアントに無効なリダイレクトURIが含まれていました。", "Failed to get redirect URIs from the Sector Identifier URI.",
"pairwiseClientRedirectURIsMissingHost": "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。", "pairwiseRedirectURIsMismatch":
"pairwiseClientRedirectURIsMultipleHosts": "設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。", "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。", },
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。", "es": {
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。" "invalidPasswordMinLengthMessage":
}, "Contraseña incorrecta: longitud mínima {0}.",
"lt": { "invalidPasswordMinLowerCaseCharsMessage":
"invalidPasswordMinLengthMessage": "Per trumpas slaptažodis: mažiausias ilgis {0}.", "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.",
"invalidPasswordMinLowerCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.", "invalidPasswordMinDigitsMessage":
"invalidPasswordMinDigitsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.", "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"invalidPasswordMinUpperCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.", "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.",
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).", "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.", "invalidPasswordNotUsernameMessage":
"ldapErrorInvalidCustomFilter": "Sukonfigūruotas LDAP filtras neprasideda \"(\" ir nesibaigia \")\" simboliais.", "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
"ldapErrorMissingClientId": "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.", "invalidPasswordRegexPatternMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.", "Contraseña incorrecta: no cumple la expresión regular.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE", "invalidPasswordHistoryMessage":
"ldapErrorCantWriteOnlyAndReadOnly": "Negalima nustatyti tik rašyti ir tik skaityti kartu", "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
"clientRedirectURIsFragmentError": "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose", },
"clientRootURLFragmentError": "Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese", "fr": {
"pairwiseMalformedClientRedirectURI": "Klientas pateikė neteisingą nukreipimo nuorodą.", "invalidPasswordMinLengthMessage":
"pairwiseClientRedirectURIsMissingHost": "Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.", "Mot de passe invalide : longueur minimale requise de {0}.",
"pairwiseClientRedirectURIsMultipleHosts": "Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.", "invalidPasswordMinLowerCaseCharsMessage":
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.", "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.",
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.", "invalidPasswordMinDigitsMessage":
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI." "Mot de passe invalide : doit contenir au moins {0} chiffre(s).",
}, "invalidPasswordMinUpperCaseCharsMessage":
"nl": { "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.",
"invalidPasswordMinLengthMessage": "Ongeldig wachtwoord: de minimale lengte is {0} karakters.", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.", "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
"invalidPasswordMinDigitsMessage": "Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.", "invalidPasswordNotUsernameMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.", "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
"invalidPasswordMinSpecialCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.", "invalidPasswordRegexPatternMessage":
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.", "Mot de passe invalide : ne valide pas l'expression rationnelle.",
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.", "invalidPasswordHistoryMessage":
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.", "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe.",
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.", },
"ldapErrorInvalidCustomFilter": "LDAP filter met aangepaste configuratie start niet met \"(\" of eindigt niet met \")\".", "it": {},
"ldapErrorConnectionTimeoutNotNumber": "Verbindingstimeout moet een getal zijn", "ja": {
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn", "invalidPasswordMinLengthMessage":
"ldapErrorMissingClientId": "Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.", "無効なパスワード: 最小{0}の長さが必要です。",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Kan groepsovererving niet behouden bij UID-lidmaatschapstype.", "invalidPasswordMinLowerCaseCharsMessage":
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is", "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。",
"ldapErrorCantWriteOnlyAndReadOnly": "Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn", "invalidPasswordMinDigitsMessage":
"clientRedirectURIsFragmentError": "Redirect URIs mogen geen URI fragment bevatten", "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。",
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten", "invalidPasswordMinUpperCaseCharsMessage":
"pairwiseMalformedClientRedirectURI": "Client heeft een ongeldige redirect URI.", "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。",
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs moeten een geldige host-component bevatten.", "invalidPasswordMinSpecialCharsMessage":
"pairwiseClientRedirectURIsMultipleHosts": "Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.", "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。",
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.", "invalidPasswordNotUsernameMessage":
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.", "無効なパスワード: ユーザー名と同じパスワードは禁止されています。",
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI." "invalidPasswordRegexPatternMessage":
}, "無効なパスワード: 正規表現パターンと一致しません。",
"no": { "invalidPasswordHistoryMessage":
"invalidPasswordMinLengthMessage": "Ugyldig passord: minimum lengde {0}.", "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。",
"invalidPasswordMinLowerCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} små bokstaver.", "invalidPasswordBlacklistedMessage":
"invalidPasswordMinDigitsMessage": "Ugyldig passord: må inneholde minst {0} sifre.", "無効なパスワード: パスワードがブラックリストに含まれています。",
"invalidPasswordMinUpperCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} store bokstaver.", "invalidPasswordGenericMessage":
"invalidPasswordMinSpecialCharsMessage": "Ugyldig passord: må inneholde minst {0} spesialtegn.", "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。",
"invalidPasswordNotUsernameMessage": "Ugyldig passord: kan ikke være likt brukernavn.", "ldapErrorInvalidCustomFilter":
"invalidPasswordRegexPatternMessage": "Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.", "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。",
"invalidPasswordHistoryMessage": "Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.", "ldapErrorConnectionTimeoutNotNumber":
"ldapErrorInvalidCustomFilter": "Tilpasset konfigurasjon av LDAP-filter starter ikke med \"(\" eller slutter ikke med \")\".", "接続タイムアウトは数字でなければなりません",
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.", "ldapErrorReadTimeoutNotNumber":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.", "読み取りタイムアウトは数字でなければなりません",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE", "ldapErrorMissingClientId":
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only" "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。",
}, "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"pl": {}, "グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。",
"pt-BR": { "ldapErrorCantWriteOnlyForReadOnlyLdap":
"invalidPasswordMinLengthMessage": "Senha inválida: deve conter ao menos {0} caracteres.", "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。",
"invalidPasswordMinLowerCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres minúsculos.", "ldapErrorCantWriteOnlyAndReadOnly":
"invalidPasswordMinDigitsMessage": "Senha inválida: deve conter ao menos {0} digitos numéricos.", "write-onlyとread-onlyを一緒に設定することはできません。",
"invalidPasswordMinUpperCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres maiúsculos.", "ldapErrorCantEnableStartTlsAndConnectionPooling":
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres especiais.", "StartTLSと接続プーリングの両方を有効にできません。",
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário.", "clientRedirectURIsFragmentError":
"invalidPasswordRegexPatternMessage": "Senha inválida: falha ao passar por padrões.", "リダイレクトURIにURIフラグメントを含めることはできません。",
"invalidPasswordHistoryMessage": "Senha inválida: não deve ser igual às últimas {0} senhas.", "clientRootURLFragmentError":
"ldapErrorInvalidCustomFilter": "Filtro LDAP não inicia com \"(\" ou não termina com \")\".", "ルートURLにURLフラグメントを含めることはできません。",
"ldapErrorMissingClientId": "ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.", "pairwiseMalformedClientRedirectURI":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.", "クライアントに無効なリダイレクトURIが含まれていました。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita", "pairwiseClientRedirectURIsMissingHost":
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo", "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。",
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos", "pairwiseClientRedirectURIsMultipleHosts":
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos" "設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
}, "pairwiseMalformedSectorIdentifierURI":
"ru": { "不正なセレクター識別子URIです。",
"invalidPasswordMinLengthMessage": "Некорректный пароль: длина пароля должна быть не менее {0} символов(а).", "pairwiseFailedToGetRedirectURIs":
"invalidPasswordMinDigitsMessage": "Некорректный пароль: должен содержать не менее {0} цифр(ы).", "セクター識別子URIからリダイレクトURIを取得できませんでした。",
"invalidPasswordMinLowerCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.", "pairwiseRedirectURIsMismatch":
"invalidPasswordMinUpperCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.", "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。",
"invalidPasswordMinSpecialCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).", },
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.", "lt": {
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не прошел проверку по регулярному выражению.", "invalidPasswordMinLengthMessage":
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).", "Per trumpas slaptažodis: mažiausias ilgis {0}.",
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.", "invalidPasswordMinLowerCaseCharsMessage":
"ldapErrorInvalidCustomFilter": "Сконфигурированный пользователем фильтр LDAP не должен начинаться с \"(\" или заканчиваться на \")\".", "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.",
"ldapErrorMissingClientId": "Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.", "invalidPasswordMinDigitsMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Не удалось унаследовать группу и использовать членство UID типа вместе.", "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Невозможно установить режим \"только на запись\", когда LDAP провайдер не в режиме WRITABLE", "invalidPasswordMinUpperCaseCharsMessage":
"ldapErrorCantWriteOnlyAndReadOnly": "Невозможно одновременно установить режимы \"только на чтение\" и \"только на запись\"", "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.",
"clientRedirectURIsFragmentError": "URI перенаправления не должен содержать фрагмент URI", "invalidPasswordMinSpecialCharsMessage":
"clientRootURLFragmentError": "Корневой URL не должен содержать фрагмент URL ", "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
"pairwiseMalformedClientRedirectURI": "Клиент содержит некорректный URI перенаправления.", "invalidPasswordNotUsernameMessage":
"pairwiseClientRedirectURIsMissingHost": "URI перенаправления клиента должен содержать корректный компонент хоста.", "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
"pairwiseClientRedirectURIsMultipleHosts": "Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.", "invalidPasswordRegexPatternMessage":
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.", "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.", "invalidPasswordHistoryMessage":
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI." "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
}, "ldapErrorInvalidCustomFilter":
"zh-CN": { 'Sukonfigūruotas LDAP filtras neprasideda "(" ir nesibaigia ")" simboliais.',
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.", "ldapErrorMissingClientId":
"invalidPasswordMinLowerCaseCharsMessage": "无效的密码:至少包含 {0} 小写字母", "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.",
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字", "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"invalidPasswordMinUpperCaseCharsMessage": "无效的密码:最短长度 {0} 大写字母", "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.",
"invalidPasswordMinSpecialCharsMessage": "无效的密码:最短长度 {0} 特殊字符", "ldapErrorCantWriteOnlyForReadOnlyLdap":
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同", "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE",
"invalidPasswordRegexPatternMessage": "无效的密码: 无法与正则表达式匹配", "ldapErrorCantWriteOnlyAndReadOnly":
"invalidPasswordHistoryMessage": "无效的密码:不能与最后使用的 {0} 个密码相同", "Negalima nustatyti tik rašyti ir tik skaityti kartu",
"ldapErrorInvalidCustomFilter": "定制的 LDAP过滤器不是以 \"(\" 开头或以 \")\"结尾.", "clientRedirectURIsFragmentError":
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout 必须是个数字", "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose",
"ldapErrorMissingClientId": "当域角色映射未启用时,客户端 ID 需要指定。", "clientRootURLFragmentError":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "无法在使用UID成员类型的同时维护组继承属性。", "Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "当LDAP提供方不是可写模式时无法设置只写", "pairwiseMalformedClientRedirectURI":
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写", "Klientas pateikė neteisingą nukreipimo nuorodą.",
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段", "pairwiseClientRedirectURIsMissingHost":
"clientRootURLFragmentError": "根URL 不应包含 URL 片段", "Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.",
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL", "pairwiseClientRedirectURIsMultipleHosts":
"pairwiseClientRedirectURIsMissingHost": "客户端重定向URL需要有一个有效的主机", "Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.", "pairwiseMalformedSectorIdentifierURI":
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.", "Neteisinga sektoriaus identifikatoriaus URI.",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL", "pairwiseFailedToGetRedirectURIs":
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。" "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
} "pairwiseRedirectURIsMismatch":
"Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
},
"nl": {
"invalidPasswordMinLengthMessage":
"Ongeldig wachtwoord: de minimale lengte is {0} karakters.",
"invalidPasswordMinLowerCaseCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.",
"invalidPasswordMinDigitsMessage":
"Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.",
"invalidPasswordMinUpperCaseCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.",
"invalidPasswordMinSpecialCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.",
"invalidPasswordNotUsernameMessage":
"Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
"invalidPasswordRegexPatternMessage":
"Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
"invalidPasswordHistoryMessage":
"Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
"invalidPasswordGenericMessage":
"Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
"ldapErrorInvalidCustomFilter":
'LDAP filter met aangepaste configuratie start niet met "(" of eindigt niet met ")".',
"ldapErrorConnectionTimeoutNotNumber":
"Verbindingstimeout moet een getal zijn",
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn",
"ldapErrorMissingClientId":
"Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Kan groepsovererving niet behouden bij UID-lidmaatschapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is",
"ldapErrorCantWriteOnlyAndReadOnly":
"Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn",
"clientRedirectURIsFragmentError":
"Redirect URIs mogen geen URI fragment bevatten",
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten",
"pairwiseMalformedClientRedirectURI":
"Client heeft een ongeldige redirect URI.",
"pairwiseClientRedirectURIsMissingHost":
"Client redirect URIs moeten een geldige host-component bevatten.",
"pairwiseClientRedirectURIsMultipleHosts":
"Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
"pairwiseMalformedSectorIdentifierURI":
"Onjuist notatie in Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs":
"Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
"pairwiseRedirectURIsMismatch":
"Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI.",
},
"no": {
"invalidPasswordMinLengthMessage":
"Ugyldig passord: minimum lengde {0}.",
"invalidPasswordMinLowerCaseCharsMessage":
"Ugyldig passord: må inneholde minst {0} små bokstaver.",
"invalidPasswordMinDigitsMessage":
"Ugyldig passord: må inneholde minst {0} sifre.",
"invalidPasswordMinUpperCaseCharsMessage":
"Ugyldig passord: må inneholde minst {0} store bokstaver.",
"invalidPasswordMinSpecialCharsMessage":
"Ugyldig passord: må inneholde minst {0} spesialtegn.",
"invalidPasswordNotUsernameMessage":
"Ugyldig passord: kan ikke være likt brukernavn.",
"invalidPasswordRegexPatternMessage":
"Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.",
"invalidPasswordHistoryMessage":
"Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.",
"ldapErrorInvalidCustomFilter":
'Tilpasset konfigurasjon av LDAP-filter starter ikke med "(" eller slutter ikke med ")".',
"ldapErrorMissingClientId":
"KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly":
"Kan ikke sette både write-only og read-only",
},
"pl": {},
"pt-BR": {
"invalidPasswordMinLengthMessage":
"Senha inválida: deve conter ao menos {0} caracteres.",
"invalidPasswordMinLowerCaseCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres minúsculos.",
"invalidPasswordMinDigitsMessage":
"Senha inválida: deve conter ao menos {0} digitos numéricos.",
"invalidPasswordMinUpperCaseCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres maiúsculos.",
"invalidPasswordMinSpecialCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres especiais.",
"invalidPasswordNotUsernameMessage":
"Senha inválida: não deve ser igual ao nome de usuário.",
"invalidPasswordRegexPatternMessage":
"Senha inválida: falha ao passar por padrões.",
"invalidPasswordHistoryMessage":
"Senha inválida: não deve ser igual às últimas {0} senhas.",
"ldapErrorInvalidCustomFilter":
'Filtro LDAP não inicia com "(" ou não termina com ")".',
"ldapErrorMissingClientId":
"ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
"ldapErrorCantWriteOnlyAndReadOnly":
"Não é possível definir somente escrita e somente leitura ao mesmo tempo",
"clientRedirectURIsFragmentError":
"URIs de redirecionamento não podem conter fragmentos",
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos",
},
"ru": {
"invalidPasswordMinLengthMessage":
"Некорректный пароль: длина пароля должна быть не менее {0} символов(а).",
"invalidPasswordMinDigitsMessage":
"Некорректный пароль: должен содержать не менее {0} цифр(ы).",
"invalidPasswordMinLowerCaseCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.",
"invalidPasswordMinUpperCaseCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.",
"invalidPasswordMinSpecialCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).",
"invalidPasswordNotUsernameMessage":
"Некорректный пароль: пароль не должен совпадать с именем пользователя.",
"invalidPasswordRegexPatternMessage":
"Некорректный пароль: пароль не прошел проверку по регулярному выражению.",
"invalidPasswordHistoryMessage":
"Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).",
"invalidPasswordGenericMessage":
"Некорректный пароль: новый пароль не соответствует правилам пароля.",
"ldapErrorInvalidCustomFilter":
'Сконфигурированный пользователем фильтр LDAP не должен начинаться с "(" или заканчиваться на ")".',
"ldapErrorMissingClientId":
"Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Не удалось унаследовать группу и использовать членство UID типа вместе.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
'Невозможно установить режим "только на запись", когда LDAP провайдер не в режиме WRITABLE',
"ldapErrorCantWriteOnlyAndReadOnly":
'Невозможно одновременно установить режимы "только на чтение" и "только на запись"',
"clientRedirectURIsFragmentError":
"URI перенаправления не должен содержать фрагмент URI",
"clientRootURLFragmentError":
"Корневой URL не должен содержать фрагмент URL ",
"pairwiseMalformedClientRedirectURI":
"Клиент содержит некорректный URI перенаправления.",
"pairwiseClientRedirectURIsMissingHost":
"URI перенаправления клиента должен содержать корректный компонент хоста.",
"pairwiseClientRedirectURIsMultipleHosts":
"Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
"pairwiseMalformedSectorIdentifierURI":
"Искаженная часть идентификатора URI.",
"pairwiseFailedToGetRedirectURIs":
"Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
"pairwiseRedirectURIsMismatch":
"Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI.",
},
"zh-CN": {
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.",
"invalidPasswordMinLowerCaseCharsMessage":
"无效的密码:至少包含 {0} 小写字母",
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字",
"invalidPasswordMinUpperCaseCharsMessage":
"无效的密码:最短长度 {0} 大写字母",
"invalidPasswordMinSpecialCharsMessage":
"无效的密码:最短长度 {0} 特殊字符",
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同",
"invalidPasswordRegexPatternMessage":
"无效的密码: 无法与正则表达式匹配",
"invalidPasswordHistoryMessage":
"无效的密码:不能与最后使用的 {0} 个密码相同",
"ldapErrorInvalidCustomFilter":
'定制的 LDAP过滤器不是以 "(" 开头或以 ")"结尾.',
"ldapErrorConnectionTimeoutNotNumber":
"Connection Timeout 必须是个数字",
"ldapErrorMissingClientId":
"当域角色映射未启用时,客户端 ID 需要指定。",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"无法在使用UID成员类型的同时维护组继承属性。",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"当LDAP提供方不是可写模式时无法设置只写",
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写",
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段",
"clientRootURLFragmentError": "根URL 不应包含 URL 片段",
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL",
"pairwiseClientRedirectURIsMissingHost":
"客户端重定向URL需要有一个有效的主机",
"pairwiseClientRedirectURIsMultipleHosts":
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI":
"Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
"pairwiseRedirectURIsMismatch":
"客户端的重定向URI与服务器端获取的URI配置不匹配。",
},
}; };
/* spell-checker: enable */ /* spell-checker: enable */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2,268 +2,474 @@
//PLEASE DO NOT EDIT MANUALLY //PLEASE DO NOT EDIT MANUALLY
/* spell-checker: disable */ /* spell-checker: disable */
export const kcMessages= { export const kcMessages = {
"ca": { "ca": {
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.", "invalidPasswordHistoryMessage":
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.", "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
"invalidPasswordMinLengthMessage": "Contrasenya incorrecta: longitud mínima {0}.", "invalidPasswordMinDigitsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.", "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.", "invalidPasswordMinLengthMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.", "Contrasenya incorrecta: longitud mínima {0}.",
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.", "invalidPasswordMinLowerCaseCharsMessage":
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular." "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.",
}, "invalidPasswordMinSpecialCharsMessage":
"de": { "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
"invalidPasswordMinLengthMessage": "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.", "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
"invalidPasswordMinDigitsMessage": "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.", "invalidPasswordNotUsernameMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.", "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
"invalidPasswordMinSpecialCharsMessage": "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.", "invalidPasswordRegexPatternMessage":
"invalidPasswordNotUsernameMessage": "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.", "Contrasenya incorrecta: no compleix l'expressió regular.",
"invalidPasswordNotEmailMessage": "Ungültiges Passwort: darf nicht identisch mit der E-Mail-Adresse sein.", },
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.", "de": {
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.", "invalidPasswordMinLengthMessage":
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.", "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.",
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht." "invalidPasswordMinLowerCaseCharsMessage":
}, "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.",
"en": { "invalidPasswordMinDigitsMessage":
"invalidPasswordMinLengthMessage": "Invalid password: minimum length {0}.", "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.",
"invalidPasswordMaxLengthMessage": "Invalid password: maximum length {0}.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Invalid password: must contain at least {0} lower case characters.", "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.",
"invalidPasswordMinDigitsMessage": "Invalid password: must contain at least {0} numerical digits.", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordMinUpperCaseCharsMessage": "Invalid password: must contain at least {0} upper case characters.", "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.",
"invalidPasswordMinSpecialCharsMessage": "Invalid password: must contain at least {0} special characters.", "invalidPasswordNotUsernameMessage":
"invalidPasswordNotUsernameMessage": "Invalid password: must not be equal to the username.", "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.",
"invalidPasswordNotEmailMessage": "Invalid password: must not be equal to the email.", "invalidPasswordNotEmailMessage":
"invalidPasswordRegexPatternMessage": "Invalid password: fails to match regex pattern(s).", "Ungültiges Passwort: darf nicht identisch mit der E-Mail-Adresse sein.",
"invalidPasswordHistoryMessage": "Invalid password: must not be equal to any of last {0} passwords.", "invalidPasswordRegexPatternMessage":
"invalidPasswordBlacklistedMessage": "Invalid password: password is blacklisted.", "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
"invalidPasswordGenericMessage": "Invalid password: new password does not match password policies.", "invalidPasswordHistoryMessage":
"ldapErrorInvalidCustomFilter": "Custom configured LDAP filter does not start with \"(\" or does not end with \")\".", "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout must be a number", "invalidPasswordBlacklistedMessage":
"ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number", "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
"ldapErrorMissingClientId": "Client ID needs to be provided in config when Realm Roles Mapping is not used.", "invalidPasswordGenericMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Not possible to preserve group inheritance and use UID membership type together.", "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Can not set write only when LDAP provider mode is not WRITABLE", },
"ldapErrorCantWriteOnlyAndReadOnly": "Can not set write-only and read-only together", "en": {
"ldapErrorCantEnableStartTlsAndConnectionPooling": "Can not enable both StartTLS and connection pooling.", "invalidPasswordMinLengthMessage":
"ldapErrorCantEnableUnsyncedAndImportOff": "Can not disable Importing users when LDAP provider mode is UNSYNCED", "Invalid password: minimum length {0}.",
"ldapErrorMissingGroupsPathGroup": "Groups path group does not exist - please create the group on specified path first", "invalidPasswordMaxLengthMessage":
"clientRedirectURIsFragmentError": "Redirect URIs must not contain an URI fragment", "Invalid password: maximum length {0}.",
"clientRootURLFragmentError": "Root URL must not contain an URL fragment", "invalidPasswordMinLowerCaseCharsMessage":
"clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme", "Invalid password: must contain at least {0} lower case characters.",
"clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme", "invalidPasswordMinDigitsMessage":
"backchannelLogoutUrlIllegalSchemeError": "Backchannel logout URL uses an illegal scheme", "Invalid password: must contain at least {0} numerical digits.",
"clientRedirectURIsIllegalSchemeError": "A redirect URI uses an illegal scheme", "invalidPasswordMinUpperCaseCharsMessage":
"clientBaseURLInvalid": "Base URL is not a valid URL", "Invalid password: must contain at least {0} upper case characters.",
"clientRootURLInvalid": "Root URL is not a valid URL", "invalidPasswordMinSpecialCharsMessage":
"clientRedirectURIsInvalid": "A redirect URI is not a valid URI", "Invalid password: must contain at least {0} special characters.",
"backchannelLogoutUrlIsInvalid": "Backchannel logout URL is not a valid URL", "invalidPasswordNotUsernameMessage":
"pairwiseMalformedClientRedirectURI": "Client contained an invalid redirect URI.", "Invalid password: must not be equal to the username.",
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs must contain a valid host component.", "invalidPasswordNotEmailMessage":
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.", "Invalid password: must not be equal to the email.",
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.", "invalidPasswordRegexPatternMessage":
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.", "Invalid password: fails to match regex pattern(s).",
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.", "invalidPasswordHistoryMessage":
"error-invalid-value": "Invalid value.", "Invalid password: must not be equal to any of last {0} passwords.",
"error-invalid-blank": "Please specify value.", "invalidPasswordBlacklistedMessage":
"error-empty": "Please specify value.", "Invalid password: password is blacklisted.",
"error-invalid-length": "Attribute {0} must have a length between {1} and {2}.", "invalidPasswordGenericMessage":
"error-invalid-length-too-short": "Attribute {0} must have minimal length of {1}.", "Invalid password: new password does not match password policies.",
"error-invalid-length-too-long": "Attribute {0} must have maximal length of {2}.", "ldapErrorInvalidCustomFilter":
"error-invalid-email": "Invalid email address.", 'Custom configured LDAP filter does not start with "(" or does not end with ")".',
"error-invalid-number": "Invalid number.", "ldapErrorConnectionTimeoutNotNumber":
"error-number-out-of-range": "Attribute {0} must be a number between {1} and {2}.", "Connection Timeout must be a number",
"error-number-out-of-range-too-small": "Attribute {0} must have minimal value of {1}.", "ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number",
"error-number-out-of-range-too-big": "Attribute {0} must have maximal value of {2}.", "ldapErrorMissingClientId":
"error-pattern-no-match": "Invalid value.", "Client ID needs to be provided in config when Realm Roles Mapping is not used.",
"error-invalid-uri": "Invalid URL.", "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"error-invalid-uri-scheme": "Invalid URL scheme.", "Not possible to preserve group inheritance and use UID membership type together.",
"error-invalid-uri-fragment": "Invalid URL fragment.", "ldapErrorCantWriteOnlyForReadOnlyLdap":
"error-user-attribute-required": "Please specify attribute {0}.", "Can not set write only when LDAP provider mode is not WRITABLE",
"error-invalid-date": "Attribute {0} is invalid date.", "ldapErrorCantWriteOnlyAndReadOnly":
"error-user-attribute-read-only": "Attribute {0} is read only.", "Can not set write-only and read-only together",
"error-username-invalid-character": "{0} contains invalid character.", "ldapErrorCantEnableStartTlsAndConnectionPooling":
"error-person-name-invalid-character": "{0} contains invalid character." "Can not enable both StartTLS and connection pooling.",
}, "ldapErrorCantEnableUnsyncedAndImportOff":
"es": { "Can not disable Importing users when LDAP provider mode is UNSYNCED",
"invalidPasswordMinLengthMessage": "Contraseña incorrecta: longitud mínima {0}.", "ldapErrorMissingGroupsPathGroup":
"invalidPasswordMinLowerCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.", "Groups path group does not exist - please create the group on specified path first",
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.", "clientRedirectURIsFragmentError":
"invalidPasswordMinUpperCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.", "Redirect URIs must not contain an URI fragment",
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.", "clientRootURLFragmentError":
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.", "Root URL must not contain an URL fragment",
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.", "clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas." "clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme",
}, "backchannelLogoutUrlIllegalSchemeError":
"fr": { "Backchannel logout URL uses an illegal scheme",
"invalidPasswordMinLengthMessage": "Mot de passe invalide : longueur minimale requise de {0}.", "clientRedirectURIsIllegalSchemeError":
"invalidPasswordMinLowerCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.", "A redirect URI uses an illegal scheme",
"invalidPasswordMinDigitsMessage": "Mot de passe invalide : doit contenir au moins {0} chiffre(s).", "clientBaseURLInvalid": "Base URL is not a valid URL",
"invalidPasswordMinUpperCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.", "clientRootURLInvalid": "Root URL is not a valid URL",
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.", "clientRedirectURIsInvalid": "A redirect URI is not a valid URI",
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.", "backchannelLogoutUrlIsInvalid":
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.", "Backchannel logout URL is not a valid URL",
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe." "pairwiseMalformedClientRedirectURI":
}, "Client contained an invalid redirect URI.",
"it": {}, "pairwiseClientRedirectURIsMissingHost":
"ja": { "Client redirect URIs must contain a valid host component.",
"invalidPasswordMinLengthMessage": "無効なパスワード: 最小{0}の長さが必要です。", "pairwiseClientRedirectURIsMultipleHosts":
"invalidPasswordMinLowerCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。", "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"invalidPasswordMinDigitsMessage": "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。", "pairwiseMalformedSectorIdentifierURI":
"invalidPasswordMinUpperCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。", "Malformed Sector Identifier URI.",
"invalidPasswordMinSpecialCharsMessage": "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。", "pairwiseFailedToGetRedirectURIs":
"invalidPasswordNotUsernameMessage": "無効なパスワード: ユーザー名と同じパスワードは禁止されています。", "Failed to get redirect URIs from the Sector Identifier URI.",
"invalidPasswordRegexPatternMessage": "無効なパスワード: 正規表現パターンと一致しません。", "pairwiseRedirectURIsMismatch":
"invalidPasswordHistoryMessage": "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。", "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
"invalidPasswordBlacklistedMessage": "無効なパスワード: パスワードがブラックリストに含まれています。", "error-invalid-value": "Invalid value.",
"invalidPasswordGenericMessage": "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。", "error-invalid-blank": "Please specify value.",
"ldapErrorInvalidCustomFilter": "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。", "error-empty": "Please specify value.",
"ldapErrorConnectionTimeoutNotNumber": "接続タイムアウトは数字でなければなりません", "error-invalid-length":
"ldapErrorReadTimeoutNotNumber": "読み取りタイムアウトは数字でなければなりません", "Attribute {0} must have a length between {1} and {2}.",
"ldapErrorMissingClientId": "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。", "error-invalid-length-too-short":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。", "Attribute {0} must have minimal length of {1}.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。", "error-invalid-length-too-long":
"ldapErrorCantWriteOnlyAndReadOnly": "write-onlyとread-onlyを一緒に設定することはできません。", "Attribute {0} must have maximal length of {2}.",
"ldapErrorCantEnableStartTlsAndConnectionPooling": "StartTLSと接続プーリングの両方を有効にできません。", "error-invalid-email": "Invalid email address.",
"clientRedirectURIsFragmentError": "リダイレクトURIにURIフラグメントを含めることはできません。", "error-invalid-number": "Invalid number.",
"clientRootURLFragmentError": "ルートURLにURLフラグメントを含めることはできません。", "error-number-out-of-range":
"pairwiseMalformedClientRedirectURI": "クライアントに無効なリダイレクトURIが含まれていました。", "Attribute {0} must be a number between {1} and {2}.",
"pairwiseClientRedirectURIsMissingHost": "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。", "error-number-out-of-range-too-small":
"pairwiseClientRedirectURIsMultipleHosts": "設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。", "Attribute {0} must have minimal value of {1}.",
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。", "error-number-out-of-range-too-big":
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。", "Attribute {0} must have maximal value of {2}.",
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。" "error-pattern-no-match": "Invalid value.",
}, "error-invalid-uri": "Invalid URL.",
"lt": { "error-invalid-uri-scheme": "Invalid URL scheme.",
"invalidPasswordMinLengthMessage": "Per trumpas slaptažodis: mažiausias ilgis {0}.", "error-invalid-uri-fragment": "Invalid URL fragment.",
"invalidPasswordMinLowerCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.", "error-user-attribute-required": "Please specify attribute {0}.",
"invalidPasswordMinDigitsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.", "error-invalid-date": "Attribute {0} is invalid date.",
"invalidPasswordMinUpperCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.", "error-user-attribute-read-only": "Attribute {0} is read only.",
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.", "error-username-invalid-character": "{0} contains invalid character.",
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.", "error-person-name-invalid-character":
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).", "{0} contains invalid character.",
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.", },
"ldapErrorInvalidCustomFilter": "Sukonfigūruotas LDAP filtras neprasideda \"(\" ir nesibaigia \")\" simboliais.", "es": {
"ldapErrorMissingClientId": "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.", "invalidPasswordMinLengthMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.", "Contraseña incorrecta: longitud mínima {0}.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE", "invalidPasswordMinLowerCaseCharsMessage":
"ldapErrorCantWriteOnlyAndReadOnly": "Negalima nustatyti tik rašyti ir tik skaityti kartu", "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.",
"clientRedirectURIsFragmentError": "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose", "invalidPasswordMinDigitsMessage":
"clientRootURLFragmentError": "Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese", "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
"pairwiseMalformedClientRedirectURI": "Klientas pateikė neteisingą nukreipimo nuorodą.", "invalidPasswordMinUpperCaseCharsMessage":
"pairwiseClientRedirectURIsMissingHost": "Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.", "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.",
"pairwiseClientRedirectURIsMultipleHosts": "Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.", "invalidPasswordMinSpecialCharsMessage":
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.", "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.", "invalidPasswordNotUsernameMessage":
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI." "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
}, "invalidPasswordRegexPatternMessage":
"nl": { "Contraseña incorrecta: no cumple la expresión regular.",
"invalidPasswordMinLengthMessage": "Ongeldig wachtwoord: de minimale lengte is {0} karakters.", "invalidPasswordHistoryMessage":
"invalidPasswordMinLowerCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.", "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
"invalidPasswordMinDigitsMessage": "Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.", },
"invalidPasswordMinUpperCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.", "fr": {
"invalidPasswordMinSpecialCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.", "invalidPasswordMinLengthMessage":
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.", "Mot de passe invalide : longueur minimale requise de {0}.",
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.", "invalidPasswordMinLowerCaseCharsMessage":
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.", "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.",
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.", "invalidPasswordMinDigitsMessage":
"ldapErrorInvalidCustomFilter": "LDAP filter met aangepaste configuratie start niet met \"(\" of eindigt niet met \")\".", "Mot de passe invalide : doit contenir au moins {0} chiffre(s).",
"ldapErrorConnectionTimeoutNotNumber": "Verbindingstimeout moet een getal zijn", "invalidPasswordMinUpperCaseCharsMessage":
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn", "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.",
"ldapErrorMissingClientId": "Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.", "invalidPasswordMinSpecialCharsMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Kan groepsovererving niet behouden bij UID-lidmaatschapstype.", "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is", "invalidPasswordNotUsernameMessage":
"ldapErrorCantWriteOnlyAndReadOnly": "Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn", "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
"clientRedirectURIsFragmentError": "Redirect URIs mogen geen URI fragment bevatten", "invalidPasswordRegexPatternMessage":
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten", "Mot de passe invalide : ne valide pas l'expression rationnelle.",
"pairwiseMalformedClientRedirectURI": "Client heeft een ongeldige redirect URI.", "invalidPasswordHistoryMessage":
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs moeten een geldige host-component bevatten.", "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe.",
"pairwiseClientRedirectURIsMultipleHosts": "Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.", },
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.", "it": {},
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.", "ja": {
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI." "invalidPasswordMinLengthMessage":
}, "無効なパスワード: 最小{0}の長さが必要です。",
"no": { "invalidPasswordMinLowerCaseCharsMessage":
"invalidPasswordMinLengthMessage": "Ugyldig passord: minimum lengde {0}.", "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。",
"invalidPasswordMinLowerCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} små bokstaver.", "invalidPasswordMinDigitsMessage":
"invalidPasswordMinDigitsMessage": "Ugyldig passord: må inneholde minst {0} sifre.", "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。",
"invalidPasswordMinUpperCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} store bokstaver.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinSpecialCharsMessage": "Ugyldig passord: må inneholde minst {0} spesialtegn.", "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。",
"invalidPasswordNotUsernameMessage": "Ugyldig passord: kan ikke være likt brukernavn.", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordRegexPatternMessage": "Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.", "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。",
"invalidPasswordHistoryMessage": "Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.", "invalidPasswordNotUsernameMessage":
"ldapErrorInvalidCustomFilter": "Tilpasset konfigurasjon av LDAP-filter starter ikke med \"(\" eller slutter ikke med \")\".", "無効なパスワード: ユーザー名と同じパスワードは禁止されています。",
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.", "invalidPasswordRegexPatternMessage":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.", "無効なパスワード: 正規表現パターンと一致しません。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE", "invalidPasswordHistoryMessage":
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only" "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。",
}, "invalidPasswordBlacklistedMessage":
"pl": {}, "無効なパスワード: パスワードがブラックリストに含まれています。",
"pt-BR": { "invalidPasswordGenericMessage":
"invalidPasswordMinLengthMessage": "Senha inválida: deve conter ao menos {0} caracteres.", "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。",
"invalidPasswordMinLowerCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres minúsculos.", "ldapErrorInvalidCustomFilter":
"invalidPasswordMinDigitsMessage": "Senha inválida: deve conter ao menos {0} digitos numéricos.", "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。",
"invalidPasswordMinUpperCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres maiúsculos.", "ldapErrorConnectionTimeoutNotNumber":
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres especiais.", "接続タイムアウトは数字でなければなりません",
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário.", "ldapErrorReadTimeoutNotNumber":
"invalidPasswordRegexPatternMessage": "Senha inválida: falha ao passar por padrões.", "読み取りタイムアウトは数字でなければなりません",
"invalidPasswordHistoryMessage": "Senha inválida: não deve ser igual às últimas {0} senhas.", "ldapErrorMissingClientId":
"ldapErrorInvalidCustomFilter": "Filtro LDAP não inicia com \"(\" ou não termina com \")\".", "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。",
"ldapErrorMissingClientId": "ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.", "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.", "グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita", "ldapErrorCantWriteOnlyForReadOnlyLdap":
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo", "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。",
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos", "ldapErrorCantWriteOnlyAndReadOnly":
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos" "write-onlyとread-onlyを一緒に設定することはできません。",
}, "ldapErrorCantEnableStartTlsAndConnectionPooling":
"ru": { "StartTLSと接続プーリングの両方を有効にできません。",
"invalidPasswordMinLengthMessage": "Некорректный пароль: длина пароля должна быть не менее {0} символов(а).", "clientRedirectURIsFragmentError":
"invalidPasswordMinDigitsMessage": "Некорректный пароль: должен содержать не менее {0} цифр(ы).", "リダイレクトURIにURIフラグメントを含めることはできません。",
"invalidPasswordMinLowerCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.", "clientRootURLFragmentError":
"invalidPasswordMinUpperCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.", "ルートURLにURLフラグメントを含めることはできません。",
"invalidPasswordMinSpecialCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).", "pairwiseMalformedClientRedirectURI":
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.", "クライアントに無効なリダイレクトURIが含まれていました。",
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не прошел проверку по регулярному выражению.", "pairwiseClientRedirectURIsMissingHost":
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).", "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。",
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.", "pairwiseClientRedirectURIsMultipleHosts":
"ldapErrorInvalidCustomFilter": "Сконфигурированный пользователем фильтр LDAP не должен начинаться с \"(\" или заканчиваться на \")\".", "設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
"ldapErrorMissingClientId": "Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.", "pairwiseMalformedSectorIdentifierURI":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Не удалось унаследовать группу и использовать членство UID типа вместе.", "不正なセレクター識別子URIです。",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Невозможно установить режим \"только на запись\", когда LDAP провайдер не в режиме WRITABLE", "pairwiseFailedToGetRedirectURIs":
"ldapErrorCantWriteOnlyAndReadOnly": "Невозможно одновременно установить режимы \"только на чтение\" и \"только на запись\"", "セクター識別子URIからリダイレクトURIを取得できませんでした。",
"clientRedirectURIsFragmentError": "URI перенаправления не должен содержать фрагмент URI", "pairwiseRedirectURIsMismatch":
"clientRootURLFragmentError": "Корневой URL не должен содержать фрагмент URL ", "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。",
"pairwiseMalformedClientRedirectURI": "Клиент содержит некорректный URI перенаправления.", },
"pairwiseClientRedirectURIsMissingHost": "URI перенаправления клиента должен содержать корректный компонент хоста.", "lt": {
"pairwiseClientRedirectURIsMultipleHosts": "Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.", "invalidPasswordMinLengthMessage":
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.", "Per trumpas slaptažodis: mažiausias ilgis {0}.",
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.", "invalidPasswordMinLowerCaseCharsMessage":
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI." "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.",
}, "invalidPasswordMinDigitsMessage":
"zh-CN": { "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.",
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.", "invalidPasswordMinUpperCaseCharsMessage":
"invalidPasswordMinLowerCaseCharsMessage": "无效的密码:至少包含 {0} 小写字母", "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.",
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字", "invalidPasswordMinSpecialCharsMessage":
"invalidPasswordMinUpperCaseCharsMessage": "无效的密码:最短长度 {0} 大写字母", "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
"invalidPasswordMinSpecialCharsMessage": "无效的密码:最短长度 {0} 特殊字符", "invalidPasswordNotUsernameMessage":
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同", "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
"invalidPasswordRegexPatternMessage": "无效的密码: 无法与正则表达式匹配", "invalidPasswordRegexPatternMessage":
"invalidPasswordHistoryMessage": "无效的密码:不能与最后使用的 {0} 个密码相同", "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
"ldapErrorInvalidCustomFilter": "定制的 LDAP过滤器不是以 \"(\" 开头或以 \")\"结尾.", "invalidPasswordHistoryMessage":
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout 必须是个数字", "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
"ldapErrorMissingClientId": "当域角色映射未启用时,客户端 ID 需要指定。", "ldapErrorInvalidCustomFilter":
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "无法在使用UID成员类型的同时维护组继承属性。", 'Sukonfigūruotas LDAP filtras neprasideda "(" ir nesibaigia ")" simboliais.',
"ldapErrorCantWriteOnlyForReadOnlyLdap": "当LDAP提供方不是可写模式时无法设置只写", "ldapErrorMissingClientId":
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写", "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.",
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段", "ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"clientRootURLFragmentError": "根URL 不应包含 URL 片段", "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.",
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL", "ldapErrorCantWriteOnlyForReadOnlyLdap":
"pairwiseClientRedirectURIsMissingHost": "客户端重定向URL需要有一个有效的主机", "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE",
"pairwiseClientRedirectURIsMultipleHosts": "Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.", "ldapErrorCantWriteOnlyAndReadOnly":
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.", "Negalima nustatyti tik rašyti ir tik skaityti kartu",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL", "clientRedirectURIsFragmentError":
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。" "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose",
} "clientRootURLFragmentError":
"Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese",
"pairwiseMalformedClientRedirectURI":
"Klientas pateikė neteisingą nukreipimo nuorodą.",
"pairwiseClientRedirectURIsMissingHost":
"Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.",
"pairwiseClientRedirectURIsMultipleHosts":
"Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
"pairwiseMalformedSectorIdentifierURI":
"Neteisinga sektoriaus identifikatoriaus URI.",
"pairwiseFailedToGetRedirectURIs":
"Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
"pairwiseRedirectURIsMismatch":
"Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
},
"nl": {
"invalidPasswordMinLengthMessage":
"Ongeldig wachtwoord: de minimale lengte is {0} karakters.",
"invalidPasswordMinLowerCaseCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.",
"invalidPasswordMinDigitsMessage":
"Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.",
"invalidPasswordMinUpperCaseCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.",
"invalidPasswordMinSpecialCharsMessage":
"Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.",
"invalidPasswordNotUsernameMessage":
"Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
"invalidPasswordRegexPatternMessage":
"Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
"invalidPasswordHistoryMessage":
"Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
"invalidPasswordGenericMessage":
"Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
"ldapErrorInvalidCustomFilter":
'LDAP filter met aangepaste configuratie start niet met "(" of eindigt niet met ")".',
"ldapErrorConnectionTimeoutNotNumber":
"Verbindingstimeout moet een getal zijn",
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn",
"ldapErrorMissingClientId":
"Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Kan groepsovererving niet behouden bij UID-lidmaatschapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is",
"ldapErrorCantWriteOnlyAndReadOnly":
"Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn",
"clientRedirectURIsFragmentError":
"Redirect URIs mogen geen URI fragment bevatten",
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten",
"pairwiseMalformedClientRedirectURI":
"Client heeft een ongeldige redirect URI.",
"pairwiseClientRedirectURIsMissingHost":
"Client redirect URIs moeten een geldige host-component bevatten.",
"pairwiseClientRedirectURIsMultipleHosts":
"Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
"pairwiseMalformedSectorIdentifierURI":
"Onjuist notatie in Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs":
"Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
"pairwiseRedirectURIsMismatch":
"Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI.",
},
"no": {
"invalidPasswordMinLengthMessage":
"Ugyldig passord: minimum lengde {0}.",
"invalidPasswordMinLowerCaseCharsMessage":
"Ugyldig passord: må inneholde minst {0} små bokstaver.",
"invalidPasswordMinDigitsMessage":
"Ugyldig passord: må inneholde minst {0} sifre.",
"invalidPasswordMinUpperCaseCharsMessage":
"Ugyldig passord: må inneholde minst {0} store bokstaver.",
"invalidPasswordMinSpecialCharsMessage":
"Ugyldig passord: må inneholde minst {0} spesialtegn.",
"invalidPasswordNotUsernameMessage":
"Ugyldig passord: kan ikke være likt brukernavn.",
"invalidPasswordRegexPatternMessage":
"Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.",
"invalidPasswordHistoryMessage":
"Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.",
"ldapErrorInvalidCustomFilter":
'Tilpasset konfigurasjon av LDAP-filter starter ikke med "(" eller slutter ikke med ")".',
"ldapErrorMissingClientId":
"KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly":
"Kan ikke sette både write-only og read-only",
},
"pl": {},
"pt-BR": {
"invalidPasswordMinLengthMessage":
"Senha inválida: deve conter ao menos {0} caracteres.",
"invalidPasswordMinLowerCaseCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres minúsculos.",
"invalidPasswordMinDigitsMessage":
"Senha inválida: deve conter ao menos {0} digitos numéricos.",
"invalidPasswordMinUpperCaseCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres maiúsculos.",
"invalidPasswordMinSpecialCharsMessage":
"Senha inválida: deve conter ao menos {0} caracteres especiais.",
"invalidPasswordNotUsernameMessage":
"Senha inválida: não deve ser igual ao nome de usuário.",
"invalidPasswordRegexPatternMessage":
"Senha inválida: falha ao passar por padrões.",
"invalidPasswordHistoryMessage":
"Senha inválida: não deve ser igual às últimas {0} senhas.",
"ldapErrorInvalidCustomFilter":
'Filtro LDAP não inicia com "(" ou não termina com ")".',
"ldapErrorMissingClientId":
"ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
"ldapErrorCantWriteOnlyAndReadOnly":
"Não é possível definir somente escrita e somente leitura ao mesmo tempo",
"clientRedirectURIsFragmentError":
"URIs de redirecionamento não podem conter fragmentos",
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos",
},
"ru": {
"invalidPasswordMinLengthMessage":
"Некорректный пароль: длина пароля должна быть не менее {0} символов(а).",
"invalidPasswordMinDigitsMessage":
"Некорректный пароль: должен содержать не менее {0} цифр(ы).",
"invalidPasswordMinLowerCaseCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.",
"invalidPasswordMinUpperCaseCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.",
"invalidPasswordMinSpecialCharsMessage":
"Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).",
"invalidPasswordNotUsernameMessage":
"Некорректный пароль: пароль не должен совпадать с именем пользователя.",
"invalidPasswordRegexPatternMessage":
"Некорректный пароль: пароль не прошел проверку по регулярному выражению.",
"invalidPasswordHistoryMessage":
"Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).",
"invalidPasswordGenericMessage":
"Некорректный пароль: новый пароль не соответствует правилам пароля.",
"ldapErrorInvalidCustomFilter":
'Сконфигурированный пользователем фильтр LDAP не должен начинаться с "(" или заканчиваться на ")".',
"ldapErrorMissingClientId":
"Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"Не удалось унаследовать группу и использовать членство UID типа вместе.",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
'Невозможно установить режим "только на запись", когда LDAP провайдер не в режиме WRITABLE',
"ldapErrorCantWriteOnlyAndReadOnly":
'Невозможно одновременно установить режимы "только на чтение" и "только на запись"',
"clientRedirectURIsFragmentError":
"URI перенаправления не должен содержать фрагмент URI",
"clientRootURLFragmentError":
"Корневой URL не должен содержать фрагмент URL ",
"pairwiseMalformedClientRedirectURI":
"Клиент содержит некорректный URI перенаправления.",
"pairwiseClientRedirectURIsMissingHost":
"URI перенаправления клиента должен содержать корректный компонент хоста.",
"pairwiseClientRedirectURIsMultipleHosts":
"Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
"pairwiseMalformedSectorIdentifierURI":
"Искаженная часть идентификатора URI.",
"pairwiseFailedToGetRedirectURIs":
"Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
"pairwiseRedirectURIsMismatch":
"Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI.",
},
"zh-CN": {
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.",
"invalidPasswordMinLowerCaseCharsMessage":
"无效的密码:至少包含 {0} 小写字母",
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字",
"invalidPasswordMinUpperCaseCharsMessage":
"无效的密码:最短长度 {0} 大写字母",
"invalidPasswordMinSpecialCharsMessage":
"无效的密码:最短长度 {0} 特殊字符",
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同",
"invalidPasswordRegexPatternMessage":
"无效的密码: 无法与正则表达式匹配",
"invalidPasswordHistoryMessage":
"无效的密码:不能与最后使用的 {0} 个密码相同",
"ldapErrorInvalidCustomFilter":
'定制的 LDAP过滤器不是以 "(" 开头或以 ")"结尾.',
"ldapErrorConnectionTimeoutNotNumber":
"Connection Timeout 必须是个数字",
"ldapErrorMissingClientId":
"当域角色映射未启用时,客户端 ID 需要指定。",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
"无法在使用UID成员类型的同时维护组继承属性。",
"ldapErrorCantWriteOnlyForReadOnlyLdap":
"当LDAP提供方不是可写模式时无法设置只写",
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写",
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段",
"clientRootURLFragmentError": "根URL 不应包含 URL 片段",
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL",
"pairwiseClientRedirectURIsMissingHost":
"客户端重定向URL需要有一个有效的主机",
"pairwiseClientRedirectURIsMultipleHosts":
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI":
"Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
"pairwiseRedirectURIsMismatch":
"客户端的重定向URI与服务器端获取的URI配置不匹配。",
},
}; };
/* spell-checker: enable */ /* spell-checker: enable */

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,3 @@
import { kcMessages } from "../generated_kcMessages/15.0.2/login"; import { kcMessages } from "../generated_kcMessages/15.0.2/login";
import { Evt } from "evt"; import { Evt } from "evt";
import { objectKeys } from "tsafe/objectKeys"; import { objectKeys } from "tsafe/objectKeys";
@ -11,8 +10,8 @@ export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
kcMessages[kcLanguage], kcMessages[kcLanguage],
key, key,
(() => { (() => {
let value =
let value = key === "termsText" ? "⏳" : kcMessages[kcLanguage][key]; key === "termsText" ? "⏳" : kcMessages[kcLanguage][key];
return { return {
"enumerable": true, "enumerable": true,
@ -20,14 +19,11 @@ export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
"set": (newValue: string) => { "set": (newValue: string) => {
value = newValue; value = newValue;
Evt.asPostable(evtTermsUpdated).post(); Evt.asPostable(evtTermsUpdated).post();
} },
}; };
})(),
),
})() ),
)
)
); );
export { kcMessages }; export { kcMessages };

View File

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

View File

@ -1,4 +1,3 @@
import { useCallback, useReducer } from "react"; import { useCallback, useReducer } from "react";
import { useKcLanguageTag } from "./useKcLanguageTag"; import { useKcLanguageTag } from "./useKcLanguageTag";
import { kcMessages, evtTermsUpdated } from "./kcMessages/login"; import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
@ -9,7 +8,7 @@ import { id } from "tsafe/id";
export type MessageKey = keyof typeof kcMessages["en"]; export type MessageKey = keyof typeof kcMessages["en"];
/** /**
* When the language is switched the page is reloaded, this may appear * 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 * as a bug as you might notice that the language successfully switch before
* reload. * reload.
@ -17,60 +16,67 @@ export type MessageKey = keyof typeof kcMessages["en"];
* during login so we can retrieve the "local" field of the JWT encoded accessToken. * during login so we can retrieve the "local" field of the JWT encoded accessToken.
*/ */
export function useKcMessage() { export function useKcMessage() {
const { kcLanguageTag } = useKcLanguageTag(); const { kcLanguageTag } = useKcLanguageTag();
const [trigger, forceUpdate] = useReducer((counter: number) => counter + 1, 0); const [trigger, forceUpdate] = useReducer(
(counter: number) => counter + 1,
0,
);
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []); useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
const msgStr = useCallback( const msgStr = useCallback(
(key: MessageKey, ...args: (string | undefined)[]): string => { (key: MessageKey, ...args: (string | undefined)[]): string => {
let str: string =
let str: string = kcMessages[kcLanguageTag as any as "en"][key] ?? kcMessages["en"][key]; kcMessages[kcLanguageTag as any as "en"][key] ??
kcMessages["en"][key];
args.forEach((arg, i) => { args.forEach((arg, i) => {
if (arg === undefined) { if (arg === undefined) {
return; return;
} }
str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg); str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
}); });
return str; return str;
}, },
[kcLanguageTag, trigger] [kcLanguageTag, trigger],
); );
const msg = useCallback<(...args: Parameters<typeof msgStr>) => JSX.Element>( const msg = useCallback<
(key, ...args) => (...args: Parameters<typeof msgStr>) => JSX.Element
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}> >(
(key, ...args) => (
<ReactMarkdown
allowDangerousHtml
renderers={
key === "termsText" ? undefined : { "paragraph": "span" }
}
>
{msgStr(key, ...args)} {msgStr(key, ...args)}
</ReactMarkdown>, </ReactMarkdown>
[msgStr] ),
[msgStr],
); );
const advancedMsg = useCallback( const advancedMsg = useCallback(
(key: string): string | undefined => { (key: string): string | undefined => {
const match = key.match(/^\$\{([^{]+)\}$/); const match = key.match(/^\$\{([^{]+)\}$/);
if( match === null ){ if (match === null) {
return key; return key;
} }
return ( return (
id<Record<string, string | undefined>>(kcMessages[kcLanguageTag])[key] ?? id<Record<string, string | undefined>>(
kcMessages[kcLanguageTag],
)[key] ??
id<Record<string, string | undefined>>(kcMessages["en"])[key] id<Record<string, string | undefined>>(kcMessages["en"])[key]
); );
}, },
[msgStr] [msgStr],
); );
return { msg, msgStr, advancedMsg }; return { msg, msgStr, advancedMsg };
}
}

View File

@ -16,4 +16,3 @@ export * from "./components/LoginVerifyEmail";
export * from "./keycloakJsAdapter"; export * from "./keycloakJsAdapter";
export * from "./tools/assert"; export * from "./tools/assert";

View File

@ -1,18 +1,22 @@
export declare namespace keycloak_js { export declare namespace keycloak_js {
export type KeycloakPromiseCallback<T> = (result: T) => void; export type KeycloakPromiseCallback<T> = (result: T) => void;
export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> { export class KeycloakPromise<TSuccess, TError> extends Promise<TSuccess> {
success(callback: KeycloakPromiseCallback<TSuccess>): KeycloakPromise<TSuccess, TError>; success(
error(callback: KeycloakPromiseCallback<TError>): KeycloakPromise<TSuccess, TError>; callback: KeycloakPromiseCallback<TSuccess>,
): KeycloakPromise<TSuccess, TError>;
error(
callback: KeycloakPromiseCallback<TError>,
): KeycloakPromise<TSuccess, TError>;
} }
export interface KeycloakAdapter { export interface KeycloakAdapter {
login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>; login(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>; logout(options?: KeycloakLogoutOptions): KeycloakPromise<void, void>;
register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>; register(options?: KeycloakLoginOptions): KeycloakPromise<void, void>;
accountManagement(): KeycloakPromise<void, void>; accountManagement(): KeycloakPromise<void, void>;
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string; redirectUri(
options: { redirectUri: string },
encodeHash: boolean,
): string;
} }
export interface KeycloakLogoutOptions { export interface KeycloakLogoutOptions {
redirectUri?: string; redirectUri?: string;
@ -20,7 +24,7 @@ export declare namespace keycloak_js {
export interface KeycloakLoginOptions { export interface KeycloakLoginOptions {
scope?: string; scope?: string;
redirectUri?: string; redirectUri?: string;
prompt?: 'none' | 'login'; prompt?: "none" | "login";
action?: string; action?: string;
maxAge?: number; maxAge?: number;
loginHint?: string; loginHint?: string;
@ -30,73 +34,59 @@ export declare namespace keycloak_js {
} }
export type KeycloakInstance = Record< export type KeycloakInstance = Record<
"createLoginUrl" | "createLoginUrl" | "createLogoutUrl" | "createRegisterUrl",
"createLogoutUrl" |
"createRegisterUrl",
(options: KeycloakLoginOptions | undefined) => string (options: KeycloakLoginOptions | undefined) => string
> & { > & {
createAccountUrl(): string; createAccountUrl(): string;
redirectUri?: string; redirectUri?: string;
} };
} }
/** /**
* NOTE: This is just a slightly modified version of the default adapter in keycloak-js * NOTE: This is just a slightly modified version of the default adapter in keycloak-js
* The goal here is just to be able to inject search param in url before keycloak redirect. * The goal here is just to be able to inject search param in url before keycloak redirect.
* Our use case for it is to pass over the login screen the states of useGlobalState * Our use case for it is to pass over the login screen the states of useGlobalState
* namely isDarkModeEnabled, lgn... * namely isDarkModeEnabled, lgn...
*/ */
export function createKeycloakAdapter( export function createKeycloakAdapter(params: {
params: { keycloakInstance: keycloak_js.KeycloakInstance;
keycloakInstance: keycloak_js.KeycloakInstance; transformUrlBeforeRedirect(url: string): string;
transformUrlBeforeRedirect(url: string): string; }): keycloak_js.KeycloakAdapter {
}
): keycloak_js.KeycloakAdapter {
const { keycloakInstance, transformUrlBeforeRedirect } = params; const { keycloakInstance, transformUrlBeforeRedirect } = params;
const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> = Object.defineProperties( const neverResolvingPromise: keycloak_js.KeycloakPromise<void, void> =
new Promise(() => { }), Object.defineProperties(new Promise(() => {}), {
{ "success": { "value": () => {} },
"success": { "value": () => { } }, "error": { "value": () => {} },
"error": { "value": () => { } } }) as any;
}
) as any;
return { return {
"login": options => { "login": options => {
window.location.href= window.location.href = transformUrlBeforeRedirect(
transformUrlBeforeRedirect( keycloakInstance.createLoginUrl(options),
keycloakInstance.createLoginUrl( );
options
)
);
return neverResolvingPromise; return neverResolvingPromise;
}, },
"logout": options => { "logout": options => {
window.location.replace( window.location.replace(
transformUrlBeforeRedirect( transformUrlBeforeRedirect(
keycloakInstance.createLogoutUrl( keycloakInstance.createLogoutUrl(options),
options ),
)
)
); );
return neverResolvingPromise; return neverResolvingPromise;
}, },
"register": options => { "register": options => {
window.location.href = window.location.href = transformUrlBeforeRedirect(
transformUrlBeforeRedirect( keycloakInstance.createRegisterUrl(options),
keycloakInstance.createRegisterUrl( );
options
)
);
return neverResolvingPromise; return neverResolvingPromise;
}, },
"accountManagement": () => { "accountManagement": () => {
var accountUrl = transformUrlBeforeRedirect(keycloakInstance.createAccountUrl()); var accountUrl = transformUrlBeforeRedirect(
if (typeof accountUrl !== 'undefined') { keycloakInstance.createAccountUrl(),
);
if (typeof accountUrl !== "undefined") {
window.location.href = accountUrl; window.location.href = accountUrl;
} else { } else {
throw new Error("Not supported by the OIDC server"); throw new Error("Not supported by the OIDC server");
@ -111,8 +101,6 @@ export function createKeycloakAdapter(
} else { } else {
return window.location.href; return window.location.href;
} }
} },
}; };
}
}

View File

@ -1,35 +1,27 @@
export type AndByDiscriminatingKey< export type AndByDiscriminatingKey<
DiscriminatingKey extends string, DiscriminatingKey extends string,
U1 extends Record<DiscriminatingKey, string>, U1 extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string> U2 extends Record<DiscriminatingKey, string>,
> = > = AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
export declare namespace AndByDiscriminatingKey { export declare namespace AndByDiscriminatingKey {
export type Tf1<
DiscriminatingKey extends string,
U1,
U1Again extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>,
> = U1 extends Pick<U2, DiscriminatingKey>
? Tf2<DiscriminatingKey, U1, U2, U1Again>
: U1;
export type Tf1< export type Tf2<
DiscriminatingKey extends string, DiscriminatingKey extends string,
U1, SingletonU1 extends Record<DiscriminatingKey, string>,
U1Again extends Record<DiscriminatingKey, string>, U2,
U2 extends Record<DiscriminatingKey, string> U1 extends Record<DiscriminatingKey, string>,
> = > = U2 extends Pick<SingletonU1, DiscriminatingKey>
U1 extends Pick<U2, DiscriminatingKey> ? ? U2 & SingletonU1
Tf2<DiscriminatingKey, U1, U2, U1Again> : : U2 extends Pick<U1, DiscriminatingKey>
U1; ? never
: U2;
export type Tf2<
DiscriminatingKey extends string,
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> =
U2 extends Pick<SingletonU1, DiscriminatingKey> ?
U2 & SingletonU1 :
U2 extends Pick<U1, DiscriminatingKey> ?
never :
U2;
} }

View File

@ -1,4 +1,3 @@
export type DeepPartial<T> = { export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>; [P in keyof T]?: DeepPartial<T[P]>;
}; };

View File

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-types */
import type { FC, ComponentClass } from "react"; import type { FC, ComponentClass } from "react";
export type ReactComponent<Props extends Record<string, unknown> = {}> = export type ReactComponent<Props extends Record<string, unknown> = {}> =
| ((props: Props) => ReturnType<FC>) | ((props: Props) => ReturnType<FC>)
| ComponentClass<Props>; | ComponentClass<Props>;

View File

@ -1,10 +1,9 @@
import "minimal-polyfills/Object.fromEntries"; import "minimal-polyfills/Object.fromEntries";
export function allPropertiesValuesToUndefined<T extends Record<string, unknown>>(obj: T): Record<keyof T, undefined> { export function allPropertiesValuesToUndefined<
T extends Record<string, unknown>,
>(obj: T): Record<keyof T, undefined> {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj) Object.entries(obj).map(([key]) => [key, undefined]),
.map(([key]) => [key, undefined])
) as any; ) as any;
} }

View File

@ -1,23 +1,25 @@
import { Deferred } from "evt/tools/Deferred"; import { Deferred } from "evt/tools/Deferred";
export function appendHead( export function appendHead(
params: { params:
type: "css"; | {
href: string; type: "css";
} | { href: string;
type: "javascript"; }
src: string; | {
} type: "javascript";
src: string;
},
) { ) {
const htmlElement = document.createElement( const htmlElement = document.createElement(
(() => { (() => {
switch (params.type) { switch (params.type) {
case "css": return "link"; case "css":
case "javascript": return "script"; return "link";
case "javascript":
return "script";
} }
})() })(),
); );
const dLoaded = new Deferred<void>(); const dLoaded = new Deferred<void>();
@ -28,22 +30,23 @@ export function appendHead(
htmlElement, htmlElement,
(() => { (() => {
switch (params.type) { switch (params.type) {
case "css": return { case "css":
"href": params.href, return {
"type": "text/css", "href": params.href,
"rel": "stylesheet", "type": "text/css",
"media": "screen,print" "rel": "stylesheet",
}; "media": "screen,print",
case "javascript": return { };
"src": params.src, case "javascript":
"type": "text/javascript", return {
}; "src": params.src,
"type": "text/javascript",
};
} }
})() })(),
); );
document.getElementsByTagName("head")[0].appendChild(htmlElement); document.getElementsByTagName("head")[0].appendChild(htmlElement);
return dLoaded.pr; return dLoaded.pr;
}
}

View File

@ -1,2 +1 @@
export { assert } from "tsafe/assert";
export { assert } from "tsafe/assert";

View File

@ -1,59 +1,47 @@
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { is } from "tsafe/is"; import { is } from "tsafe/is";
//Warning: Be mindful that because of array this is not idempotent. //Warning: Be mindful that because of array this is not idempotent.
export function deepAssign( export function deepAssign(params: {
params: { target: Record<string, unknown>;
target: Record<string, unknown>; source: Record<string, unknown>;
source: Record<string, unknown>; }) {
} const { target, source } = params;
) {
const { target, source } = params; Object.keys(source).forEach(key => {
var dereferencedSource = source[key];
Object.keys(source).forEach(key => { if (
var dereferencedSource = source[key]; target[key] === undefined ||
!(dereferencedSource instanceof Object)
) {
Object.defineProperty(target, key, {
"enumerable": true,
"writable": true,
"configurable": true,
"value": dereferencedSource,
});
if ( return;
target[key] === undefined || }
!(dereferencedSource instanceof Object)
) {
Object.defineProperty( const dereferencedTarget = target[key];
target,
key,
{
"enumerable": true,
"writable": true,
"configurable": true,
"value": dereferencedSource
}
);
return; if (dereferencedSource instanceof Array) {
} assert(is<unknown[]>(dereferencedTarget));
assert(is<unknown[]>(dereferencedSource));
const dereferencedTarget = target[key]; dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
if (dereferencedSource instanceof Array) { return;
}
assert(is<unknown[]>(dereferencedTarget)); assert(is<Record<string, unknown>>(dereferencedTarget));
assert(is<unknown[]>(dereferencedSource)); assert(is<Record<string, unknown>>(dereferencedSource));
dereferencedSource.forEach(entry => dereferencedTarget.push(entry)); deepAssign({
"target": dereferencedTarget,
return; "source": dereferencedSource,
} });
});
assert(is<Record<string, unknown>>(dereferencedTarget)); }
assert(is<Record<string, unknown>>(dereferencedSource));
deepAssign({
"target": dereferencedTarget,
"source": dereferencedSource
});
});
}

View File

@ -1,4 +1,3 @@
export function deepClone<T>(arg: T): T { export function deepClone<T>(arg: T): T {
return JSON.parse(JSON.stringify(arg)); return JSON.parse(JSON.stringify(arg));
} }

View File

@ -1,9 +1,8 @@
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { generateKeycloakThemeResources } from "../../bin/build-keycloak-theme/generateKeycloakThemeResources"; import { generateKeycloakThemeResources } from "../../bin/build-keycloak-theme/generateKeycloakThemeResources";
import { import {
setupSampleReactProject, setupSampleReactProject,
sampleReactProjectDirPath sampleReactProjectDirPath,
} from "./setupSampleReactProject"; } from "./setupSampleReactProject";
setupSampleReactProject(); setupSampleReactProject();
@ -11,11 +10,13 @@ setupSampleReactProject();
generateKeycloakThemeResources({ generateKeycloakThemeResources({
"themeName": "keycloakify-demo-app", "themeName": "keycloakify-demo-app",
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"), "reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"), "keycloakThemeBuildingDirPath": pathJoin(
sampleReactProjectDirPath,
"build_keycloak_theme",
),
"urlPathname": "/keycloakify-demo-app/", "urlPathname": "/keycloakify-demo-app/",
"urlOrigin": undefined, "urlOrigin": undefined,
"extraPagesId": ["my-custom-page.ftl"], "extraPagesId": ["my-custom-page.ftl"],
"extraThemeProperties": ["env=test"], "extraThemeProperties": ["env=test"],
"keycloakVersion": "11.0.3" "keycloakVersion": "11.0.3",
}); });

View File

@ -1,8 +1,6 @@
import { import {
setupSampleReactProject, setupSampleReactProject,
sampleReactProjectDirPath sampleReactProjectDirPath,
} from "./setupSampleReactProject"; } from "./setupSampleReactProject";
import * as st from "scripting-tools"; import * as st from "scripting-tools";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
@ -10,15 +8,15 @@ import { getProjectRoot } from "../../bin/tools/getProjectRoot";
setupSampleReactProject(); setupSampleReactProject();
const binDirPath= pathJoin(getProjectRoot(), "dist", "bin"); const binDirPath = pathJoin(getProjectRoot(), "dist", "bin");
st.execSyncTrace( st.execSyncTrace(
//`node ${pathJoin(binDirPath, "build-keycloak-theme")} --external-assets`, //`node ${pathJoin(binDirPath, "build-keycloak-theme")} --external-assets`,
`node ${pathJoin(binDirPath, "build-keycloak-theme")}`, `node ${pathJoin(binDirPath, "build-keycloak-theme")}`,
{ "cwd": sampleReactProjectDirPath } { "cwd": sampleReactProjectDirPath },
); );
st.execSyncTrace( st.execSyncTrace(
`node ${pathJoin(binDirPath, "download-builtin-keycloak-theme")}`, `node ${pathJoin(binDirPath, "download-builtin-keycloak-theme")}`,
{ "cwd": sampleReactProjectDirPath } { "cwd": sampleReactProjectDirPath },
); );

View File

@ -1,8 +1,7 @@
import {
import { 
replaceImportsFromStaticInJsCode, replaceImportsFromStaticInJsCode,
replaceImportsInCssCode, replaceImportsInCssCode,
generateCssCodeToDefineGlobals generateCssCodeToDefineGlobals,
} from "../../bin/build-keycloak-theme/replaceImportFromStatic"; } from "../../bin/build-keycloak-theme/replaceImportFromStatic";
const { fixedJsCode } = replaceImportsFromStaticInJsCode({ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
@ -19,7 +18,7 @@ const { fixedJsCode } = replaceImportsFromStaticInJsCode({
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
`, `,
"urlOrigin": undefined "urlOrigin": undefined,
}); });
const { fixedJsCode: fixedJsCodeExternal } = replaceImportsFromStaticInJsCode({ const { fixedJsCode: fixedJsCodeExternal } = replaceImportsFromStaticInJsCode({
@ -36,10 +35,10 @@ const { fixedJsCode: fixedJsCodeExternal } = replaceImportsFromStaticInJsCode({
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
`, `,
"urlOrigin": "https://www.example.com" "urlOrigin": "https://www.example.com",
}); });
console.log({ fixedJsCode, fixedJsCodeExternal }); console.log({ fixedJsCode, fixedJsCodeExternal });
const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({ const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
"cssCode": ` "cssCode": `
@ -55,13 +54,14 @@ const { fixedCssCode, cssGlobalsToDefine } = replaceImportsInCssCode({
.my-div { .my-div {
background-image: url(/static/media/something.svg); background-image: url(/static/media/something.svg);
} }
` `,
}); });
console.log({ fixedCssCode, cssGlobalsToDefine }); console.log({ fixedCssCode, cssGlobalsToDefine });
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
"urlPathname": "/",
});
const { cssCodeToPrependInHead } = generateCssCodeToDefineGlobals({ cssGlobalsToDefine, "urlPathname": "/" }); console.log({ cssCodeToPrependInHead });
console.log({ cssCodeToPrependInHead });

View File

@ -1,14 +1,15 @@
import { getProjectRoot } from "../../bin/tools/getProjectRoot"; import { getProjectRoot } from "../../bin/tools/getProjectRoot";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { downloadAndUnzip } from "../../bin/tools/downloadAndUnzip"; import { downloadAndUnzip } from "../../bin/tools/downloadAndUnzip";
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project"); export const sampleReactProjectDirPath = pathJoin(
getProjectRoot(),
"sample_react_project",
);
export function setupSampleReactProject() { export function setupSampleReactProject() {
downloadAndUnzip({ downloadAndUnzip({
"url": "https://github.com/garronej/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip", "url": "https://github.com/garronej/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": sampleReactProjectDirPath "destDirPath": sampleReactProjectDirPath,
}); });
} }

View File

@ -1,249 +1,279 @@
import { getKcContext } from "../../lib/getKcContext"; import { getKcContext } from "../../lib/getKcContext";
import type { KcContextBase } from "../../lib/getKcContext"; import type { KcContextBase } from "../../lib/getKcContext";
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext"; import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
import { same } from "evt/tools/inDepth"; import { same } from "evt/tools/inDepth";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import { kcContextMocks, kcContextCommonMock } from "../../lib/getKcContext/kcContextMocks"; import {
kcContextMocks,
kcContextCommonMock,
} from "../../lib/getKcContext/kcContextMocks";
import { deepClone } from "../../lib/tools/deepClone"; import { deepClone } from "../../lib/tools/deepClone";
{ {
const authorizedMailDomains = [
"example.com",
"another-example.com",
"*.yet-another-example.com",
"*.example.com",
"hello-world.com",
];
const displayName = "this is an overwritten common value";
const aNonStandardValue1 = "a non standard value 1";
const aNonStandardValue2 = "a non standard value 2";
type KcContextExtended =
| {
pageId: "register.ftl";
authorizedMailDomains: string[];
}
| {
pageId: "info.ftl";
aNonStandardValue1: string;
}
| {
pageId: "my-extra-page-1.ftl";
}
| {
pageId: "my-extra-page-2.ftl";
aNonStandardValue2: string;
};
const getKcContextProxy = (params: {
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
}) => {
const { mockPageId } = params;
const { kcContext } = getKcContext<KcContextExtended>({
mockPageId,
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName },
},
{
"pageId": "info.ftl",
aNonStandardValue1,
},
{
"pageId": "register.ftl",
authorizedMailDomains,
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue2,
},
],
});
return { kcContext };
};
{
const pageId = "login.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContextBase.Login>>();
assert(
same(
//NOTE: deepClone for printIfExists or other functions...
deepClone(kcContext),
(() => {
const mock = deepClone(
kcContextMocks.find(
({ pageId: pageId_i }) => pageId_i === pageId,
)!,
);
mock.realm.displayName = displayName;
return mock;
})(),
),
);
console.log(`PASS ${pageId}`);
}
{
const pageId = "info.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<
Equals<
typeof kcContext,
KcContextBase.Info & {
pageId: typeof pageId;
aNonStandardValue1: string;
}
>
>();
assert(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(
kcContextMocks.find(
({ pageId: pageId_i }) => pageId_i === pageId,
)!,
);
Object.assign(mock, { aNonStandardValue1 });
return mock;
})(),
),
);
console.log(`PASS ${pageId}`);
}
{
const pageId = "register.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<
Equals<
typeof kcContext,
KcContextBase.Register & {
pageId: typeof pageId;
authorizedMailDomains: string[];
}
>
>();
assert(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(
kcContextMocks.find(
({ pageId: pageId_i }) => pageId_i === pageId,
)!,
);
Object.assign(mock, { authorizedMailDomains });
return mock;
})(),
),
);
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-2.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<
Equals<
typeof kcContext,
KcContextBase.Common & {
pageId: typeof pageId;
aNonStandardValue2: string;
}
>
>();
kcContext.aNonStandardValue2;
assert(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId, aNonStandardValue2 });
return mock;
})(),
),
);
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-1.ftl";
console.log("We expect a warning here =>");
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<
Equals<
typeof kcContext,
KcContextBase.Common & { pageId: typeof pageId }
>
>();
const authorizedMailDomains = [ assert(
"example.com", same(
"another-example.com", deepClone(kcContext),
"*.yet-another-example.com", (() => {
"*.example.com", const mock = deepClone(kcContextCommonMock);
"hello-world.com"
];
const displayName = "this is an overwritten common value"; Object.assign(mock, { pageId });
const aNonStandardValue1 = "a non standard value 1"; return mock;
const aNonStandardValue2 = "a non standard value 2"; })(),
),
type KcContextExtended = { );
pageId: "register.ftl";
authorizedMailDomains: string[];
} | {
pageId: "info.ftl";
aNonStandardValue1: string;
} | {
pageId: "my-extra-page-1.ftl";
} | {
pageId: "my-extra-page-2.ftl";
aNonStandardValue2: string;
};
const getKcContextProxy = (
params: {
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
}
) => {
const { mockPageId } = params;
const { kcContext } = getKcContext<KcContextExtended>({
mockPageId,
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName }
},
{
"pageId": "info.ftl",
aNonStandardValue1
},
{
"pageId": "register.ftl",
authorizedMailDomains
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue2
}
]
});
return { kcContext };
};
{
const pageId = "login.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContextBase.Login>>();
assert(same(
//NOTE: deepClone for printIfExists or other functions...
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
mock.realm.displayName = displayName;
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "info.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<Equals<typeof kcContext, KcContextBase.Info & { pageId: typeof pageId; aNonStandardValue1: string; }>>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { aNonStandardValue1 });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "register.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<Equals<typeof kcContext, KcContextBase.Register & { pageId: typeof pageId; authorizedMailDomains: string[]; }>>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { authorizedMailDomains });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-2.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; aNonStandardValue2: string; }>>();
kcContext.aNonStandardValue2;
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId, aNonStandardValue2 });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
{
const pageId = "my-extra-page-1.ftl";
console.log("We expect a warning here =>");
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; }>>();
assert(same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId });
return mock;
})()
));
console.log(`PASS ${pageId}`);
}
console.log(`PASS ${pageId}`);
}
} }
{ {
const pageId = "login.ftl";
const pageId = "login.ftl"; const { kcContext } = getKcContext({
"mockPageId": pageId,
});
const { kcContext } = getKcContext({ assert<Equals<typeof kcContext, KcContextBase | undefined>>();
"mockPageId": pageId
});
assert<Equals<typeof kcContext, KcContextBase | undefined>>(); assert(
same(
assert(same( deepClone(kcContext),
deepClone(kcContext), deepClone(
deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!) kcContextMocks.find(
)); ({ pageId: pageId_i }) => pageId_i === pageId,
)!,
console.log("PASS no extension"); ),
),
);
console.log("PASS no extension");
} }
{ {
const { kcContext } = getKcContext();
const { kcContext } = getKcContext(); assert<Equals<typeof kcContext, KcContextBase | undefined>>();
assert<Equals<typeof kcContext, KcContextBase | undefined>>(); assert(kcContext === undefined);
assert(kcContext === undefined);
console.log("PASS no extension, no mock");
console.log("PASS no extension, no mock");
} }

View File

@ -1,2 +1 @@
import "./getKcContext";
import "./getKcContext";

View File

@ -1,91 +1,79 @@
import { AndByDiscriminatingKey } from "../../../lib/tools/AndByDiscriminatingKey"; import { AndByDiscriminatingKey } from "../../../lib/tools/AndByDiscriminatingKey";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
type Base = type Base =
{ pageId: "a"; onlyA: string; } | | { pageId: "a"; onlyA: string }
{ pageId: "b"; onlyB: string; } | | { pageId: "b"; onlyB: string }
{ pageId: "only base"; onlyBase: string; }; | { pageId: "only base"; onlyBase: string };
type Extension = type Extension =
{ pageId: "a"; onlyExtA: string; } | | { pageId: "a"; onlyExtA: string }
{ pageId: "b"; onlyExtB: string; } | | { pageId: "b"; onlyExtB: string }
{ pageId: "only ext"; onlyExt: string; }; | { pageId: "only ext"; onlyExt: string };
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>; type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
type Expected = type Expected =
{ pageId: "a"; onlyA: string; onlyExtA: string; } | | { pageId: "a"; onlyA: string; onlyExtA: string }
{ pageId: "b"; onlyB: string; onlyExtB: string; } | | { pageId: "b"; onlyB: string; onlyExtB: string }
{ pageId: "only base"; onlyBase: string; } | | { pageId: "only base"; onlyBase: string }
{ pageId: "only ext"; onlyExt: string; }; | { pageId: "only ext"; onlyExt: string };
assert<Equals<Got, Expected>>(); assert<Equals<Got, Expected>>();
const x: Got = null as any; const x: Got = null as any;
if (x.pageId === "a") { if (x.pageId === "a") {
x.onlyA;
x.onlyExtA;
x.onlyA; //@ts-expect-error
x.onlyExtA; x.onlyB;
//@ts-expect-error //@ts-expect-error
x.onlyB; x.onlyBase;
//@ts-expect-error
x.onlyBase;
//@ts-expect-error
x.onlyExt;
//@ts-expect-error
x.onlyExt;
} }
if (x.pageId === "b") { if (x.pageId === "b") {
x.onlyB;
x.onlyExtB;
x.onlyB; //@ts-expect-error
x.onlyExtB; x.onlyA;
//@ts-expect-error //@ts-expect-error
x.onlyA; x.onlyBase;
//@ts-expect-error
x.onlyBase;
//@ts-expect-error
x.onlyExt;
//@ts-expect-error
x.onlyExt;
} }
if (x.pageId === "only base") { if (x.pageId === "only base") {
x.onlyBase;
x.onlyBase; //@ts-expect-error
x.onlyA;
//@ts-expect-error //@ts-expect-error
x.onlyA; x.onlyB;
//@ts-expect-error
x.onlyB;
//@ts-expect-error
x.onlyExt;
//@ts-expect-error
x.onlyExt;
} }
if (x.pageId === "only ext") { if (x.pageId === "only ext") {
x.onlyExt;
x.onlyExt; //@ts-expect-error
x.onlyA;
//@ts-expect-error
x.onlyB;
//@ts-expect-error //@ts-expect-error
x.onlyA; x.onlyBase;
//@ts-expect-error
x.onlyB;
//@ts-expect-error
x.onlyBase;
} }

644
yarn.lock
View File

@ -2,13 +2,6 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/code-frame@7.12.11":
version "7.12.11"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
dependencies:
"@babel/highlight" "^7.10.4"
"@babel/code-frame@^7.0.0": "@babel/code-frame@^7.0.0":
version "7.15.8" version "7.15.8"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.15.8.tgz#45990c47adadb00c03677baa89221f7cc23d2503"
@ -21,7 +14,7 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz#220df993bfe904a4a6b02ab4f3385a5ebf6e2389"
integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w== integrity sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==
"@babel/highlight@^7.10.4", "@babel/highlight@^7.14.5": "@babel/highlight@^7.14.5":
version "7.14.5" version "7.14.5"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.14.5.tgz#6861a52f03966405001f6aa534a01a24d99e8cd9"
integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg== integrity sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==
@ -112,61 +105,6 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==
"@eslint/eslintrc@^0.4.3":
version "0.4.3"
resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c"
integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
espree "^7.3.0"
globals "^13.9.0"
ignore "^4.0.6"
import-fresh "^3.2.1"
js-yaml "^3.13.1"
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@humanwhocodes/config-array@^0.5.0":
version "0.5.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9"
integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==
dependencies:
"@humanwhocodes/object-schema" "^1.2.0"
debug "^4.1.1"
minimatch "^3.0.4"
"@humanwhocodes/object-schema@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@types/json-schema@^7.0.7":
version "7.0.9"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d"
integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==
"@types/mdast@^3.0.0", "@types/mdast@^3.0.3": "@types/mdast@^3.0.0", "@types/mdast@^3.0.3":
version "3.0.10" version "3.0.10"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af"
@ -208,86 +146,6 @@
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
"@typescript-eslint/eslint-plugin@^4.24.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz#c24dc7c8069c7706bc40d99f6fa87edcb2005276"
integrity sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==
dependencies:
"@typescript-eslint/experimental-utils" "4.33.0"
"@typescript-eslint/scope-manager" "4.33.0"
debug "^4.3.1"
functional-red-black-tree "^1.0.1"
ignore "^5.1.8"
regexpp "^3.1.0"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/experimental-utils@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz#6f2a786a4209fa2222989e9380b5331b2810f7fd"
integrity sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==
dependencies:
"@types/json-schema" "^7.0.7"
"@typescript-eslint/scope-manager" "4.33.0"
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/typescript-estree" "4.33.0"
eslint-scope "^5.1.1"
eslint-utils "^3.0.0"
"@typescript-eslint/parser@^4.24.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.33.0.tgz#dfe797570d9694e560528d18eecad86c8c744899"
integrity sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==
dependencies:
"@typescript-eslint/scope-manager" "4.33.0"
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/typescript-estree" "4.33.0"
debug "^4.3.1"
"@typescript-eslint/scope-manager@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz#d38e49280d983e8772e29121cf8c6e9221f280a3"
integrity sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==
dependencies:
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/visitor-keys" "4.33.0"
"@typescript-eslint/types@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.33.0.tgz#a1e59036a3b53ae8430ceebf2a919dc7f9af6d72"
integrity sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==
"@typescript-eslint/typescript-estree@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz#0dfb51c2908f68c5c08d82aefeaf166a17c24609"
integrity sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==
dependencies:
"@typescript-eslint/types" "4.33.0"
"@typescript-eslint/visitor-keys" "4.33.0"
debug "^4.3.1"
globby "^11.0.3"
is-glob "^4.0.1"
semver "^7.3.5"
tsutils "^3.21.0"
"@typescript-eslint/visitor-keys@4.33.0":
version "4.33.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz#2a22f77a41604289b7a186586e9ec48ca92ef1dd"
integrity sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==
dependencies:
"@typescript-eslint/types" "4.33.0"
eslint-visitor-keys "^2.0.0"
acorn-jsx@^5.3.1:
version "5.3.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937"
integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
acorn@^7.4.0:
version "7.4.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
aggregate-error@^3.0.0: aggregate-error@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@ -296,26 +154,6 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0" clean-stack "^2.0.0"
indent-string "^4.0.0" indent-string "^4.0.0"
ajv@^6.10.0, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
dependencies:
fast-deep-equal "^3.1.1"
fast-json-stable-stringify "^2.0.0"
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.1:
version "8.6.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"
integrity sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ansi-colors@^4.1.1: ansi-colors@^4.1.1:
version "4.1.1" version "4.1.1"
resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"
@ -347,18 +185,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies: dependencies:
color-convert "^2.0.1" color-convert "^2.0.1"
argparse@^1.0.7:
version "1.0.10"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
dependencies:
sprintf-js "~1.0.2"
array-union@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
astral-regex@^2.0.0: astral-regex@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@ -567,7 +393,7 @@ cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
path-type "^4.0.0" path-type "^4.0.0"
yaml "^1.10.0" yaml "^1.10.0"
cross-spawn@^7.0.2, cross-spawn@^7.0.3: cross-spawn@^7.0.3:
version "7.0.3" version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
@ -605,32 +431,13 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50" es5-ext "^0.10.50"
type "^1.0.1" type "^1.0.1"
debug@^4.0.0, debug@^4.0.1, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2: debug@^4.0.0, debug@^4.3.2:
version "4.3.2" version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
deep-is@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
dir-glob@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==
dependencies:
path-type "^4.0.0"
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==
dependencies:
esutils "^2.0.2"
dom-serializer@^1.0.1, dom-serializer@^1.3.2: dom-serializer@^1.0.1, dom-serializer@^1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.3.2.tgz#6206437d32ceefaec7161803230c7a20bc1b4d91"
@ -673,7 +480,7 @@ emoji-regex@^8.0.0:
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
enquirer@^2.3.5, enquirer@^2.3.6: enquirer@^2.3.6:
version "2.3.6" version "2.3.6"
resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d"
integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==
@ -743,137 +550,6 @@ escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^8.3.0:
version "8.3.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz#f7471b20b6fe8a9a9254cc684454202886a2dd7a"
integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==
eslint-scope@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c"
integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==
dependencies:
esrecurse "^4.3.0"
estraverse "^4.1.1"
eslint-utils@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27"
integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==
dependencies:
eslint-visitor-keys "^1.1.0"
eslint-utils@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672"
integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==
dependencies:
eslint-visitor-keys "^2.0.0"
eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e"
integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==
eslint-visitor-keys@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303"
integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==
eslint@^7.26.0:
version "7.32.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d"
integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.3"
"@humanwhocodes/config-array" "^0.5.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
escape-string-regexp "^4.0.0"
eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^2.0.0"
espree "^7.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
fast-deep-equal "^3.1.3"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.1.2"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash.merge "^4.6.2"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^6.0.9"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^7.3.0, espree@^7.3.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==
dependencies:
acorn "^7.4.0"
acorn-jsx "^5.3.1"
eslint-visitor-keys "^1.3.0"
esprima@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71"
integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
esquery@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5"
integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==
dependencies:
estraverse "^5.1.0"
esrecurse@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921"
integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
dependencies:
estraverse "^5.2.0"
estraverse@^4.1.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d"
integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==
estraverse@^5.1.0, estraverse@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.2.0.tgz#307df42547e6cc7324d3cf03c155d5cdb8c53880"
integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==
esutils@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
event-emitter@^0.3.5: event-emitter@^0.3.5:
version "0.3.5" version "0.3.5"
resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39"
@ -918,46 +594,6 @@ extend@^3.0.0:
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
version "3.1.3"
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-glob@^3.1.1:
version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
integrity sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fast-json-stable-stringify@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633"
integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
fast-levenshtein@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
fastq@^1.6.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c"
integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==
dependencies:
reusify "^1.0.4"
file-entry-cache@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027"
integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==
dependencies:
flat-cache "^3.0.4"
fill-range@^7.0.1: fill-range@^7.0.1:
version "7.0.1" version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
@ -980,29 +616,11 @@ find-versions@^4.0.0:
dependencies: dependencies:
semver-regex "^3.1.2" semver-regex "^3.1.2"
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==
dependencies:
flatted "^3.1.0"
rimraf "^3.0.2"
flatted@^3.1.0:
version "3.2.2"
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.2.tgz#64bfed5cb68fe3ca78b3eb214ad97b63bedce561"
integrity sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==
fs.realpath@^1.0.0: fs.realpath@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
get-caller-file@^2.0.5: get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@ -1018,13 +636,6 @@ get-stream@^6.0.0:
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7"
integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==
glob-parent@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob@^7.0.5, glob@^7.1.3: glob@^7.0.5, glob@^7.1.3:
version "7.2.0" version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
@ -1037,25 +648,6 @@ glob@^7.0.5, glob@^7.1.3:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
globals@^13.6.0, globals@^13.9.0:
version "13.11.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.11.0.tgz#40ef678da117fe7bd2e28f1fab24951bd0255be7"
integrity sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==
dependencies:
type-fest "^0.20.2"
globby@^11.0.3:
version "11.0.4"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.4.tgz#2cbaff77c2f2a62e71e9b2813a67b97a3a3001a5"
integrity sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==
dependencies:
array-union "^2.1.0"
dir-glob "^3.0.1"
fast-glob "^3.1.1"
ignore "^5.1.4"
merge2 "^1.3.0"
slash "^3.0.0"
has-flag@^3.0.0: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -1153,17 +745,7 @@ husky@^4.3.8:
slash "^3.0.0" slash "^3.0.0"
which-pm-runs "^1.0.0" which-pm-runs "^1.0.0"
ignore@^4.0.6: import-fresh@^3.2.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.4, ignore@^5.1.8:
version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b"
integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==
@ -1171,11 +753,6 @@ import-fresh@^3.0.0, import-fresh@^3.2.1:
parent-module "^1.0.0" parent-module "^1.0.0"
resolve-from "^4.0.0" resolve-from "^4.0.0"
imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
indent-string@^4.0.0: indent-string@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
@ -1232,23 +809,11 @@ is-decimal@^1.0.0:
resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5" resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-1.0.4.tgz#65a3a5958a1c5b63a706e1b333d7cd9f630d3fa5"
integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw== integrity sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=
is-fullwidth-code-point@^3.0.0: is-fullwidth-code-point@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.0, is-glob@^4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-hexadecimal@^1.0.0: is-hexadecimal@^1.0.0:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7"
@ -1304,42 +869,11 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
js-yaml@^3.13.1:
version "3.14.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537"
integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
dependencies:
argparse "^1.0.7"
esprima "^4.0.0"
json-parse-even-better-errors@^2.3.0: json-parse-even-better-errors@^2.3.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json-schema-traverse@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660"
integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
json-schema-traverse@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
json-stable-stringify-without-jsonify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
dependencies:
prelude-ls "^1.2.1"
type-check "~0.4.0"
lines-and-columns@^1.1.6: lines-and-columns@^1.1.6:
version "1.1.6" version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@ -1390,21 +924,6 @@ lodash.camelcase@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY=
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.truncate@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
log-update@^4.0.0: log-update@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1"
@ -1422,13 +941,6 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
lru-cache@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
dependencies:
yallist "^4.0.0"
lru-queue@^0.1.0: lru-queue@^0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3"
@ -1478,11 +990,6 @@ merge-stream@^2.0.0:
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromark@~2.11.0: micromark@~2.11.0:
version "2.11.4" version "2.11.4"
resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a" resolved "https://registry.yarnpkg.com/micromark/-/micromark-2.11.4.tgz#d13436138eea826383e822449c9a5c50ee44665a"
@ -1539,11 +1046,6 @@ multipipe@^1.0.2:
duplexer2 "^0.1.2" duplexer2 "^0.1.2"
object-assign "^4.1.0" object-assign "^4.1.0"
natural-compare@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-tick@1, next-tick@^1.1.0: next-tick@1, next-tick@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
@ -1610,18 +1112,6 @@ opencollective-postinstall@^2.0.2:
resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259" resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz#7a0fff978f6dbfa4d006238fbac98ed4198c3259"
integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q== integrity sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"
integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==
dependencies:
deep-is "^0.1.3"
fast-levenshtein "^2.0.6"
levn "^0.4.1"
prelude-ls "^1.2.1"
type-check "^0.4.0"
word-wrap "^1.2.3"
p-limit@^3.0.2: p-limit@^3.0.2:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
@ -1741,10 +1231,10 @@ powerhooks@^0.9.3:
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
tsafe "^0.4.1" tsafe "^0.4.1"
prelude-ls@^1.2.1: prettier@^2.3.0:
version "1.2.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.4.1.tgz#671e11c89c14a4cfc876ce564106c4a6726c9f5c"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== integrity sha512-9fbDAXSBcc6Bs1mZrDYb3XKzDLm4EXXL9sC1LqKP5rZkT6KRr/rf9amVUcODVXgguK/isJz0d0hP72WeaKWsvA==
process-nextick-args@~2.0.0: process-nextick-args@~2.0.0:
version "2.0.1" version "2.0.1"
@ -1756,11 +1246,6 @@ process@^0.11.1:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
progress@^2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@^15.7.2: prop-types@^15.7.2:
version "15.7.2" version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@ -1777,16 +1262,6 @@ properties-parser@^0.3.1:
dependencies: dependencies:
string.prototype.codepointat "^0.2.0" string.prototype.codepointat "^0.2.0"
punycode@^2.1.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
ramda@^0.27.1: ramda@^0.27.1:
version "0.27.1" version "0.27.1"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9"
@ -1854,11 +1329,6 @@ regenerator-runtime@^0.13.4:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
regexpp@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==
remark-parse@^9.0.0: remark-parse@^9.0.0:
version "9.0.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640" resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-9.0.0.tgz#4d20a299665880e4f4af5d90b7c7b8a935853640"
@ -1871,11 +1341,6 @@ require-directory@^2.1.1:
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-from-string@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909"
integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==
resize-observer-polyfill@^1.5.1: resize-observer-polyfill@^1.5.1:
version "1.5.1" version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
@ -1894,11 +1359,6 @@ restore-cursor@^3.1.0:
onetime "^5.1.0" onetime "^5.1.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
rimraf@^3.0.2: rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
@ -1913,13 +1373,6 @@ run-exclusive@^2.2.14:
dependencies: dependencies:
minimal-polyfills "^2.1.5" minimal-polyfills "^2.1.5"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
rxjs@^6.6.7: rxjs@^6.6.7:
version "6.6.7" version "6.6.7"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9"
@ -1947,13 +1400,6 @@ semver-regex@^3.1.2:
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.3.tgz#b2bcc6f97f63269f286994e297e229b6245d0dc3" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.3.tgz#b2bcc6f97f63269f286994e297e229b6245d0dc3"
integrity sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ== integrity sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==
semver@^7.2.1, semver@^7.3.5:
version "7.3.5"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7"
integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
dependencies:
lru-cache "^6.0.0"
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -1994,17 +1440,12 @@ slice-ansi@^4.0.0:
astral-regex "^2.0.0" astral-regex "^2.0.0"
is-fullwidth-code-point "^3.0.0" is-fullwidth-code-point "^3.0.0"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
string-argv@0.3.1: string-argv@0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: string-width@^4.1.0, string-width@^4.2.0:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@ -2051,11 +1492,6 @@ strip-final-newline@^2.0.0:
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
strip-json-comments@^3.1.0, strip-json-comments@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
style-to-js@1.1.0: style-to-js@1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.0.tgz#631cbb20fce204019b3aa1fcb5b69d951ceac4ac" resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.0.tgz#631cbb20fce204019b3aa1fcb5b69d951ceac4ac"
@ -2096,23 +1532,6 @@ supports-color@^7.1.0:
dependencies: dependencies:
has-flag "^4.0.0" has-flag "^4.0.0"
table@^6.0.9:
version "6.7.2"
resolved "https://registry.yarnpkg.com/table/-/table-6.7.2.tgz#a8d39b9f5966693ca8b0feba270a78722cbaf3b0"
integrity sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==
dependencies:
ajv "^8.0.1"
lodash.clonedeep "^4.5.0"
lodash.truncate "^4.4.2"
slice-ansi "^4.0.0"
string-width "^4.2.3"
strip-ansi "^6.0.1"
text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
through2@^2.0.1: through2@^2.0.1:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@ -2164,7 +1583,7 @@ tsafe@^0.8.1:
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.8.1.tgz#9af7e1540bc04313a82d60c98056a5017c8b086b" resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.8.1.tgz#9af7e1540bc04313a82d60c98056a5017c8b086b"
integrity sha512-EfPjxQHzndQAV/uh0SMGP26Wg3dCuaw8dRv2VPEuGHen5qzg2oqsMvZw2wkQFkiMisZq2fm95m5lheimW2Fpvg== integrity sha512-EfPjxQHzndQAV/uh0SMGP26Wg3dCuaw8dRv2VPEuGHen5qzg2oqsMvZw2wkQFkiMisZq2fm95m5lheimW2Fpvg==
tslib@^1.8.1, tslib@^1.9.0: tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
@ -2185,25 +1604,6 @@ tss-react@^1.0.0:
"@emotion/utils" "^1.0.0" "@emotion/utils" "^1.0.0"
html-react-parser "^1.2.7" html-react-parser "^1.2.7"
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"
integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==
dependencies:
tslib "^1.8.1"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
dependencies:
prelude-ls "^1.2.1"
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.21.3: type-fest@^0.21.3:
version "0.21.3" version "0.21.3"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
@ -2275,13 +1675,6 @@ untildify@^4.0.0:
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
uri-js@^4.2.2:
version "4.4.1"
resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e"
integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
dependencies:
punycode "^2.1.0"
util-deprecate@~1.0.1: util-deprecate@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@ -2294,11 +1687,6 @@ util@^0.10.3:
dependencies: dependencies:
inherits "2.0.3" inherits "2.0.3"
v8-compile-cache@^2.0.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee"
integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==
vfile-message@^2.0.0: vfile-message@^2.0.0:
version "2.0.4" version "2.0.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a" resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
@ -2329,11 +1717,6 @@ which@^2.0.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
word-wrap@^1.2.3:
version "1.2.3"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
wrap-ansi@^6.2.0: wrap-ansi@^6.2.0:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
@ -2374,11 +1757,6 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yallist@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yaml@^1.10.0: yaml@^1.10.0:
version "1.10.2" version "1.10.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"