Compare commits
185 Commits
Author | SHA1 | Date | |
---|---|---|---|
947fd0564e | |||
bd51d02902 | |||
36d75c8641 | |||
c75f158b48 | |||
bb37ce9cef | |||
77ff33570d | |||
20383d60a9 | |||
79aa5ac5f2 | |||
8be6c0d1d2 | |||
7f5a9e77de | |||
ff19ab8b08 | |||
63dcb2ad39 | |||
795e8ed0e5 | |||
bccb56ed61 | |||
02e2ad89ec | |||
a236e2e5de | |||
ba294c85f8 | |||
beb3dca495 | |||
04101536c6 | |||
2912e7e5dd | |||
bf6fadbde8 | |||
001b49d09a | |||
bbd5bdda95 | |||
7e950e8e2b | |||
8b0efbc737 | |||
93cfbd6696 | |||
acc1d028ab | |||
3476b5acc3 | |||
72ca5da842 | |||
e214280fcd | |||
bf32987a3e | |||
8941fe230b | |||
b6d4abee21 | |||
786bdc41c2 | |||
ed9f08f678 | |||
33fd6768f1 | |||
87b8456531 | |||
a12bde4656 | |||
6f219a4c2a | |||
49d7818b64 | |||
fb0be3272c | |||
994f7d6bea | |||
6e8dcecaf1 | |||
40237374a8 | |||
3d98860369 | |||
804fe33665 | |||
703171f96b | |||
27bdefeea8 | |||
7a3c74020d | |||
7509170dd0 | |||
cd17a97916 | |||
d5e690f964 | |||
a19bd20b6b | |||
f78526dfff | |||
11d6a2020f | |||
fabd48a22c | |||
e2ea98b5ef | |||
4473ab0704 | |||
1f68cc305a | |||
ec2543551f | |||
7b0bedc755 | |||
ab054ca515 | |||
1b49c7804c | |||
764a288b1a | |||
fc6910bc2c | |||
91dd1dcddc | |||
97e6aaca65 | |||
af5ff1ecfb | |||
c9b53b0d3a | |||
d05a62e1ea | |||
a83eec31d8 | |||
729503fe31 | |||
7137ff4257 | |||
6db11a7433 | |||
8666aa62dd | |||
eedcd7a2a6 | |||
e3e8fb663a | |||
6e663210ee | |||
42cd0fe2f0 | |||
daac05c1ad | |||
1d63c393a3 | |||
e8a3751b32 | |||
cb8a41d5be | |||
a8d4f7e23c | |||
93bcdac3be | |||
32d0388556 | |||
633a32ffd6 | |||
cb8b165c8e | |||
57134359b9 | |||
377436e46a | |||
51129aaeff | |||
a2bd5050ff | |||
128c416ce7 | |||
7184773521 | |||
1138313028 | |||
46bd319ebe | |||
cfcc48259c | |||
785ce7a8ab | |||
ad5de216b0 | |||
26b80d6af7 | |||
a8623d8066 | |||
86ab9f72a5 | |||
b3892dab8d | |||
57a5d034dd | |||
cee9569581 | |||
159429da6e | |||
a292cb0b4b | |||
d70985d8d2 | |||
484f95f5d2 | |||
6e0553af9b | |||
cb18d3d765 | |||
f316f38ae5 | |||
5f07cb374b | |||
96d31e07c3 | |||
99a5efe36c | |||
5c46ecc0ed | |||
cf93b68816 | |||
457421b8d6 | |||
d36ea9539a | |||
5a5337dc63 | |||
443081cc28 | |||
ac8503f8c8 | |||
1cc1fd0a5a | |||
34314aa4ca | |||
0d8dcf4829 | |||
47c6d0dd62 | |||
84937e3eec | |||
303e270b56 | |||
29fbcdc0a6 | |||
bb1ada6e14 | |||
4a422cc796 | |||
be0f244c02 | |||
78a8dc8458 | |||
38062af889 | |||
f2eadf5441 | |||
a42931384f | |||
8116ce697b | |||
4964b86d67 | |||
2b331e7655 | |||
c1468b688e | |||
4f7837c88e | |||
fd8e06f1dd | |||
b01a351eaa | |||
604655c02d | |||
6603ac4389 | |||
cca6f952ee | |||
df94a6322d | |||
73e7f64860 | |||
e17e1650d5 | |||
3ecb63d500 | |||
41ee7e90ef | |||
c70bba727e | |||
747248454d | |||
59386241b4 | |||
c70b9b0dd1 | |||
2ee00ed919 | |||
cbfc271da5 | |||
d45b492837 | |||
ed54c145b7 | |||
64ed9a6044 | |||
75267abd91 | |||
ba9a3992b7 | |||
a74c32ed6d | |||
c5f9812acc | |||
bb0d6853e5 | |||
8c9fe168d8 | |||
6c874c01b7 | |||
5bc84b621c | |||
dd421eedf5 | |||
570d8a73cc | |||
a95df42843 | |||
4ecbb30a1b | |||
96b40b9c49 | |||
c32eebdd46 | |||
5b17287555 | |||
fb01257c8b | |||
53470f8788 | |||
89b86936f6 | |||
d3a07edfcb | |||
98a3d6564e | |||
50a20c68ed | |||
3aad681538 | |||
92fb3b7529 | |||
1572f1137a | |||
b5075dd1eb |
36
.github/workflows/ci.yaml
vendored
36
.github/workflows/ci.yaml
vendored
@ -22,7 +22,6 @@ jobs:
|
||||
PACKAGE_MANAGER=yarn
|
||||
fi
|
||||
$PACKAGE_MANAGER run format:check
|
||||
|
||||
test:
|
||||
runs-on: macos-10.15
|
||||
needs: test_formatting
|
||||
@ -33,7 +32,7 @@ jobs:
|
||||
steps:
|
||||
- name: Tell if project is using npm or yarn
|
||||
id: step1
|
||||
uses: garronej/github_actions_toolkit@v2.2
|
||||
uses: garronej/ts-ci@v1.1.6
|
||||
with:
|
||||
action_name: tell_if_project_uses_npm_or_yarn
|
||||
- uses: actions/checkout@v2.3.4
|
||||
@ -51,28 +50,35 @@ jobs:
|
||||
npm test
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
if: github.event_name == 'push'
|
||||
# We run this only if it's a push on the default branch or if it's a PR from a
|
||||
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is
|
||||
# defined but GitHub Action don't allow it yet.
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
outputs:
|
||||
from_version: ${{ steps.step1.outputs.from_version }}
|
||||
to_version: ${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: ${{steps.step1.outputs.is_upgraded_version }}
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
is_release_beta: ${{steps.step1.outputs.is_release_beta }}
|
||||
steps:
|
||||
- uses: garronej/github_actions_toolkit@v2.2
|
||||
- uses: garronej/ts-ci@v1.1.6
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
branch: ${{ github.head_ref || github.ref }}
|
||||
|
||||
update_changelog:
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_if_version_upgraded
|
||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
||||
steps:
|
||||
- uses: garronej/github_actions_toolkit@v2.4
|
||||
- uses: garronej/ts-ci@v1.1.6
|
||||
with:
|
||||
action_name: update_changelog
|
||||
branch: ${{ github.ref }}
|
||||
branch: ${{ github.head_ref || github.ref }}
|
||||
|
||||
create_github_release:
|
||||
runs-on: ubuntu-latest
|
||||
@ -80,9 +86,6 @@ jobs:
|
||||
- update_changelog
|
||||
- check_if_version_upgraded
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
- name: Build GitHub release body
|
||||
id: step1
|
||||
run: |
|
||||
@ -98,10 +101,10 @@ jobs:
|
||||
with:
|
||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
target_commitish: ${{ github.ref }}
|
||||
target_commitish: ${{ github.head_ref || github.ref }}
|
||||
body: ${{ steps.step1.outputs.body }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
prerelease: ${{ needs.check_if_version_upgraded.outputs.is_release_beta == 'true' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -138,7 +141,12 @@ jobs:
|
||||
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
|
||||
false
|
||||
fi
|
||||
npm publish
|
||||
EXTRA_ARGS=""
|
||||
if [ "$IS_BETA" = "true" ]; then
|
||||
EXTRA_ARGS="--tag beta"
|
||||
fi
|
||||
npm publish $EXTRA_ARGS
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
IS_BETA: ${{ needs.check_if_version_upgraded.outputs.is_release_beta }}
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -44,3 +44,5 @@ jspm_packages
|
||||
|
||||
/sample_react_project/
|
||||
/.yarn_home/
|
||||
|
||||
.idea
|
||||
|
@ -3,4 +3,5 @@ node_modules/
|
||||
/CHANGELOG.md
|
||||
/.yarn_home/
|
||||
/src/test/apps/
|
||||
/src/tools/types/
|
||||
/src/tools/types/
|
||||
/sample_react_project
|
190
CHANGELOG.md
190
CHANGELOG.md
@ -1,3 +1,193 @@
|
||||
### **4.7.3** (2022-04-08)
|
||||
|
||||
- Mention that there is still problems with M1 Mac
|
||||
|
||||
### **4.7.2** (2022-04-06)
|
||||
|
||||
- #43: M1 Mac support
|
||||
|
||||
### **4.7.1** (2022-03-30)
|
||||
|
||||
- Improve browser autofill
|
||||
- factorization
|
||||
|
||||
## **4.7.0** (2022-03-17)
|
||||
|
||||
- Add support for options validator
|
||||
- remove duplicate dependency
|
||||
|
||||
## **4.6.0** (2022-03-07)
|
||||
|
||||
- Remove powerhooks as dev dependency
|
||||
|
||||
### **4.5.5** (2022-03-07)
|
||||
|
||||
- Update tss-react
|
||||
|
||||
### **4.5.4** (2022-03-06)
|
||||
|
||||
- Remove tss-react from peerDependencies (it becomes a dependency)
|
||||
- (dev script) Use tsconfig.json to tell we are at the root of the project
|
||||
|
||||
### **4.5.3** (2022-01-26)
|
||||
|
||||
- Themes no longer have to break on minor Keycloakify update
|
||||
|
||||
### **4.5.2** (2022-01-20)
|
||||
|
||||
- Test container uses Keycloak 16.1.0
|
||||
- Merge pull request #78 from InseeFrLab/Ann2827/pull
|
||||
|
||||
Ann2827/pull
|
||||
- Refactor #78
|
||||
- Compat with Keycloak 16 (and probably 17, 18) #79
|
||||
- Warning about compat issues with Keycloak 16
|
||||
- fix: changes
|
||||
- fix: Errors on pages login-idp-link-confirm and login-idp-link-email
|
||||
|
||||
ref: https://github.com/InseeFrLab/keycloakify/issues/75
|
||||
|
||||
### **4.5.1** (2022-01-18)
|
||||
|
||||
- fix previous version
|
||||
|
||||
## **4.5.0** (2022-01-18)
|
||||
|
||||
- Read public/CNAME for domain name in --externel-assets mode
|
||||
|
||||
## **4.4.0** (2022-01-01)
|
||||
|
||||
- Merge pull request #73 from lazToum/main
|
||||
|
||||
(feature) added login-page-expired.ftl
|
||||
- added login-page-expired.ftl
|
||||
- Add update instruction for 4.3.0
|
||||
|
||||
## **4.3.0** (2021-12-27)
|
||||
|
||||
- Merge pull request #72 from praiz/main
|
||||
|
||||
feat(*): added login-update-password
|
||||
- feat(*): added login-update-password
|
||||
|
||||
### **4.2.21** (2021-12-27)
|
||||
|
||||
- update dependencies
|
||||
|
||||
### **4.2.19** (2021-12-21)
|
||||
|
||||
- Merge pull request #70 from VBustamante/patch-1
|
||||
- Added realm name field to KcContext mocks object
|
||||
- Merge pull request #69 from VBustamante/patch-1
|
||||
|
||||
Adding name field to realm in KcContext type
|
||||
- Adding name field to realm in KcContext type
|
||||
|
||||
### **4.2.18** (2021-12-17)
|
||||
|
||||
- Improve css url() import (fix CRA 5)
|
||||
|
||||
### **4.2.17** (2021-12-16)
|
||||
|
||||
- Fix path.join polyfill
|
||||
|
||||
### **4.2.16** (2021-12-16)
|
||||
|
||||
|
||||
|
||||
### **4.2.15** (2021-12-16)
|
||||
|
||||
- use custom polyfill for path.join (fix webpack 5 build)
|
||||
|
||||
### **4.2.14** (2021-12-12)
|
||||
|
||||
- Merge pull request #65 from InseeFrLab/doge_ftl_errors
|
||||
|
||||
Prevent ftl errors in Keycloak log
|
||||
- Encourage users to report errors in logs
|
||||
- Fix ftl error related to url.loginAction in saml-post-form.ftl
|
||||
- Ftl prevent error with updateProfileCtx
|
||||
- Ftl prevent error with auth.attemptedUsername
|
||||
- Fix ftl error as comment formatting
|
||||
- Merge remote-tracking branch 'origin/main' into doge_ftl_errors
|
||||
- Update README, remove all instruction about errors in logs
|
||||
- Avoid error in Keycloak logs, fix long template loading time
|
||||
- Add missing collon in README sample code
|
||||
|
||||
Add miss ','
|
||||
|
||||
### **4.2.13** (2021-12-08)
|
||||
|
||||
- Fix broken link about how to import fonts #62
|
||||
- Add a video to show how to test the theme in a local container
|
||||
|
||||
### **4.2.12** (2021-12-08)
|
||||
|
||||
- Update post build instructions
|
||||
|
||||
### **4.2.11** (2021-12-07)
|
||||
|
||||
|
||||
|
||||
### **4.2.10** (2021-11-12)
|
||||
|
||||
- Export an exaustive list of KcLanguageTag
|
||||
|
||||
### **4.2.9** (2021-11-11)
|
||||
|
||||
- Fix useAdvancedMsg
|
||||
|
||||
### **4.2.8** (2021-11-10)
|
||||
|
||||
- Update doc about pattern that can be used for user attributes #50
|
||||
- Bring back Safari compat
|
||||
|
||||
### **4.2.7** (2021-11-09)
|
||||
|
||||
- Fix useFormValidationSlice
|
||||
|
||||
### **4.2.6** (2021-11-08)
|
||||
|
||||
- Fix deepClone so we can overwrite with undefined in when we mock kcContext
|
||||
|
||||
### **4.2.5** (2021-11-07)
|
||||
|
||||
- Better debugging experience with user profile
|
||||
|
||||
### **4.2.4** (2021-11-01)
|
||||
|
||||
- Better autoComplete typings
|
||||
|
||||
### **4.2.3** (2021-11-01)
|
||||
|
||||
- Make it more easy to understand that error in the log are expected
|
||||
|
||||
### **4.2.2** (2021-10-27)
|
||||
|
||||
- Replace 'path' by 'browserify-path' #47
|
||||
|
||||
### **4.2.1** (2021-10-26)
|
||||
|
||||
- useFormValidationSlice: update when params have changed
|
||||
- Explains that the password can't be validated
|
||||
|
||||
## **4.2.0** (2021-10-26)
|
||||
|
||||
- Export types definitions for Attribue and Validator
|
||||
|
||||
## **4.1.0** (2021-10-26)
|
||||
|
||||
- Document what's new in v4
|
||||
|
||||
# **4.0.0** (2021-10-26)
|
||||
|
||||
- fix RegisterUserProfile password confirmation field
|
||||
- Much better support for frontend field validation
|
||||
- Fix css injection order
|
||||
- Makes the download output predictable. This fixes the case where GitHub redirects and wget was trying to download a filename called "15.0.2", and then unzip wouldn't pick it up.
|
||||
Changes wget to curl because curl is awesome. -L is to follow the GitHub redirects.
|
||||
- Remove duplicates
|
||||
|
||||
### **3.0.2** (2021-10-18)
|
||||
|
||||
- Scan deeper to retreive user attribute
|
||||
|
217
README.md
217
README.md
@ -5,11 +5,21 @@
|
||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
||||
<br>
|
||||
<br>
|
||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=develop">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
||||
<img src="https://img.shields.io/npm/dw/keycloakify">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||
<a href="https://github.com/garronej/keycloakify/actions">
|
||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||
</a>
|
||||
<a href="https://bundlephobia.com/package/keycloakify">
|
||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/keycloakify">
|
||||
<img src="https://img.shields.io/npm/dw/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/InseeFrLab/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||
</a>
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
@ -20,31 +30,6 @@
|
||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||
</p>
|
||||
|
||||
**NEW in v3**
|
||||
|
||||
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
||||
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
||||
[when passing params from the app to the login page](https://github.com/InseeFrLab/keycloakify#implement-context-persistence-optional).
|
||||
|
||||
**NEW in v2.5**
|
||||
|
||||
- User Profile ([`register-user-profile.ftl`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/RegisterUserProfile.tsx))
|
||||
is now supported! 🎉
|
||||
It enables to [define, from the admin console](https://user-images.githubusercontent.com/6702424/136872461-1f5b64ef-d2ef-4c6b-bb8d-07d4729552b3.png),
|
||||
what information you want to collect on your users in the register page and to validate inputs
|
||||
[**on the frontend**, in realtime](https://github.com/InseeFrLab/keycloakify/blob/6dca6a93d8cfe634ee4d8574ad0c091641220092/src/lib/getKcContext/KcContextBase.ts#L225-L261)!
|
||||
NOTE: User profile is only available in Keycloak 15 and it's a beta feature that
|
||||
[needs to be enabled when launching keycloak](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/build-keycloak-theme.ts#L116-L117) and [enabled in the console](https://user-images.githubusercontent.com/6702424/136874428-b071d614-c7f7-440d-9b2e-670faadc0871.png).
|
||||
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
||||
- Test container now uses Keycloak version `15.0.2`.
|
||||
|
||||
**NEW in v2**
|
||||
|
||||
- It's now possible to implement custom `.ftl` pages.
|
||||
- Support for Keycloak plugins that introduce non standard ftl values.
|
||||
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
|
||||
|
||||
# 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.
|
||||
@ -85,42 +70,49 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
|
||||
- [Advanced pages configuration](#advanced-pages-configuration)
|
||||
- [Hot reload](#hot-reload)
|
||||
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
|
||||
- [User profile and frontend form validation](#user-profile-and-frontend-form-validation)
|
||||
- [Support for Terms and conditions](#support-for-terms-and-conditions)
|
||||
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
|
||||
- [GitHub Actions](#github-actions)
|
||||
- [Limitations](#limitations)
|
||||
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported)
|
||||
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-thesrc-dir)
|
||||
- [`@font-face` importing fonts from the `src/` dir](#font-face-importing-fonts-from-the-src-dir)
|
||||
- [Example of setup that **won't** work](#example-of-setup-that-wont-work)
|
||||
- [Possible workarounds](#possible-workarounds)
|
||||
- [Implement context persistence (optional)](#implement-context-persistence-optional)
|
||||
- [Kickstart video](#kickstart-video)
|
||||
- [About the errors related to `objectToJson` in Keycloak logs.](#about-the-errors-related-to-objecttojson-in-keycloak-logs)
|
||||
- [FTL errors related to `ftl_object_to_js_code_declaring_an_object` in Keycloak logs.](#ftl-errors-related-to-ftl_object_to_js_code_declaring_an_object-in-keycloak-logs)
|
||||
- [Adding custom message (to `i18n/useKcMessage.tsx`)](#adding-custom-message-to-i18nusekcmessagetsx)
|
||||
- [Email domain whitelist](#email-domain-whitelist)
|
||||
- [Changelog highlights](#changelog-highlights)
|
||||
- [v4](#v4)
|
||||
- [v3](#v3)
|
||||
- [v2.5](#v25)
|
||||
- [v2](#v2)
|
||||
|
||||
# Requirements
|
||||
|
||||
On Windows OS you'll have to use [WSL](https://docs.microsoft.com/en-us/windows/wsl/install-win10). More info [here](https://github.com/InseeFrLab/keycloakify/issues/54%23issuecomment-984834217)
|
||||
|
||||
Tested with the following Keycloak versions:
|
||||
|
||||
- [11.0.3](https://hub.docker.com/layers/jboss/keycloak/11.0.3/images/sha256-4438f1e51c1369371cb807dffa526e1208086b3ebb9cab009830a178de949782?context=explore)
|
||||
- [12.0.4](https://hub.docker.com/layers/jboss/keycloak/12.0.4/images/sha256-67e0c88e69bd0c7aef972c40bdeb558a974013a28b3668ca790ed63a04d70584?context=explore)
|
||||
- [15.0.2](https://hub.docker.com/layers/jboss/keycloak/15.0.2/images/sha256-d8ed1ee5df42a178c341f924377da75db49eab08ea9f058ff39a8ed7ee05ec93?context=explore)
|
||||
- [16.1.0](https://hub.docker.com/layers/jboss/keycloak/16.1.0/images/sha256-6ecb9492224c6cfbb55d43f64a5ab634145d8cc1eba14eae8c37e3afde89546e?context=explore)
|
||||
|
||||
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
|
||||
(before you customize it) will always be the ones of Keycloak v11.
|
||||
|
||||
This tool assumes you are bundling your app with Webpack (tested with 4.44.2) .
|
||||
This tool assumes you are bundling your app with Webpack (tested with the versions that ships with CRA v4.44.2 and v5.0.0) .
|
||||
It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
|
||||
and a `build/static/` directory generated by webpack.
|
||||
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432)
|
||||
|
||||
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
|
||||
|
||||
- `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `wget`, `unzip` are assumed to be available.
|
||||
- `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).
|
||||
- `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `curl`, `unzip` are assumed to be available.
|
||||
- `docker` must be up and running when running `start_keycloak_testing_container.sh` (Instructions provided after running `yarn keycloak`).
|
||||
|
||||
## My framework doesn’t seem to be supported, what can I do?
|
||||
|
||||
@ -137,7 +129,7 @@ separate module. Checkout [ts_ci](https://github.com/garronej/ts_ci), it can hel
|
||||
## Setting up the build tool
|
||||
|
||||
```bash
|
||||
yarn add keycloakify @emotion/react tss-react powerhooks
|
||||
yarn add keycloakify @emotion/react
|
||||
```
|
||||
|
||||
[`package.json`](https://github.com/garronej/keycloakify-demo-app/blob/main/package.json)
|
||||
@ -165,27 +157,23 @@ your index should look something like:
|
||||
`src/index.tsx`
|
||||
|
||||
```tsx
|
||||
import { App } from "./<wherever>/App";
|
||||
import {
|
||||
KcApp,
|
||||
defaultKcProps,
|
||||
getKcContext
|
||||
} from "keycloakify";
|
||||
import { css } from "tss-react/@emotion/css";
|
||||
import { App } from "./<wherever>/App";
|
||||
import { KcApp, defaultKcProps, getKcContext } from "keycloakify";
|
||||
import { css } from "tss-react/@emotion/css";
|
||||
|
||||
const { kcContext } = getKcContext();
|
||||
|
||||
const myClassName = css({ "color": "red" });
|
||||
|
||||
reactDom.render(
|
||||
<KcApp
|
||||
kcContext={kcContext}
|
||||
{...{
|
||||
...defaultKcProps,
|
||||
"kcHeaderWrapperClass": myClassName
|
||||
}}
|
||||
/>
|
||||
document.getElementById("root")
|
||||
<KcApp
|
||||
kcContext={kcContext}
|
||||
{...{
|
||||
...defaultKcProps,
|
||||
"kcHeaderWrapperClass": myClassName,
|
||||
}}
|
||||
/>,
|
||||
document.getElementById("root"),
|
||||
);
|
||||
```
|
||||
|
||||
@ -226,8 +214,8 @@ reactDom.render(
|
||||
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
|
||||
</p>
|
||||
|
||||
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-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) )
|
||||
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx)
|
||||
(the [index.tsx](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) )
|
||||
and the result you can expect:
|
||||
|
||||
<p align="center">
|
||||
@ -242,7 +230,7 @@ If you want to go beyond only customizing the CSS you can re-implement some of t
|
||||
pages or even add new ones.
|
||||
|
||||
If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel).
|
||||
If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/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/ui/components/KcApp).
|
||||
The web app is in production [here](https://datalab.sspcloud.fr).
|
||||
|
||||
Main takeaways are:
|
||||
@ -265,8 +253,6 @@ WARNING: If you chose to go this way use:
|
||||
}
|
||||
```
|
||||
|
||||
in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app.
|
||||
|
||||
### Hot reload
|
||||
|
||||
Rebuild the theme each time you make a change to see the result is not practical.
|
||||
@ -304,7 +290,7 @@ If you are specifically building a theme to integrate with an app or a website t
|
||||
to first browse unauthenticated before logging in, you will get a significant
|
||||
performance boost if you jump through those hoops:
|
||||
|
||||
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2)
|
||||
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2) or in a `public/CNAME` file. [ex](https://github.com/garronej/keycloakify-demo-app/blob/main/public/CNAME).
|
||||
- Build the theme using `npx build-keycloak-theme --external-assets` [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/.github/workflows/ci.yaml#L21)
|
||||
- Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app.
|
||||
- Make sure not to build your app and the keycloak theme separately
|
||||
@ -313,6 +299,34 @@ performance boost if you jump through those hoops:
|
||||
|
||||
Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-app#about-keycloakify)
|
||||
|
||||
# User profile and frontend form validation
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/keycloakify_fontend_validation.mp4">
|
||||
<img src="https://user-images.githubusercontent.com/6702424/138880146-6fef3280-c4a5-46d2-bbb3-8b9598c057a5.gif">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
NOTE: In reality the regexp used in this gif doesn't work server side, the regexp pattern should be `^[^@]@gmail\.com$` 😬.
|
||||
|
||||
User Profile is a Keycloak feature that enables to
|
||||
[define, from the admin console](https://user-images.githubusercontent.com/6702424/136872461-1f5b64ef-d2ef-4c6b-bb8d-07d4729552b3.png),
|
||||
what information you want to collect on your users in the register page and to validate inputs
|
||||
[**on the frontend**, in realtime](https://github.com/InseeFrLab/keycloakify/blob/6dca6a93d8cfe634ee4d8574ad0c091641220092/src/lib/getKcContext/KcContextBase.ts#L225-L261)!
|
||||
|
||||
NOTE: User profile is only available in Keycloak 15 and it's a beta feature that
|
||||
[needs to be enabled when launching keycloak](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/build-keycloak-theme.ts#L116-L117)
|
||||
and [enabled in the console](https://user-images.githubusercontent.com/6702424/136874428-b071d614-c7f7-440d-9b2e-670faadc0871.png).
|
||||
|
||||
Keycloakify, in [`register-user-profile.ftl`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/RegisterUserProfile.tsx),
|
||||
provides frontend validation out of the box.
|
||||
|
||||
For implementing your own `register-user-profile.ftl` page, you can use [`import { useFormValidationSlice } from "keycloakify";`](https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/useFormValidationSlice.tsx).
|
||||
Find usage example [`here`](https://github.com/InseeFrLab/keycloakify/blob/d3a07edfcb3739e30032dc96fc2a55944dfc3387/src/lib/components/RegisterUserProfile.tsx#L79-L112).
|
||||
|
||||
As for right now [it's not possible to define a pattern for the password](https://keycloak.discourse.group/t/make-password-policies-available-to-freemarker/11632)
|
||||
from the admin console. You can however pass validators for it to the `useFormValidationSlice` function.
|
||||
|
||||
# Support for Terms and conditions
|
||||
|
||||
[Many organizations have a requirement that when a new user logs in for the first time, they need to agree to the terms and conditions of the website.](https://www.keycloak.org/docs/4.8/server_admin/#terms-and-conditions).
|
||||
@ -343,7 +357,7 @@ the building and publishing of the theme (the .jar file).
|
||||
You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
|
||||
(This isn't recommended anyway).
|
||||
|
||||
## `@font-face` importing fonts from the `src/` dir
|
||||
## `@font-face` importing fonts from the `src/` dir
|
||||
|
||||
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
|
||||
this limitation doesn't apply, you can import fonts however you see fit.
|
||||
@ -359,7 +373,8 @@ this limitation doesn't apply, you can import fonts however you see fit.
|
||||
- If it is possible, use Google Fonts or any other font provider.
|
||||
- If you want to host your font recommended approach is to move your fonts into the `public`
|
||||
directory and to place your `@font-face` statements in the `public/index.html`.
|
||||
Example [here](https://github.com/InseeFrLab/onyxia-ui/blob/0e3a04610cfe872ca71dad59e05ced8f785dee4b/public/index.html#L6-L51).
|
||||
Example [here](https://github.com/garronej/keycloakify-demo-app/blob/9aa2dbaec28a7786d6b2983c9a59d393dec1b2d6/public/index.html#L27-L73)
|
||||
(and the font are [here](https://github.com/garronej/keycloakify-demo-app/tree/main/public/fonts/WorkSans)).
|
||||
- You can also [use non relative url](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/src/fonts.scss#L16) but don't forget [`Access-Control-Allow-Origin`](https://github.com/garronej/keycloakify-demo-app/blob/2de8a9eb6f5de9c94f9cd3991faad0377e63268c/nginx.conf#L17-L19).
|
||||
|
||||
# Implement context persistence (optional)
|
||||
@ -419,31 +434,29 @@ keycloakInstance.init({
|
||||
|
||||
If you really want to go the extra miles and avoid having the white
|
||||
flash of the blank html before the js bundle have been evaluated
|
||||
[here is a snippet](https://github.com/InseeFrLab/onyxia-ui/blob/a77eb502870cfe6878edd0d956c646d28746d053/public/index.html#L5-L54) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
||||
[here is a snippet](https://github.com/InseeFrLab/onyxia-web/blob/e1c1f309aaa3d5f860df39ba0b75cce89c88a9de/public/index.html#L117-L166) that you can place in your `public/index.html` if you are using `powerhooks/useGlobalState`.
|
||||
|
||||
# Kickstart video
|
||||
|
||||
_NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded_
|
||||
[](https://youtu.be/xTz0Rj7i2v8)
|
||||
|
||||
# About the errors related to `objectToJson` in Keycloak logs.
|
||||
# FTL errors related to `ftl_object_to_js_code_declaring_an_object` in Keycloak logs.
|
||||
|
||||
The logs of your keycloak server will always show this kind of errors every time a client request a page:
|
||||
If you ever encounter one of these errors:
|
||||
|
||||
```log
|
||||
FTL stack trace ("~" means nesting-related):
|
||||
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson" at line 70, column 21]
|
||||
- Reached through: @compress [in template "login.ftl" in macro "objectToJson" at line 36, column 5]
|
||||
- Reached through: @objectToJson object=value depth=(dep... [in template "login.ftl" in macro "objectToJson" at line 81, column 27]
|
||||
- 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]
|
||||
- Failed at: #local value = object[key] [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 70, column 21]
|
||||
- Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
|
||||
- Reached through: @ftl_object_to_js_code_declaring_an_object object=value depth=(dep... [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 81, column 27]
|
||||
- Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
|
||||
- Reached through: @ftl_object_to_js_code_declaring_an_object object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
|
||||
```
|
||||
|
||||
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.
|
||||
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 just noise, they can be safely ignored.
|
||||
You can, however, and are encouraged to, report any that you would spot.
|
||||
Just open an issue about it and I will release a patched version of Keycloakify in the better delays.
|
||||
|
||||
# Adding custom message (to `i18n/useKcMessage.tsx`)
|
||||
|
||||
@ -453,5 +466,61 @@ This approach is a bit hacky as it doesn't provide type safety but it works.
|
||||
|
||||
# Email domain whitelist
|
||||
|
||||
NOTE: This have been kind of deprecated by [user attribute](#user-profile-and-frontend-form-validation) you could
|
||||
use a pattern [like this one](https://github.com/InseeFrLab/onyxia-web/blob/f1206e0329b3b8d401ca7bffa95ca9c213cb190a/src/app/components/KcApp/kcContext.ts#L106) to whitelist email domains.
|
||||
|
||||
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
|
||||
and `kcRegisterContext["authorizedMailDomains"]` to validate on.
|
||||
|
||||
# Changelog highlights
|
||||
|
||||
# v4.7.2
|
||||
|
||||
> WARNING: This is broken.
|
||||
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
|
||||
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||
|
||||
# v4.7.0
|
||||
|
||||
Register with user profile enabled: Out of the box `options` validator support.
|
||||
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
|
||||
|
||||
# v4.6.0
|
||||
|
||||
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
|
||||
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
|
||||
|
||||
## v4.5.3
|
||||
|
||||
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
|
||||
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
|
||||
with this new method your theme wont break on minor Keycloakify update.
|
||||
|
||||
## v4.3.0
|
||||
|
||||
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
|
||||
Every time a page is added it's a breaking change for non CSS-only theme.
|
||||
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
|
||||
|
||||
## v4
|
||||
|
||||
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
|
||||
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
|
||||
|
||||
## v3
|
||||
|
||||
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
||||
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
||||
[when passing params from the app to the login page](https://github.com/InseeFrLab/keycloakify#implement-context-persistence-optional).
|
||||
|
||||
## v2.5
|
||||
|
||||
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
||||
- Test container now uses Keycloak version `15.0.2`.
|
||||
|
||||
## v2
|
||||
|
||||
- It's now possible to implement custom `.ftl` pages.
|
||||
- Support for Keycloak plugins that introduce non standard ftl values.
|
||||
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
|
||||
|
26
package.json
26
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "3.0.2",
|
||||
"version": "4.7.3",
|
||||
"description": "Keycloak theme generator for Reacts app",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -55,33 +55,31 @@
|
||||
],
|
||||
"homepage": "https://github.com/garronej/keycloakify",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0",
|
||||
"tss-react": "^1.1.0",
|
||||
"powerhooks": "^0.9.6",
|
||||
"@emotion/react": "^11.4.1"
|
||||
"@emotion/react": "^11.4.1",
|
||||
"react": "^16.8.0 || ^17.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tss-react": "^1.1.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"powerhooks": "^0.9.6",
|
||||
"@types/node": "^10.0.0",
|
||||
"@types/react": "^17.0.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.0",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "^17.0.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.2.3",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^11.0.0",
|
||||
"prettier": "^2.3.0"
|
||||
"typescript": "^4.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"evt": "2.0.0-beta.38",
|
||||
"evt": "2.0.0-beta.39",
|
||||
"minimal-polyfills": "^2.2.1",
|
||||
"path": "^0.12.7",
|
||||
"path-browserify": "^1.0.1",
|
||||
"react-markdown": "^5.0.3",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"tsafe": "^0.8.1"
|
||||
"tsafe": "^0.9.0",
|
||||
"tss-react": "^3.5.2",
|
||||
"powerhooks": "^0.14.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
export const keycloakVersions = ["11.0.3", "15.0.2"] as const;
|
||||
export const keycloakVersions = ["11.0.3", "15.0.2", "16.1.0"] as const;
|
||||
|
||||
export type KeycloakVersion = typeof keycloakVersions[number];
|
||||
|
@ -4,6 +4,8 @@ import { join as pathJoin, relative as pathRelative, basename as pathBasename }
|
||||
import * as child_process from "child_process";
|
||||
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
||||
import { URL } from "url";
|
||||
import * as fs from "fs";
|
||||
import { getIsM1 } from "../tools/isM1";
|
||||
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
@ -41,7 +43,17 @@ export function main() {
|
||||
const url = (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
return homepage === undefined ? undefined : new URL(homepage);
|
||||
if (homepage !== undefined) {
|
||||
return new URL(homepage);
|
||||
}
|
||||
|
||||
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
||||
|
||||
if (fs.existsSync(cnameFilePath)) {
|
||||
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
return {
|
||||
@ -61,7 +73,7 @@ export function main() {
|
||||
extraPagesId,
|
||||
extraThemeProperties,
|
||||
//We have to leave it at that otherwise we break our default theme.
|
||||
//Problem is that we can't guarantee that the the old resources common
|
||||
//Problem is that we can't guarantee that the the old resources
|
||||
//will still be available on the newer keycloak version.
|
||||
"keycloakVersion": "11.0.3",
|
||||
});
|
||||
@ -80,7 +92,10 @@ export function main() {
|
||||
generateDebugFiles({
|
||||
keycloakThemeBuildingDirPath,
|
||||
themeName,
|
||||
"keycloakVersion": "15.0.2",
|
||||
//We want, however to test in a container running the latest Keycloak version
|
||||
//Except on M1 where we can't use the default image and we only have
|
||||
//https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658
|
||||
"keycloakVersion": getIsM1() ? "15.0.2" : "16.1.0",
|
||||
});
|
||||
|
||||
console.log(
|
||||
@ -121,11 +136,14 @@ export function main() {
|
||||
"",
|
||||
`👉 $ ./${pathRelative(reactProjectDirPath, 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 running: ",
|
||||
"- Log into the admin console 👉 http://localhost:8080 username: admin, password: admin 👈",
|
||||
'- Create a realm named "myrealm"',
|
||||
'- Create a client with id "myclient" and root url: "https://www.keycloak.org/app/"',
|
||||
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
|
||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||
"",
|
||||
"Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈",
|
||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
@ -1,17 +1,23 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import type { KeycloakVersion } from "../../KeycloakVersion";
|
||||
import { getIsM1 } from "../../tools/isM1";
|
||||
|
||||
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
||||
|
||||
/** Files for being able to run a hot reload keycloak container */
|
||||
export function generateDebugFiles(params: { keycloakVersion: "11.0.3" | "15.0.2"; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
||||
export function generateDebugFiles(params: { keycloakVersion: KeycloakVersion; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
||||
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
||||
Buffer.from(
|
||||
[
|
||||
`FROM jboss/keycloak:${keycloakVersion}`,
|
||||
`FROM ${
|
||||
getIsM1()
|
||||
? "eduardosanzb/keycloak@sha256:b1f5bc674eaff6f4e7b37808b9863440310ff93c282fc9bff812377be48bf519"
|
||||
: `jboss/keycloak:${keycloakVersion}`
|
||||
}`,
|
||||
"",
|
||||
"USER root",
|
||||
"",
|
||||
|
@ -0,0 +1,652 @@
|
||||
<?xml version="1.0" ?>
|
||||
|
||||
<server xmlns="urn:jboss:domain:19.0">
|
||||
<extensions>
|
||||
<extension module="org.jboss.as.clustering.infinispan"/>
|
||||
<extension module="org.jboss.as.clustering.jgroups"/>
|
||||
<extension module="org.jboss.as.connector"/>
|
||||
<extension module="org.jboss.as.deployment-scanner"/>
|
||||
<extension module="org.jboss.as.ee"/>
|
||||
<extension module="org.jboss.as.ejb3"/>
|
||||
<extension module="org.jboss.as.jaxrs"/>
|
||||
<extension module="org.jboss.as.jmx"/>
|
||||
<extension module="org.jboss.as.jpa"/>
|
||||
<extension module="org.jboss.as.logging"/>
|
||||
<extension module="org.jboss.as.mail"/>
|
||||
<extension module="org.jboss.as.modcluster"/>
|
||||
<extension module="org.jboss.as.naming"/>
|
||||
<extension module="org.jboss.as.remoting"/>
|
||||
<extension module="org.jboss.as.transactions"/>
|
||||
<extension module="org.jboss.as.weld"/>
|
||||
<extension module="org.keycloak.keycloak-server-subsystem"/>
|
||||
<extension module="org.wildfly.extension.bean-validation"/>
|
||||
<extension module="org.wildfly.extension.core-management"/>
|
||||
<extension module="org.wildfly.extension.elytron"/>
|
||||
<extension module="org.wildfly.extension.health"/>
|
||||
<extension module="org.wildfly.extension.io"/>
|
||||
<extension module="org.wildfly.extension.metrics"/>
|
||||
<extension module="org.wildfly.extension.request-controller"/>
|
||||
<extension module="org.wildfly.extension.security.manager"/>
|
||||
<extension module="org.wildfly.extension.undertow"/>
|
||||
</extensions>
|
||||
<management>
|
||||
<audit-log>
|
||||
<formatters>
|
||||
<json-formatter name="json-formatter"/>
|
||||
</formatters>
|
||||
<handlers>
|
||||
<file-handler name="file" formatter="json-formatter" path="audit-log.log" relative-to="jboss.server.data.dir"/>
|
||||
</handlers>
|
||||
<logger log-boot="true" log-read-only="false" enabled="false">
|
||||
<handlers>
|
||||
<handler name="file"/>
|
||||
</handlers>
|
||||
</logger>
|
||||
</audit-log>
|
||||
<management-interfaces>
|
||||
<http-interface http-authentication-factory="management-http-authentication">
|
||||
<http-upgrade enabled="true" sasl-authentication-factory="management-sasl-authentication"/>
|
||||
<socket-binding http="management-http"/>
|
||||
</http-interface>
|
||||
</management-interfaces>
|
||||
<access-control provider="simple">
|
||||
<role-mapping>
|
||||
<role name="SuperUser">
|
||||
<include>
|
||||
<user name="$local"/>
|
||||
</include>
|
||||
</role>
|
||||
</role-mapping>
|
||||
</access-control>
|
||||
</management>
|
||||
<profile>
|
||||
<subsystem xmlns="urn:jboss:domain:logging:8.0">
|
||||
<console-handler name="CONSOLE">
|
||||
<formatter>
|
||||
<named-formatter name="COLOR-PATTERN"/>
|
||||
</formatter>
|
||||
</console-handler>
|
||||
<logger category="com.arjuna">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="io.jaegertracing.Configuration">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.jboss.as.config">
|
||||
<level name="DEBUG"/>
|
||||
</logger>
|
||||
<logger category="sun.rmi">
|
||||
<level name="WARN"/>
|
||||
</logger>
|
||||
<logger category="org.keycloak">
|
||||
<level name="${env.KEYCLOAK_LOGLEVEL:INFO}"/>
|
||||
</logger>
|
||||
<root-logger>
|
||||
<level name="${env.ROOT_LOGLEVEL:INFO}"/>
|
||||
<handlers>
|
||||
<handler name="CONSOLE"/>
|
||||
</handlers>
|
||||
</root-logger>
|
||||
<formatter name="PATTERN">
|
||||
<pattern-formatter pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
<formatter name="COLOR-PATTERN">
|
||||
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%e%n"/>
|
||||
</formatter>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:bean-validation:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:datasources:6.0">
|
||||
<datasources>
|
||||
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true" statistics-enabled="${wildfly.datasources.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<connection-url>jdbc:h2:${jboss.server.data.dir}/keycloak;AUTO_SERVER=TRUE</connection-url>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
<user-name>sa</user-name>
|
||||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<drivers>
|
||||
<driver name="h2" module="com.h2database.h2">
|
||||
<xa-datasource-class>org.h2.jdbcx.JdbcDataSource</xa-datasource-class>
|
||||
</driver>
|
||||
</drivers>
|
||||
</datasources>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:deployment-scanner:2.0">
|
||||
<deployment-scanner path="deployments" relative-to="jboss.server.base.dir" scan-interval="5000" runtime-failure-causes-rollback="${jboss.deployment.scanner.rollback.on.failure:false}"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ee:6.0">
|
||||
<spec-descriptor-property-replacement>false</spec-descriptor-property-replacement>
|
||||
<concurrent>
|
||||
<context-services>
|
||||
<context-service name="default" jndi-name="java:jboss/ee/concurrency/context/default" use-transaction-setup-provider="true"/>
|
||||
</context-services>
|
||||
<managed-thread-factories>
|
||||
<managed-thread-factory name="default" jndi-name="java:jboss/ee/concurrency/factory/default" context-service="default"/>
|
||||
</managed-thread-factories>
|
||||
<managed-executor-services>
|
||||
<managed-executor-service name="default" jndi-name="java:jboss/ee/concurrency/executor/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="5000"/>
|
||||
</managed-executor-services>
|
||||
<managed-scheduled-executor-services>
|
||||
<managed-scheduled-executor-service name="default" jndi-name="java:jboss/ee/concurrency/scheduler/default" context-service="default" hung-task-termination-period="0" hung-task-threshold="60000" keepalive-time="3000"/>
|
||||
</managed-scheduled-executor-services>
|
||||
</concurrent>
|
||||
<default-bindings context-service="java:jboss/ee/concurrency/context/default" datasource="java:jboss/datasources/ExampleDS" managed-executor-service="java:jboss/ee/concurrency/executor/default" managed-scheduled-executor-service="java:jboss/ee/concurrency/scheduler/default" managed-thread-factory="java:jboss/ee/concurrency/factory/default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:ejb3:9.0">
|
||||
<session-bean>
|
||||
<stateless>
|
||||
<bean-instance-pool-ref pool-name="slsb-strict-max-pool"/>
|
||||
</stateless>
|
||||
<stateful default-access-timeout="5000" cache-ref="distributable" passivation-disabled-cache-ref="simple"/>
|
||||
<singleton default-access-timeout="5000"/>
|
||||
</session-bean>
|
||||
<pools>
|
||||
<bean-instance-pools>
|
||||
<strict-max-pool name="mdb-strict-max-pool" derive-size="from-cpu-count" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
<strict-max-pool name="slsb-strict-max-pool" derive-size="from-worker-pools" instance-acquisition-timeout="5" instance-acquisition-timeout-unit="MINUTES"/>
|
||||
</bean-instance-pools>
|
||||
</pools>
|
||||
<caches>
|
||||
<cache name="simple"/>
|
||||
<cache name="distributable" passivation-store-ref="infinispan" aliases="passivating clustered"/>
|
||||
</caches>
|
||||
<passivation-stores>
|
||||
<passivation-store name="infinispan" cache-container="ejb" max-size="10000"/>
|
||||
</passivation-stores>
|
||||
<async thread-pool-name="default"/>
|
||||
<timer-service thread-pool-name="default" default-data-store="default-file-store">
|
||||
<data-stores>
|
||||
<file-data-store name="default-file-store" path="timer-service-data" relative-to="jboss.server.data.dir"/>
|
||||
</data-stores>
|
||||
</timer-service>
|
||||
<remote cluster="ejb" connectors="http-remoting-connector" thread-pool-name="default">
|
||||
<channel-creation-options>
|
||||
<option name="MAX_OUTBOUND_MESSAGES" value="1234" type="remoting"/>
|
||||
</channel-creation-options>
|
||||
</remote>
|
||||
<thread-pools>
|
||||
<thread-pool name="default">
|
||||
<max-threads count="10"/>
|
||||
<keepalive-time time="60" unit="seconds"/>
|
||||
</thread-pool>
|
||||
</thread-pools>
|
||||
<default-security-domain value="other"/>
|
||||
<application-security-domains>
|
||||
<application-security-domain name="other" security-domain="ApplicationDomain"/>
|
||||
</application-security-domains>
|
||||
<default-missing-method-permissions-deny-access value="true"/>
|
||||
<statistics enabled="${wildfly.ejb3.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<log-system-exceptions value="true"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:elytron:15.0" final-providers="combined-providers" disallowed-providers="OracleUcrypto">
|
||||
<providers>
|
||||
<aggregate-providers name="combined-providers">
|
||||
<providers name="elytron"/>
|
||||
<providers name="openssl"/>
|
||||
</aggregate-providers>
|
||||
<provider-loader name="elytron" module="org.wildfly.security.elytron"/>
|
||||
<provider-loader name="openssl" module="org.wildfly.openssl"/>
|
||||
</providers>
|
||||
<audit-logging>
|
||||
<file-audit-log name="local-audit" path="audit.log" relative-to="jboss.server.log.dir" format="JSON"/>
|
||||
</audit-logging>
|
||||
<security-domains>
|
||||
<security-domain name="ManagementDomain" default-realm="ManagementRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ManagementRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local" role-mapper="super-user-mapper"/>
|
||||
</security-domain>
|
||||
<security-domain name="ApplicationDomain" default-realm="ApplicationRealm" permission-mapper="default-permission-mapper">
|
||||
<realm name="ApplicationRealm" role-decoder="groups-to-roles"/>
|
||||
<realm name="local"/>
|
||||
</security-domain>
|
||||
</security-domains>
|
||||
<security-realms>
|
||||
<identity-realm name="local" identity="$local"/>
|
||||
<properties-realm name="ApplicationRealm">
|
||||
<users-properties path="application-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ApplicationRealm"/>
|
||||
<groups-properties path="application-roles.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
<properties-realm name="ManagementRealm">
|
||||
<users-properties path="mgmt-users.properties" relative-to="jboss.server.config.dir" digest-realm-name="ManagementRealm"/>
|
||||
<groups-properties path="mgmt-groups.properties" relative-to="jboss.server.config.dir"/>
|
||||
</properties-realm>
|
||||
</security-realms>
|
||||
<mappers>
|
||||
<simple-permission-mapper name="default-permission-mapper" mapping-mode="first">
|
||||
<permission-mapping>
|
||||
<principal name="anonymous"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
<permission-mapping match-all="true">
|
||||
<permission-set name="login-permission"/>
|
||||
<permission-set name="default-permissions"/>
|
||||
</permission-mapping>
|
||||
</simple-permission-mapper>
|
||||
<constant-realm-mapper name="local" realm-name="local"/>
|
||||
<simple-role-decoder name="groups-to-roles" attribute="groups"/>
|
||||
<constant-role-mapper name="super-user-mapper">
|
||||
<role name="SuperUser"/>
|
||||
</constant-role-mapper>
|
||||
</mappers>
|
||||
<permission-sets>
|
||||
<permission-set name="login-permission">
|
||||
<permission class-name="org.wildfly.security.auth.permission.LoginPermission"/>
|
||||
</permission-set>
|
||||
<permission-set name="default-permissions">
|
||||
<permission class-name="org.wildfly.extension.batch.jberet.deployment.BatchPermission" module="org.wildfly.extension.batch.jberet" target-name="*"/>
|
||||
<permission class-name="org.wildfly.transaction.client.RemoteTransactionPermission" module="org.wildfly.transaction.client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
<permission class-name="org.jboss.ejb.client.RemoteEJBPermission" module="org.jboss.ejb-client"/>
|
||||
</permission-set>
|
||||
</permission-sets>
|
||||
<http>
|
||||
<http-authentication-factory name="management-http-authentication" security-domain="ManagementDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="DIGEST">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<http-authentication-factory name="application-http-authentication" security-domain="ApplicationDomain" http-server-mechanism-factory="global">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="BASIC">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</http-authentication-factory>
|
||||
<provider-http-server-mechanism-factory name="global"/>
|
||||
</http>
|
||||
<sasl>
|
||||
<sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ManagementRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
|
||||
<mechanism-configuration>
|
||||
<mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
|
||||
<mechanism mechanism-name="DIGEST-MD5">
|
||||
<mechanism-realm realm-name="ApplicationRealm"/>
|
||||
</mechanism>
|
||||
</mechanism-configuration>
|
||||
</sasl-authentication-factory>
|
||||
<configurable-sasl-server-factory name="configured" sasl-server-factory="elytron">
|
||||
<properties>
|
||||
<property name="wildfly.sasl.local-user.default-user" value="$local"/>
|
||||
<property name="wildfly.sasl.local-user.challenge-path" value="${jboss.server.temp.dir}/auth"/>
|
||||
</properties>
|
||||
</configurable-sasl-server-factory>
|
||||
<mechanism-provider-filtering-sasl-server-factory name="elytron" sasl-server-factory="global">
|
||||
<filters>
|
||||
<filter provider-name="WildFlyElytron"/>
|
||||
</filters>
|
||||
</mechanism-provider-filtering-sasl-server-factory>
|
||||
<provider-sasl-server-factory name="global"/>
|
||||
</sasl>
|
||||
<tls>
|
||||
<key-stores>
|
||||
<key-store name="applicationKS">
|
||||
<credential-reference clear-text="password"/>
|
||||
<implementation type="JKS"/>
|
||||
<file path="application.keystore" relative-to="jboss.server.config.dir"/>
|
||||
</key-store>
|
||||
</key-stores>
|
||||
<key-managers>
|
||||
<key-manager name="applicationKM" key-store="applicationKS" generate-self-signed-certificate-host="localhost">
|
||||
<credential-reference clear-text="password"/>
|
||||
</key-manager>
|
||||
</key-managers>
|
||||
<server-ssl-contexts>
|
||||
<server-ssl-context name="applicationSSC" key-manager="applicationKM"/>
|
||||
</server-ssl-contexts>
|
||||
</tls>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:health:1.0" security-enabled="false"/>
|
||||
<subsystem xmlns="urn:jboss:domain:infinispan:13.0">
|
||||
<cache-container name="ejb" default-cache="dist" marshaller="PROTOSTREAM" aliases="sfsb" modules="org.wildfly.clustering.ejb.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
</cache-container>
|
||||
<cache-container name="keycloak" marshaller="JBOSS" modules="org.keycloak.keycloak-model-infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="realms">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="users">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="authorization">
|
||||
<heap-memory size="10000"/>
|
||||
</local-cache>
|
||||
<local-cache name="keys">
|
||||
<heap-memory size="1000"/>
|
||||
<expiration max-idle="3600000"/>
|
||||
</local-cache>
|
||||
<replicated-cache name="work">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</replicated-cache>
|
||||
<distributed-cache name="sessions" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="authenticationSessions" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="offlineSessions" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="clientSessions" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="offlineClientSessions" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="loginFailures" owners="1">
|
||||
<expiration lifespan="900000000000000000"/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="actionTokens" owners="2">
|
||||
<heap-memory size="-1"/>
|
||||
<expiration interval="300000" lifespan="900000000000000000" max-idle="-1"/>
|
||||
</distributed-cache>
|
||||
</cache-container>
|
||||
<cache-container name="server" default-cache="default" marshaller="PROTOSTREAM" aliases="singleton cluster" modules="org.wildfly.clustering.server">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="default">
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
</cache-container>
|
||||
<cache-container name="web" default-cache="dist" marshaller="PROTOSTREAM" modules="org.wildfly.clustering.web.infinispan">
|
||||
<transport lock-timeout="60000"/>
|
||||
<replicated-cache name="sso">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
</replicated-cache>
|
||||
<distributed-cache name="dist">
|
||||
<locking isolation="REPEATABLE_READ"/>
|
||||
<transaction mode="BATCH"/>
|
||||
<file-store/>
|
||||
</distributed-cache>
|
||||
<distributed-cache name="routing"/>
|
||||
</cache-container>
|
||||
<cache-container name="hibernate" marshaller="JBOSS" modules="org.infinispan.hibernate-cache">
|
||||
<transport lock-timeout="60000"/>
|
||||
<local-cache name="local-query">
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</local-cache>
|
||||
<local-cache name="pending-puts">
|
||||
<expiration max-idle="60000"/>
|
||||
</local-cache>
|
||||
<invalidation-cache name="entity">
|
||||
<transaction mode="NON_XA"/>
|
||||
<heap-memory size="10000"/>
|
||||
<expiration max-idle="100000"/>
|
||||
</invalidation-cache>
|
||||
<replicated-cache name="timestamps"/>
|
||||
</cache-container>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:io:3.0">
|
||||
<worker name="default"/>
|
||||
<buffer-pool name="default"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jaxrs:2.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:jca:5.0">
|
||||
<archive-validation enabled="true" fail-on-error="true" fail-on-warn="false"/>
|
||||
<bean-validation enabled="true"/>
|
||||
<default-workmanager>
|
||||
<short-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</short-running-threads>
|
||||
<long-running-threads>
|
||||
<core-threads count="50"/>
|
||||
<queue-length count="50"/>
|
||||
<max-threads count="50"/>
|
||||
<keepalive-time time="10" unit="seconds"/>
|
||||
</long-running-threads>
|
||||
</default-workmanager>
|
||||
<cached-connection-manager/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jgroups:8.0">
|
||||
<channels default="ee">
|
||||
<channel name="ee" stack="udp" cluster="ejb"/>
|
||||
</channels>
|
||||
<stacks>
|
||||
<stack name="udp">
|
||||
<transport type="UDP" socket-binding="jgroups-udp"/>
|
||||
<protocol type="PING"/>
|
||||
<protocol type="MERGE3"/>
|
||||
<socket-protocol type="FD_SOCK" socket-binding="jgroups-udp-fd"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS"/>
|
||||
<protocol type="UFC"/>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG3"/>
|
||||
</stack>
|
||||
<stack name="tcp">
|
||||
<transport type="TCP" socket-binding="jgroups-tcp"/>
|
||||
<socket-protocol type="MPING" socket-binding="jgroups-mping"/>
|
||||
<protocol type="MERGE3"/>
|
||||
<socket-protocol type="FD_SOCK" socket-binding="jgroups-tcp-fd"/>
|
||||
<protocol type="FD_ALL"/>
|
||||
<protocol type="VERIFY_SUSPECT"/>
|
||||
<protocol type="pbcast.NAKACK2"/>
|
||||
<protocol type="UNICAST3"/>
|
||||
<protocol type="pbcast.STABLE"/>
|
||||
<protocol type="pbcast.GMS"/>
|
||||
<protocol type="MFC"/>
|
||||
<protocol type="FRAG3"/>
|
||||
</stack>
|
||||
</stacks>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jmx:1.3">
|
||||
<expose-resolved-model/>
|
||||
<expose-expression-model/>
|
||||
<remoting-connector/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:jpa:1.1">
|
||||
<jpa default-extended-persistence-inheritance="DEEP"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:keycloak-server:1.1">
|
||||
<web-context>auth</web-context>
|
||||
<providers>
|
||||
<provider>
|
||||
classpath:${jboss.home.dir}/providers/*
|
||||
</provider>
|
||||
</providers>
|
||||
<master-realm-name>master</master-realm-name>
|
||||
<scheduled-task-interval>900</scheduled-task-interval>
|
||||
<theme>
|
||||
<staticMaxAge>2592000</staticMaxAge>
|
||||
<cacheThemes>true</cacheThemes>
|
||||
<cacheTemplates>true</cacheTemplates>
|
||||
<welcomeTheme>${env.KEYCLOAK_WELCOME_THEME:keycloak}</welcomeTheme>
|
||||
<default>${env.KEYCLOAK_DEFAULT_THEME:keycloak}</default>
|
||||
<dir>${jboss.home.dir}/themes</dir>
|
||||
</theme>
|
||||
<spi name="eventsStore">
|
||||
<provider name="jpa" enabled="true">
|
||||
<properties>
|
||||
<property name="exclude-events" value="["REFRESH_TOKEN"]"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="userCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="userSessionPersister">
|
||||
<default-provider>jpa</default-provider>
|
||||
</spi>
|
||||
<spi name="timer">
|
||||
<default-provider>basic</default-provider>
|
||||
</spi>
|
||||
<spi name="connectionsHttpClient">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsJpa">
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="dataSource" value="java:jboss/datasources/KeycloakDS"/>
|
||||
<property name="initializeEmpty" value="true"/>
|
||||
<property name="migrationStrategy" value="update"/>
|
||||
<property name="migrationExport" value="${jboss.home.dir}/keycloak-database-update.sql"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="realmCache">
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="connectionsInfinispan">
|
||||
<default-provider>default</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="cacheContainer" value="java:jboss/infinispan/container/keycloak"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="jta-lookup">
|
||||
<default-provider>${keycloak.jta.lookup.provider:jboss}</default-provider>
|
||||
<provider name="jboss" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="publicKeyStorage">
|
||||
<provider name="infinispan" enabled="true">
|
||||
<properties>
|
||||
<property name="minTimeBetweenRequests" value="10"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
<spi name="x509cert-lookup">
|
||||
<default-provider>${keycloak.x509cert.lookup.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true"/>
|
||||
</spi>
|
||||
<spi name="hostname">
|
||||
<default-provider>${keycloak.hostname.provider:default}</default-provider>
|
||||
<provider name="default" enabled="true">
|
||||
<properties>
|
||||
<property name="frontendUrl" value="${keycloak.frontendUrl:}"/>
|
||||
<property name="forceBackendUrlToFrontendUrl" value="false"/>
|
||||
</properties>
|
||||
</provider>
|
||||
<provider name="fixed" enabled="true">
|
||||
<properties>
|
||||
<property name="hostname" value="${keycloak.hostname.fixed.hostname:localhost}"/>
|
||||
<property name="httpPort" value="${keycloak.hostname.fixed.httpPort:-1}"/>
|
||||
<property name="httpsPort" value="${keycloak.hostname.fixed.httpsPort:-1}"/>
|
||||
<property name="alwaysHttps" value="${keycloak.hostname.fixed.alwaysHttps:false}"/>
|
||||
</properties>
|
||||
</provider>
|
||||
</spi>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:mail:4.0">
|
||||
<mail-session name="default" jndi-name="java:jboss/mail/Default">
|
||||
<smtp-server outbound-socket-binding-ref="mail-smtp"/>
|
||||
</mail-session>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:wildfly:metrics:1.0" security-enabled="false" exposed-subsystems="*" prefix="${wildfly.metrics.prefix:wildfly}"/>
|
||||
<subsystem xmlns="urn:jboss:domain:modcluster:5.0">
|
||||
<proxy name="default" advertise-socket="modcluster" listener="ajp">
|
||||
<dynamic-load-provider>
|
||||
<load-metric type="cpu"/>
|
||||
</dynamic-load-provider>
|
||||
</proxy>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:naming:2.0">
|
||||
<remote-naming/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:remoting:4.0">
|
||||
<http-connector name="http-remoting-connector" connector-ref="default" sasl-authentication-factory="application-sasl-authentication"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:request-controller:1.0"/>
|
||||
<subsystem xmlns="urn:jboss:domain:security-manager:1.0">
|
||||
<deployment-permissions>
|
||||
<maximum-set>
|
||||
<permission class="java.security.AllPermission"/>
|
||||
</maximum-set>
|
||||
</deployment-permissions>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:transactions:6.0">
|
||||
<core-environment node-identifier="${jboss.tx.node.id:1}">
|
||||
<process-id>
|
||||
<uuid/>
|
||||
</process-id>
|
||||
</core-environment>
|
||||
<recovery-environment socket-binding="txn-recovery-environment" status-socket-binding="txn-status-manager"/>
|
||||
<coordinator-environment statistics-enabled="${wildfly.transactions.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
|
||||
<object-store path="tx-object-store" relative-to="jboss.server.data.dir"/>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:undertow:12.0" default-server="default-server" default-virtual-host="default-host" default-servlet-container="default" default-security-domain="other" statistics-enabled="${wildfly.undertow.statistics-enabled:${wildfly.statistics-enabled:false}}">
|
||||
<buffer-cache name="default"/>
|
||||
<server name="default-server">
|
||||
<ajp-listener name="ajp" socket-binding="ajp"/>
|
||||
<http-listener name="default" socket-binding="http" redirect-socket="https" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
|
||||
<https-listener name="https" socket-binding="https" ssl-context="applicationSSC" proxy-address-forwarding="${env.PROXY_ADDRESS_FORWARDING:false}" enable-http2="true"/>
|
||||
<host name="default-host" alias="localhost">
|
||||
<location name="/" handler="welcome-content"/>
|
||||
<http-invoker http-authentication-factory="application-http-authentication"/>
|
||||
</host>
|
||||
</server>
|
||||
<servlet-container name="default">
|
||||
<jsp-config/>
|
||||
<websockets/>
|
||||
</servlet-container>
|
||||
<handlers>
|
||||
<file name="welcome-content" path="${jboss.home.dir}/welcome-content"/>
|
||||
</handlers>
|
||||
<application-security-domains>
|
||||
<application-security-domain name="other" security-domain="ApplicationDomain"/>
|
||||
</application-security-domains>
|
||||
</subsystem>
|
||||
<subsystem xmlns="urn:jboss:domain:weld:4.0"/>
|
||||
</profile>
|
||||
<interfaces>
|
||||
<interface name="management">
|
||||
<inet-address value="${jboss.bind.address.management:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="private">
|
||||
<inet-address value="${jboss.bind.address.private:127.0.0.1}"/>
|
||||
</interface>
|
||||
<interface name="public">
|
||||
<inet-address value="${jboss.bind.address:127.0.0.1}"/>
|
||||
</interface>
|
||||
</interfaces>
|
||||
<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
|
||||
<socket-binding name="ajp" port="${jboss.ajp.port:8009}"/>
|
||||
<socket-binding name="http" port="${jboss.http.port:8080}"/>
|
||||
<socket-binding name="https" port="${jboss.https.port:8443}"/>
|
||||
<socket-binding name="jgroups-mping" interface="private" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45700"/>
|
||||
<socket-binding name="jgroups-tcp" interface="private" port="7600"/>
|
||||
<socket-binding name="jgroups-tcp-fd" interface="private" port="57600"/>
|
||||
<socket-binding name="jgroups-udp" interface="private" port="55200" multicast-address="${jboss.default.multicast.address:230.0.0.4}" multicast-port="45688"/>
|
||||
<socket-binding name="jgroups-udp-fd" interface="private" port="54200"/>
|
||||
<socket-binding name="management-http" interface="management" port="${jboss.management.http.port:9990}"/>
|
||||
<socket-binding name="management-https" interface="management" port="${jboss.management.https.port:9993}"/>
|
||||
<socket-binding name="modcluster" multicast-address="${jboss.modcluster.multicast.address:224.0.1.105}" multicast-port="23364"/>
|
||||
<socket-binding name="txn-recovery-environment" port="4712"/>
|
||||
<socket-binding name="txn-status-manager" port="4713"/>
|
||||
<outbound-socket-binding name="mail-smtp">
|
||||
<remote-destination host="${jboss.mail.server.host:localhost}" port="${jboss.mail.server.port:25}"/>
|
||||
</outbound-socket-binding>
|
||||
</socket-binding-group>
|
||||
</server>
|
@ -1,28 +0,0 @@
|
||||
|
||||
Object.defineProperty(
|
||||
Object,
|
||||
"deepAssign",
|
||||
{
|
||||
"value": function callee(target, source) {
|
||||
Object.keys(source).forEach(function (key) {
|
||||
var value = source[key];
|
||||
if (target[key] === undefined) {
|
||||
target[key] = value;
|
||||
return;
|
||||
}
|
||||
if (value instanceof Object) {
|
||||
if (value instanceof Array) {
|
||||
value.forEach(function (entry) {
|
||||
target[key].push(entry);
|
||||
});
|
||||
return;
|
||||
}
|
||||
callee(target[key], value);
|
||||
return;
|
||||
}
|
||||
target[key] = value;
|
||||
});
|
||||
return target;
|
||||
}
|
||||
}
|
||||
);
|
@ -1,208 +0,0 @@
|
||||
<script>const _=
|
||||
<#macro objectToJson object depth>
|
||||
<@compress>
|
||||
|
||||
<#local isHash = false>
|
||||
<#attempt>
|
||||
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||
<#recover>
|
||||
/* can't evaluate if object is hash */
|
||||
undefined
|
||||
<#return>
|
||||
</#attempt>
|
||||
<#if isHash>
|
||||
|
||||
<#local keys = "">
|
||||
|
||||
<#attempt>
|
||||
<#local keys = object?keys>
|
||||
<#recover>
|
||||
/* can't list keys of object */
|
||||
undefined
|
||||
<#return>
|
||||
</#attempt>
|
||||
|
||||
{${'\n'}
|
||||
|
||||
<#list keys as key>
|
||||
|
||||
<#if key == "class">
|
||||
/* skipping "class" property of object */
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local value = "">
|
||||
|
||||
<#attempt>
|
||||
<#local value = object[key]>
|
||||
<#recover>
|
||||
/* couldn't dereference ${key} of object */
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#if depth gt 7>
|
||||
/* Avoid calling recustively too many times depth: ${depth}, key: ${key} */
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
"${key}": <@objectToJson object=value depth=depth+1/>,
|
||||
|
||||
</#list>
|
||||
|
||||
}${'\n'}
|
||||
|
||||
<#return>
|
||||
|
||||
</#if>
|
||||
|
||||
|
||||
<#local isMethod = "">
|
||||
<#attempt>
|
||||
<#local isMethod = object?is_method>
|
||||
<#recover>
|
||||
/* can't test if object is a method */
|
||||
undefined
|
||||
<#return>
|
||||
</#attempt>
|
||||
|
||||
<#if isMethod>
|
||||
undefined
|
||||
<#return>
|
||||
</#if>
|
||||
|
||||
|
||||
|
||||
<#local isBoolean = "">
|
||||
<#attempt>
|
||||
<#local isBoolean = object?is_boolean>
|
||||
<#recover>
|
||||
/* can't test if object is a boolean */
|
||||
undefined
|
||||
<#return>
|
||||
</#attempt>
|
||||
|
||||
<#if isBoolean>
|
||||
${object?c}
|
||||
<#return>
|
||||
</#if>
|
||||
|
||||
|
||||
<#local isEnumerable = "">
|
||||
<#attempt>
|
||||
<#local isEnumerable = object?is_enumerable>
|
||||
<#recover>
|
||||
/* can't test if object is enumerable */
|
||||
undefined
|
||||
<#return>
|
||||
</#attempt>
|
||||
|
||||
<#if isEnumerable>
|
||||
|
||||
[${'\n'}
|
||||
|
||||
<#list object as item>
|
||||
|
||||
<@objectToJson object=item depth=depth+1/>,
|
||||
|
||||
</#list>
|
||||
|
||||
]${'\n'}
|
||||
|
||||
<#return>
|
||||
</#if>
|
||||
|
||||
|
||||
<#attempt>
|
||||
"${object?replace('"', '\\"')?no_esc}"
|
||||
<#recover>
|
||||
/* couldn't convert into string non hash, non method, non boolean, non enumerable object */
|
||||
undefined;
|
||||
<#return>
|
||||
</#attempt>
|
||||
|
||||
|
||||
</@compress>
|
||||
</#macro>
|
||||
|
||||
(()=>{
|
||||
|
||||
const nonAutomaticallyConvertible = {
|
||||
"messagesPerField": {
|
||||
|
||||
<#assign fieldNames = ["global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm"]>
|
||||
|
||||
<#attempt>
|
||||
<#list profile.attributes as attribute>
|
||||
<#assign fieldNames += [attribute.name]>
|
||||
</#list>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
"printIfExists": function (fieldName, x) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"existsError": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"get": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
<#if messagesPerField.existsError('${fieldName}')>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"exists": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
}
|
||||
},
|
||||
"msg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
|
||||
"advancedMsg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }
|
||||
};
|
||||
|
||||
const out = {};
|
||||
|
||||
Object.deepAssign(
|
||||
out,
|
||||
//Removing all the undefined
|
||||
JSON.parse(JSON.stringify(<@objectToJson object=.data_model depth=0 />))
|
||||
);
|
||||
|
||||
Object.deepAssign(
|
||||
out,
|
||||
nonAutomaticallyConvertible
|
||||
);
|
||||
|
||||
return out;
|
||||
|
||||
})()
|
||||
</script>
|
@ -0,0 +1,298 @@
|
||||
<script>const _=
|
||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||
(()=>{
|
||||
|
||||
const out =
|
||||
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
|
||||
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
|
||||
out["messagesPerField"]= {
|
||||
<#assign fieldNames = [
|
||||
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
|
||||
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
|
||||
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
|
||||
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
|
||||
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
|
||||
]>
|
||||
|
||||
<#attempt>
|
||||
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
|
||||
<#list profile.attributes as attribute>
|
||||
<#if fieldNames?seq_contains(attribute.name)>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#assign fieldNames += [attribute.name]>
|
||||
</#list>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
"printIfExists": function (fieldName, x) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"existsError": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"get": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
<#if messagesPerField.existsError('${fieldName}')>
|
||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
},
|
||||
"exists": function (fieldName) {
|
||||
<#list fieldNames as fieldName>
|
||||
if(fieldName === "${fieldName}" ){
|
||||
<#attempt>
|
||||
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
}
|
||||
</#list>
|
||||
throw new Error("There is no " + fieldName + " field");
|
||||
}
|
||||
};
|
||||
|
||||
out["pageId"] = "${pageId}";
|
||||
|
||||
return out;
|
||||
|
||||
})()
|
||||
<#function ftl_object_to_js_code_declaring_an_object object path>
|
||||
|
||||
<#local isHash = "">
|
||||
<#attempt>
|
||||
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
|
||||
</#attempt>
|
||||
|
||||
<#if isHash>
|
||||
|
||||
<#if path?size gt 10>
|
||||
<#return "ABORT: Too many recursive calls">
|
||||
</#if>
|
||||
|
||||
<#local keys = "">
|
||||
|
||||
<#attempt>
|
||||
<#local keys = object?keys>
|
||||
<#recover>
|
||||
<#return "ABORT: We can't list keys on this object">
|
||||
</#attempt>
|
||||
|
||||
|
||||
<#local out_seq = []>
|
||||
|
||||
<#list keys as key>
|
||||
|
||||
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if
|
||||
(
|
||||
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
|
||||
are_same_path(path, ["url"])
|
||||
) || (
|
||||
key == "updateProfileCtx" &&
|
||||
are_same_path(path, [])
|
||||
) || (
|
||||
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 -->
|
||||
key == "loginAction" &&
|
||||
are_same_path(path, ["url"]) &&
|
||||
pageId == "saml-post-form.ftl"
|
||||
) || (
|
||||
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
|
||||
are_same_path(path, ["brokerContext"]) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
) || (
|
||||
key == "identityProviderBrokerCtx" &&
|
||||
are_same_path(path, []) &&
|
||||
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
|
||||
)
|
||||
>
|
||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
|
||||
|
||||
<#attempt>
|
||||
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
|
||||
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
<#if !object[key]??>
|
||||
<#continue>
|
||||
</#if>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local propertyValue = "">
|
||||
|
||||
<#attempt>
|
||||
<#local propertyValue = object[key]>
|
||||
<#recover>
|
||||
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
|
||||
<#continue>
|
||||
</#attempt>
|
||||
|
||||
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
|
||||
|
||||
<#if rec_out?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#local isMethod = "">
|
||||
<#attempt>
|
||||
<#local isMethod = object?is_method>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it'sa method.">
|
||||
</#attempt>
|
||||
|
||||
<#if isMethod>
|
||||
<#return "ABORT: It's a method">
|
||||
</#if>
|
||||
|
||||
<#local isBoolean = "">
|
||||
<#attempt>
|
||||
<#local isBoolean = object?is_boolean>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's a boolean">
|
||||
</#attempt>
|
||||
|
||||
<#if isBoolean>
|
||||
<#return object?c>
|
||||
</#if>
|
||||
|
||||
<#local isEnumerable = "">
|
||||
<#attempt>
|
||||
<#local isEnumerable = object?is_enumerable>
|
||||
<#recover>
|
||||
<#return "ABORT: Can't test if it's an enumerable">
|
||||
</#attempt>
|
||||
|
||||
|
||||
<#if isEnumerable>
|
||||
|
||||
<#local out_seq = []>
|
||||
|
||||
<#local i = 0>
|
||||
|
||||
<#list object as array_item>
|
||||
|
||||
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
|
||||
|
||||
<#local i = i + 1>
|
||||
|
||||
<#if rec_out?starts_with("ABORT:")>
|
||||
|
||||
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
|
||||
|
||||
<#if errorMessage != " It's a method" >
|
||||
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
|
||||
</#if>
|
||||
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#local out_seq += [rec_out + ","]>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
|
||||
|
||||
</#if>
|
||||
|
||||
<#attempt>
|
||||
<#return '"' + object?js_string + '"'>;
|
||||
<#recover>
|
||||
</#attempt>
|
||||
|
||||
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
|
||||
|
||||
</#function>
|
||||
<#function are_same_path path searchedPath>
|
||||
|
||||
<#if path?size != path?size>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#local i=0>
|
||||
|
||||
<#list path as property>
|
||||
|
||||
<#local searchedProperty=searchedPath[i]>
|
||||
|
||||
<#if searchedProperty?is_string && searchedProperty == "*">
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_string && !property?is_string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?is_number && !property?is_number>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#if searchedProperty?string != property?string>
|
||||
<#return false>
|
||||
</#if>
|
||||
|
||||
<#local i+= 1>
|
||||
|
||||
</#list>
|
||||
|
||||
<#return true>
|
||||
|
||||
</#function>
|
||||
</script>
|
@ -16,15 +16,13 @@ export const pageIds = [
|
||||
"terms.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-page-expired.ftl",
|
||||
] as const;
|
||||
|
||||
export type PageId = typeof pageIds[number];
|
||||
|
||||
function loadAdjacentFile(fileBasename: string) {
|
||||
return fs.readFileSync(pathJoin(__dirname, fileBasename)).toString("utf8");
|
||||
}
|
||||
|
||||
export function generateFtlFilesCodeFactory(params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
indexHtmlCode: string;
|
||||
@ -77,8 +75,11 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
);
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const ftlPlaceholders = {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl").match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||
const replaceValueBySearchValue = {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||
"<#if scripts??>",
|
||||
" <#list scripts as script>",
|
||||
@ -88,8 +89,6 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
...(Object.keys(cssGlobalsToDefine).length === 0
|
||||
@ -105,18 +104,10 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
"",
|
||||
]),
|
||||
"<script>",
|
||||
loadAdjacentFile("Object.deepAssign.js"),
|
||||
"</script>",
|
||||
"<script>",
|
||||
` window.${ftlValuesGlobalName}= Object.assign(`,
|
||||
` {},`,
|
||||
` ${objectKeys(ftlPlaceholders)[0]}`,
|
||||
" );",
|
||||
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||
"</script>",
|
||||
"",
|
||||
pageSpecificCodePlaceholder,
|
||||
"",
|
||||
objectKeys(ftlPlaceholders)[1],
|
||||
objectKeys(replaceValueBySearchValue)[1],
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
@ -129,19 +120,13 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||
|
||||
let ftlCode = $.html().replace(
|
||||
pageSpecificCodePlaceholder,
|
||||
[
|
||||
"<script>",
|
||||
` Object.deepAssign(`,
|
||||
` window.${ftlValuesGlobalName},`,
|
||||
` { "pageId": "${pageId}" }`,
|
||||
" );",
|
||||
"</script>",
|
||||
].join("\n"),
|
||||
);
|
||||
let ftlCode = $.html();
|
||||
|
||||
objectKeys(ftlPlaceholders).forEach(id => (ftlCode = ftlCode.replace(id, ftlPlaceholders[id])));
|
||||
Object.entries({
|
||||
...replaceValueBySearchValue,
|
||||
//If updated, don't forget to change in the ftl script as well.
|
||||
"PAGE_ID_xIgLsPgGId9D8e": pageId,
|
||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||
|
||||
return { ftlCode };
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { join as pathJoin, basename as pathBasename } from "path";
|
||||
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
|
||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
||||
import * as child_process from "child_process";
|
||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/getKcContext/kcContextMocks/urlResourcesPath";
|
||||
import { isInside } from "../tools/isInside";
|
||||
import type { KeycloakVersion } from "../KeycloakVersion";
|
||||
|
||||
export function generateKeycloakThemeResources(params: {
|
||||
themeName: string;
|
||||
@ -17,7 +18,7 @@ export function generateKeycloakThemeResources(params: {
|
||||
urlOrigin: undefined | string;
|
||||
extraPagesId: string[];
|
||||
extraThemeProperties: string[];
|
||||
keycloakVersion: "11.0.3" | "15.0.2";
|
||||
keycloakVersion: KeycloakVersion;
|
||||
}) {
|
||||
const {
|
||||
themeName,
|
||||
@ -112,20 +113,22 @@ export function generateKeycloakThemeResources(params: {
|
||||
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": themeResourcesDirPath,
|
||||
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(resourcesCommonPath)),
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesCommonPath),
|
||||
"srcDirPath": themeResourcesDirPath,
|
||||
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
|
||||
});
|
||||
|
||||
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename);
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
|
||||
Buffer.from(["This is just a test folder that helps develop", "the login and register page without having to yarn build"].join(" ")),
|
||||
Buffer.from(
|
||||
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" "),
|
||||
),
|
||||
);
|
||||
|
||||
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
@ -37,7 +37,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPath
|
||||
const { cssCode, urlPathname, urlOrigin } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
urlPathname === "/" ? /url\(\/([^/][^)]+)\)/g : new RegExp(`url\\(${urlPathname}([^)]+)\\)`, "g"),
|
||||
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
|
||||
);
|
||||
|
||||
@ -52,7 +52,7 @@ export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/url\(\/[^/][^)]+\)[^;}]*/g) ?? []).forEach(
|
||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
|
||||
);
|
||||
|
||||
|
@ -22,7 +22,7 @@ if (require.main === module) {
|
||||
const keycloakVersion = process.argv[2] as KeycloakVersion | undefined;
|
||||
|
||||
if (keycloakVersion === undefined) {
|
||||
return "15.0.2";
|
||||
return "11.0.3";
|
||||
}
|
||||
|
||||
return keycloakVersion;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { basename as pathBasename, join as pathJoin } from "path";
|
||||
import { execSync } from "child_process";
|
||||
import fs from "fs";
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
import { rm_rf, rm, rm_r } from "./rm";
|
||||
|
||||
/** assert url ends with .zip */
|
||||
@ -9,14 +9,15 @@ export function downloadAndUnzip(params: { url: string; destDirPath: string; pat
|
||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
|
||||
const zipFilePath = pathBasename(url);
|
||||
|
||||
rm_rf(tmpDirPath);
|
||||
|
||||
fs.mkdirSync(tmpDirPath, { "recursive": true });
|
||||
|
||||
execSync(`wget ${url}`, { "cwd": tmpDirPath });
|
||||
execSync(`curl -L ${url} -o ${zipFilePath}`, { "cwd": tmpDirPath });
|
||||
|
||||
execSync(`unzip ${pathBasename(url)}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/*"`}`, {
|
||||
execSync(`unzip ${zipFilePath}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/*"`}`, {
|
||||
"cwd": tmpDirPath,
|
||||
});
|
||||
|
||||
|
@ -2,7 +2,7 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
function getProjectRootRec(dirPath: string): string {
|
||||
if (fs.existsSync(path.join(dirPath, "package.json"))) {
|
||||
if (fs.existsSync(path.join(dirPath, "tsconfig.json"))) {
|
||||
return dirPath;
|
||||
}
|
||||
return getProjectRootRec(path.join(dirPath, ".."));
|
||||
|
5
src/bin/tools/isM1.ts
Normal file
5
src/bin/tools/isM1.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import * as os from "os";
|
||||
|
||||
export function getIsM1() {
|
||||
return os.cpus()[0].model.includes("Apple M1");
|
||||
}
|
@ -10,8 +10,10 @@ import { LoginResetPassword } from "./LoginResetPassword";
|
||||
import { LoginVerifyEmail } from "./LoginVerifyEmail";
|
||||
import { Terms } from "./Terms";
|
||||
import { LoginOtp } from "./LoginOtp";
|
||||
import { LoginUpdatePassword } from "./LoginUpdatePassword";
|
||||
import { LoginUpdateProfile } from "./LoginUpdateProfile";
|
||||
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
|
||||
import { LoginPageExpired } from "./LoginPageExpired";
|
||||
|
||||
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
|
||||
switch (kcContext.pageId) {
|
||||
@ -33,9 +35,13 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase }
|
||||
return <Terms {...{ kcContext, ...props }} />;
|
||||
case "login-otp.ftl":
|
||||
return <LoginOtp {...{ kcContext, ...props }} />;
|
||||
case "login-update-password.ftl":
|
||||
return <LoginUpdatePassword {...{ kcContext, ...props }} />;
|
||||
case "login-update-profile.ftl":
|
||||
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
|
||||
case "login-idp-link-confirm.ftl":
|
||||
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
|
||||
case "login-page-expired.ftl":
|
||||
return <LoginPageExpired {...{ kcContext, ...props }} />;
|
||||
}
|
||||
});
|
||||
|
@ -20,16 +20,12 @@ export type KcTemplateClassKey =
|
||||
| "kcLocaleWrapperClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcLabelWrapperClass"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcLabelWrapperClass"
|
||||
| "kcFormGroupClass"
|
||||
| "kcResetFlowIcon"
|
||||
| "kcResetFlowIcon"
|
||||
| "kcFeedbackSuccessIcon"
|
||||
| "kcFeedbackWarningIcon"
|
||||
| "kcFeedbackErrorIcon"
|
||||
| "kcFeedbackInfoIcon"
|
||||
| "kcContentWrapperClass"
|
||||
| "kcFormSocialAccountContentClass"
|
||||
| "kcFormSocialAccountClass"
|
||||
| "kcSignUpClass"
|
||||
|
@ -5,6 +5,7 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import type { FormEventHandler } from "react";
|
||||
|
||||
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
|
||||
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
||||
@ -15,7 +16,19 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.L
|
||||
|
||||
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||
|
||||
const onSubmit = useConstCallback(() => (setIsLoginButtonDisabled(true), true));
|
||||
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoginButtonDisabled(true);
|
||||
|
||||
const formElement = e.target as HTMLFormElement;
|
||||
|
||||
//NOTE: Even if we login with email Keycloak expect username and password in
|
||||
//the POST request.
|
||||
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
|
||||
|
||||
formElement.submit();
|
||||
});
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -33,27 +46,40 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.L
|
||||
{realm.password && (
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
||||
{!realm.loginWithEmailAllowed
|
||||
? msg("username")
|
||||
: !realm.registrationEmailAsUsername
|
||||
? msg("usernameOrEmail")
|
||||
: msg("email")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id="username"
|
||||
className={cx(props.kcInputClass)}
|
||||
name="username"
|
||||
defaultValue={login.username ?? ""}
|
||||
type="text"
|
||||
{...(usernameEditDisabled
|
||||
? { "disabled": true }
|
||||
: {
|
||||
"autoFocus": true,
|
||||
"autoComplete": "off",
|
||||
})}
|
||||
/>
|
||||
{(() => {
|
||||
const label = !realm.loginWithEmailAllowed
|
||||
? "username"
|
||||
: realm.registrationEmailAsUsername
|
||||
? "email"
|
||||
: "usernameOrEmail";
|
||||
|
||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
|
||||
{msg(label)}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id={autoCompleteHelper}
|
||||
className={cx(props.kcInputClass)}
|
||||
//NOTE: This is used by Google Chrome auto fill so we use it to tell
|
||||
//the browser how to pre fill the form but before submit we put it back
|
||||
//to username because it is what keycloak expects.
|
||||
name={autoCompleteHelper}
|
||||
defaultValue={login.username ?? ""}
|
||||
type="text"
|
||||
{...(usernameEditDisabled
|
||||
? { "disabled": true }
|
||||
: {
|
||||
"autoFocus": true,
|
||||
"autoComplete": "off",
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
||||
|
@ -3,8 +3,8 @@ import { Template } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
import { appendHead } from "../tools/appendHead";
|
||||
import { join as pathJoin } from "path";
|
||||
import { headInsert } from "../tools/headInsert";
|
||||
import { pathJoin } from "../tools/pathJoin";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
|
||||
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp } & KcProps) => {
|
||||
@ -17,7 +17,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBas
|
||||
useEffect(() => {
|
||||
let isCleanedUp = false;
|
||||
|
||||
appendHead({
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js"),
|
||||
}).then(() => {
|
||||
|
36
src/lib/components/LoginPageExpired.tsx
Normal file
36
src/lib/components/LoginPageExpired.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { memo } from "react";
|
||||
import { Template } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
|
||||
export const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => {
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg } = useKcMessage();
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
displayMessage={false}
|
||||
headerNode={msg("pageExpiredTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<p id="instruction1" className="instruction">
|
||||
{msg("pageExpiredMsg1")}
|
||||
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||
{msg("doClickHere")}
|
||||
</a>{" "}
|
||||
.<br />
|
||||
{msg("pageExpiredMsg2")}{" "}
|
||||
<a id="loginContinueLink" href={url.loginAction}>
|
||||
{msg("doClickHere")}
|
||||
</a>{" "}
|
||||
.
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
117
src/lib/components/LoginUpdatePassword.tsx
Normal file
117
src/lib/components/LoginUpdatePassword.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { memo } from "react";
|
||||
import { Template } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
|
||||
export const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => {
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
const { msg, msgStr } = useKcMessage();
|
||||
|
||||
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
headerNode={msg("updatePasswordTitle")}
|
||||
formNode={
|
||||
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
readOnly={true}
|
||||
autoComplete="username"
|
||||
style={{ display: "none" }}
|
||||
/>
|
||||
<input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
|
||||
|
||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-new" className={cx(props.kcLabelClass)}>
|
||||
{msg("passwordNew")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="password"
|
||||
id="password-new"
|
||||
name="password-new"
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
className={cx(props.kcInputClass)}
|
||||
/>
|
||||
</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"
|
||||
name="password-confirm"
|
||||
autoComplete="new-password"
|
||||
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)}>
|
||||
{isAppInitiatedAction && (
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
|
||||
{msgStr("logoutOtherSessions")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
@ -1,17 +1,29 @@
|
||||
import { memo, Fragment } from "react";
|
||||
import { useMemo, memo, useEffect, useState, Fragment } from "react";
|
||||
import { Template } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "../i18n/useKcMessage";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import type { ReactComponent } from "../tools/ReactComponent";
|
||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||
import { useFormValidationSlice } from "../useFormValidationSlice";
|
||||
|
||||
export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
|
||||
const { url, messagesPerField, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
|
||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = useKcMessage();
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
const { cx, css } = useCssAndCx();
|
||||
|
||||
const props = useMemo(
|
||||
() => ({
|
||||
...props_,
|
||||
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
|
||||
}),
|
||||
[cx, css],
|
||||
);
|
||||
|
||||
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -22,71 +34,7 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
||||
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"
|
||||
autoComplete="new-password"
|
||||
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
|
||||
}
|
||||
/>
|
||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} {...props} />
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
@ -108,6 +56,7 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
disabled={!isFomSubmittable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -117,85 +66,152 @@ export const RegisterUserProfile = memo(({ kcContext, ...props }: { kcContext: K
|
||||
);
|
||||
});
|
||||
|
||||
const UserProfileFormFields = memo(
|
||||
({
|
||||
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
|
||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
};
|
||||
|
||||
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
|
||||
const { cx, css } = useCssAndCx();
|
||||
|
||||
const { advancedMsg } = useKcMessage();
|
||||
|
||||
const {
|
||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||
formValidationReducer,
|
||||
attributesWithPassword,
|
||||
} = useFormValidationSlice({
|
||||
kcContext,
|
||||
BeforeField = () => null,
|
||||
AfterField = () => null,
|
||||
...props
|
||||
}: { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
|
||||
Partial<
|
||||
Record<
|
||||
"BeforeField" | "AfterField",
|
||||
ReactComponent<{
|
||||
attribute: KcContextBase.RegisterUserProfile["profile"]["attributes"][number];
|
||||
}>
|
||||
>
|
||||
>) => {
|
||||
const { messagesPerField } = kcContext;
|
||||
});
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
useEffect(() => {
|
||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||
}, [isFormSubmittable]);
|
||||
|
||||
const { advancedMsg } = useKcMessage();
|
||||
const onChangeFactory = useCallbackFactory(
|
||||
(
|
||||
[name]: [string],
|
||||
[
|
||||
{
|
||||
target: { value },
|
||||
},
|
||||
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>],
|
||||
) =>
|
||||
formValidationReducer({
|
||||
"action": "update value",
|
||||
name,
|
||||
"newValue": value,
|
||||
}),
|
||||
);
|
||||
|
||||
let currentGroup = "";
|
||||
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
||||
formValidationReducer({
|
||||
"action": "focus lost",
|
||||
name,
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{kcContext.profile.attributes
|
||||
.map(attribute => [attribute, attribute])
|
||||
.map(([attribute, { group = "", groupDisplayHeader = "", groupDisplayDescription = "" }], i) => (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
||||
{(groupDisplayHeader !== "" && advancedMsg(groupDisplayHeader)) || currentGroup}
|
||||
let currentGroup = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{attributesWithPassword.map((attribute, i) => {
|
||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||
|
||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||
|
||||
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
||||
{advancedMsg(groupDisplayDescription) ?? ""}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<BeforeField attribute={attribute} />
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
defaultValue={attribute.value ?? ""}
|
||||
className={cx(props.kcInputClass)}
|
||||
aria-invalid={messagesPerField.existsError(attribute.name)}
|
||||
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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
)}
|
||||
<div className={formGroupClassName}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
{(() => {
|
||||
const { options } = attribute.validators;
|
||||
|
||||
if (options !== undefined) {
|
||||
return (
|
||||
<select
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
onChange={onChangeFactory(attribute.name)}
|
||||
onBlur={onBlurFactory(attribute.name)}
|
||||
value={value}
|
||||
>
|
||||
{options.options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={(() => {
|
||||
switch (attribute.name) {
|
||||
case "password-confirm":
|
||||
case "password":
|
||||
return "password";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
})()}
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={value}
|
||||
onChange={onChangeFactory(attribute.name)}
|
||||
className={cx(props.kcInputClass)}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
onBlur={onBlurFactory(attribute.name)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{displayableErrors.length !== 0 && (
|
||||
<span
|
||||
id={`input-error-${attribute.name}`}
|
||||
className={cx(
|
||||
props.kcInputErrorMessageClass,
|
||||
css({
|
||||
"position": displayableErrors.length === 1 ? "absolute" : undefined,
|
||||
"& > span": { "display": "block" },
|
||||
}),
|
||||
)}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
@ -8,8 +8,8 @@ import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
||||
import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
|
||||
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
|
||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||
import { appendHead } from "../tools/appendHead";
|
||||
import { join as pathJoin } from "path";
|
||||
import { headInsert } from "../tools/headInsert";
|
||||
import { pathJoin } from "../tools/pathJoin";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import type { KcTemplateProps } from "./KcProps";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
@ -92,12 +92,15 @@ export const Template = memo((props: TemplateProps) => {
|
||||
[
|
||||
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath)),
|
||||
].map(href =>
|
||||
appendHead({
|
||||
"type": "css",
|
||||
href,
|
||||
}),
|
||||
),
|
||||
]
|
||||
.reverse()
|
||||
.map(href =>
|
||||
headInsert({
|
||||
"type": "css",
|
||||
href,
|
||||
"position": "prepend",
|
||||
}),
|
||||
),
|
||||
).then(() => {
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
@ -107,7 +110,7 @@ export const Template = memo((props: TemplateProps) => {
|
||||
});
|
||||
|
||||
toArr(props.scripts).forEach(relativePath =>
|
||||
appendHead({
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(url.resourcesPath, relativePath),
|
||||
}),
|
||||
|
@ -21,8 +21,10 @@ export type KcContextBase =
|
||||
| KcContextBase.LoginVerifyEmail
|
||||
| KcContextBase.Terms
|
||||
| KcContextBase.LoginOtp
|
||||
| KcContextBase.LoginUpdatePassword
|
||||
| KcContextBase.LoginUpdateProfile
|
||||
| KcContextBase.LoginIdpLinkConfirm;
|
||||
| KcContextBase.LoginIdpLinkConfirm
|
||||
| KcContextBase.LoginPageExpired;
|
||||
|
||||
export declare namespace KcContextBase {
|
||||
export type Common = {
|
||||
@ -34,6 +36,7 @@ export declare namespace KcContextBase {
|
||||
loginUrl: string;
|
||||
};
|
||||
realm: {
|
||||
name: string;
|
||||
displayName?: string;
|
||||
displayNameHtml?: string;
|
||||
internationalizationEnabled: boolean;
|
||||
@ -191,6 +194,11 @@ export declare namespace KcContextBase {
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginUpdatePassword = Common & {
|
||||
pageId: "login-update-password.ftl";
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type LoginUpdateProfile = Common & {
|
||||
pageId: "login-update-profile.ftl";
|
||||
user: {
|
||||
@ -206,6 +214,10 @@ export declare namespace KcContextBase {
|
||||
pageId: "login-idp-link-confirm.ftl";
|
||||
idpAlias: string;
|
||||
};
|
||||
|
||||
export type LoginPageExpired = Common & {
|
||||
pageId: "login-page-expired.ftl";
|
||||
};
|
||||
}
|
||||
|
||||
export type Attribute = {
|
||||
@ -217,10 +229,64 @@ export type Attribute = {
|
||||
groupDisplayHeader?: string;
|
||||
groupDisplayDescription?: string;
|
||||
readOnly: boolean;
|
||||
autocomplete?: string;
|
||||
validators: Validators;
|
||||
annotations: Record<string, string>;
|
||||
groupAnnotations: Record<string, string>;
|
||||
autocomplete?:
|
||||
| "on"
|
||||
| "off"
|
||||
| "name"
|
||||
| "honorific-prefix"
|
||||
| "given-name"
|
||||
| "additional-name"
|
||||
| "family-name"
|
||||
| "honorific-suffix"
|
||||
| "nickname"
|
||||
| "email"
|
||||
| "username"
|
||||
| "new-password"
|
||||
| "current-password"
|
||||
| "one-time-code"
|
||||
| "organization-title"
|
||||
| "organization"
|
||||
| "street-address"
|
||||
| "address-line1"
|
||||
| "address-line2"
|
||||
| "address-line3"
|
||||
| "address-level4"
|
||||
| "address-level3"
|
||||
| "address-level2"
|
||||
| "address-level1"
|
||||
| "country"
|
||||
| "country-name"
|
||||
| "postal-code"
|
||||
| "cc-name"
|
||||
| "cc-given-name"
|
||||
| "cc-additional-name"
|
||||
| "cc-family-name"
|
||||
| "cc-number"
|
||||
| "cc-exp"
|
||||
| "cc-exp-month"
|
||||
| "cc-exp-year"
|
||||
| "cc-csc"
|
||||
| "cc-type"
|
||||
| "transaction-currency"
|
||||
| "transaction-amount"
|
||||
| "language"
|
||||
| "bday"
|
||||
| "bday-day"
|
||||
| "bday-month"
|
||||
| "bday-year"
|
||||
| "sex"
|
||||
| "tel"
|
||||
| "tel-country-code"
|
||||
| "tel-national"
|
||||
| "tel-area-code"
|
||||
| "tel-local"
|
||||
| "tel-extension"
|
||||
| "impp"
|
||||
| "url"
|
||||
| "photo";
|
||||
};
|
||||
|
||||
export type Validators = Partial<{
|
||||
@ -243,6 +309,13 @@ export type Validators = Partial<{
|
||||
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
||||
uri: Validators.DoIgnoreEmpty;
|
||||
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
|
||||
/** Made up validator that only exists in Keycloakify */
|
||||
_compareToOther: Validators.DoIgnoreEmpty &
|
||||
Validators.ErrorMessage & {
|
||||
name: string;
|
||||
shouldBe: "equal" | "different";
|
||||
};
|
||||
options: Validators.Options;
|
||||
}>;
|
||||
|
||||
export declare namespace Validators {
|
||||
@ -256,8 +329,11 @@ export declare namespace Validators {
|
||||
|
||||
export type Range = {
|
||||
/** "0", "1", "2"... yeah I know, don't tell me */
|
||||
min?: string;
|
||||
max?: string;
|
||||
min?: `${number}`;
|
||||
max?: `${number}`;
|
||||
};
|
||||
export type Options = {
|
||||
options: string[];
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import type { KcContextBase } from "./KcContextBase";
|
||||
import type { KcContextBase, Attribute } from "./KcContextBase";
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
||||
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||
import type { DeepPartial } from "../tools/DeepPartial";
|
||||
import { deepAssign } from "../tools/deepAssign";
|
||||
|
||||
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
|
||||
? KcContextBase
|
||||
: AndByDiscriminatingKey<"pageId", KcContextExtended & KcContextBase.Common, KcContextBase>;
|
||||
import { id } from "tsafe/id";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ExtendsKcContextBase } from "./getKcContextFromWindow";
|
||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { pathJoin } from "../tools/pathJoin";
|
||||
import { pathBasename } from "../tools/pathBasename";
|
||||
import { resourcesCommonPath } from "./kcContextMocks/urlResourcesPath";
|
||||
|
||||
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||
@ -44,12 +46,63 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
"target": kcContext,
|
||||
"source": partialKcContextCustomMock,
|
||||
});
|
||||
|
||||
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
|
||||
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
|
||||
|
||||
const { attributes } = kcContextDefaultMock.profile;
|
||||
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes = [];
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName = {};
|
||||
|
||||
const partialAttributes = [
|
||||
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? []),
|
||||
].filter(exclude(undefined));
|
||||
|
||||
attributes.forEach(attribute => {
|
||||
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
||||
|
||||
const augmentedAttribute: Attribute = {} as any;
|
||||
|
||||
deepAssign({
|
||||
"target": augmentedAttribute,
|
||||
"source": attribute,
|
||||
});
|
||||
|
||||
if (partialAttribute !== undefined) {
|
||||
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
|
||||
|
||||
deepAssign({
|
||||
"target": augmentedAttribute,
|
||||
"source": partialAttribute,
|
||||
});
|
||||
}
|
||||
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
|
||||
});
|
||||
|
||||
partialAttributes.forEach(partialAttribute => {
|
||||
const { name } = partialAttribute;
|
||||
|
||||
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
||||
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
return {
|
||||
"kcContext": typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName],
|
||||
};
|
||||
const kcContext = getKcContextFromWindow<KcContextExtended>();
|
||||
|
||||
if (kcContext !== undefined) {
|
||||
const { url } = kcContext;
|
||||
|
||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonPath));
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
11
src/lib/getKcContext/getKcContextFromWindow.ts
Normal file
11
src/lib/getKcContext/getKcContextFromWindow.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import type { KcContextBase } from "./KcContextBase";
|
||||
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
||||
|
||||
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
|
||||
? KcContextBase
|
||||
: AndByDiscriminatingKey<"pageId", KcContextExtended & KcContextBase.Common, KcContextBase>;
|
||||
|
||||
export function getKcContextFromWindow<KcContextExtended extends { pageId: string } = never>(): ExtendsKcContextBase<KcContextExtended> | undefined {
|
||||
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export type { KcContextBase } from "./KcContextBase";
|
||||
export type { KcContextBase, Attribute, Validators } from "./KcContextBase";
|
||||
export type { ExtendsKcContextBase } from "./getKcContextFromWindow";
|
||||
export { getKcContext } from "./getKcContext";
|
||||
|
@ -5,7 +5,7 @@ import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
|
||||
//NOTE: Aside because we want to be able to import them from node
|
||||
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
||||
import { id } from "tsafe/id";
|
||||
import { join as pathJoin } from "path";
|
||||
import { pathJoin } from "../../tools/pathJoin";
|
||||
|
||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||
|
||||
@ -18,6 +18,7 @@ export const kcContextCommonMock: KcContextBase.Common = {
|
||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
||||
},
|
||||
"realm": {
|
||||
"name": "myrealm",
|
||||
"displayName": "myrealm",
|
||||
"displayNameHtml": "myrealm",
|
||||
"internationalizationEnabled": true,
|
||||
@ -210,6 +211,7 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"autocomplete": "username",
|
||||
"readOnly": false,
|
||||
"name": "username",
|
||||
"value": "xxxx",
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
@ -226,6 +228,10 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"email": {
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"pattern": {
|
||||
"ignore.empty.value": true,
|
||||
"pattern": "gmail\\.com$",
|
||||
},
|
||||
},
|
||||
"displayName": "${email}",
|
||||
"annotations": {},
|
||||
@ -273,29 +279,6 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"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 {
|
||||
@ -363,6 +346,11 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
],
|
||||
},
|
||||
}),
|
||||
id<KcContextBase.LoginUpdatePassword>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-update-password.ftl",
|
||||
"username": "anUsername",
|
||||
}),
|
||||
id<KcContextBase.LoginUpdateProfile>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-update-profile.ftl",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { pathJoin } from "../../tools/pathJoin";
|
||||
|
||||
export const subDirOfPublicDirBasename = "keycloak_static";
|
||||
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "/resources");
|
||||
export const resourcesCommonPath = pathJoin(subDirOfPublicDirBasename, "/resources_common");
|
||||
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "resources");
|
||||
export const resourcesCommonPath = pathJoin(resourcesPath, "resources_common");
|
||||
|
@ -34,7 +34,7 @@ export function getKcLanguageTagLabel(language: KcLanguageTag): LanguageLabel {
|
||||
return kcLanguageByTagLabel[language] ?? language;
|
||||
}
|
||||
|
||||
const availableLanguages = objectKeys(kcMessages);
|
||||
export const kcLanguageTags = objectKeys(kcMessages);
|
||||
|
||||
/**
|
||||
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
|
||||
@ -45,7 +45,7 @@ const availableLanguages = objectKeys(kcMessages);
|
||||
export function getBestMatchAmongKcLanguageTag(languageLike: string): KcLanguageTag {
|
||||
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
|
||||
|
||||
const kcLanguageTag = availableLanguages.find(
|
||||
const kcLanguageTag = kcLanguageTags.find(
|
||||
language =>
|
||||
language.toLowerCase().includes(iso2LanguageLike) ||
|
||||
getKcLanguageTagLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase(),
|
||||
|
@ -1,7 +1,29 @@
|
||||
import { kcMessages } from "../generated_kcMessages/15.0.2/login";
|
||||
import { kcMessages as kcMessagesBase } from "../generated_kcMessages/15.0.2/login";
|
||||
import { Evt } from "evt";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
|
||||
const kcMessages = {
|
||||
...kcMessagesBase,
|
||||
"en": {
|
||||
...kcMessagesBase["en"],
|
||||
"shouldBeEqual": "{0} should be equal to {1}",
|
||||
"shouldBeDifferent": "{0} should be different to {1}",
|
||||
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||
"mustBeAnInteger": "Must be an integer",
|
||||
"notAValidOption": "Not a valid option",
|
||||
},
|
||||
"fr": {
|
||||
...kcMessagesBase["fr"],
|
||||
/* spell-checker: disable */
|
||||
"shouldBeEqual": "{0} doit être egale à {1}",
|
||||
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||
"mustBeAnInteger": "Doit être un nombre entiers",
|
||||
"notAValidOption": "N'est pas une option valide",
|
||||
/* spell-checker: enable */
|
||||
},
|
||||
};
|
||||
|
||||
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
||||
|
||||
(["termsText", "doAccept", "doDecline", "termsTitle"] as const).forEach(key =>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createUseGlobalState } from "powerhooks/useGlobalState";
|
||||
import { getKcContext } from "../getKcContext";
|
||||
import { getKcContextFromWindow } from "../getKcContext/getKcContextFromWindow";
|
||||
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
||||
import type { StatefulEvt } from "powerhooks";
|
||||
import { KcLanguageTag } from "./KcLanguageTag";
|
||||
@ -8,7 +8,7 @@ import { KcLanguageTag } from "./KcLanguageTag";
|
||||
const wrap = createUseGlobalState(
|
||||
"kcLanguageTag",
|
||||
() => {
|
||||
const { kcContext } = getKcContext();
|
||||
const kcContext = getKcContextFromWindow();
|
||||
|
||||
const languageLike = kcContext?.locale?.current ?? (typeof navigator === "undefined" ? undefined : navigator.language);
|
||||
|
||||
|
@ -1,21 +1,96 @@
|
||||
import { useCallback, useReducer } from "react";
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import { useReducer } from "react";
|
||||
import { useKcLanguageTag } from "./useKcLanguageTag";
|
||||
import { kcMessages, evtTermsUpdated } from "./kcMessages/login";
|
||||
import { useEvt } from "evt/hooks";
|
||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { id } from "tsafe/id";
|
||||
import { useGuaranteedMemo } from "powerhooks/useGuaranteedMemo";
|
||||
|
||||
export { kcMessages };
|
||||
|
||||
export type MessageKey = keyof typeof kcMessages["en"];
|
||||
|
||||
function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props: {
|
||||
key: Key;
|
||||
args: (string | undefined)[];
|
||||
kcLanguageTag: string;
|
||||
doRenderMarkdown: DoRenderMarkdown;
|
||||
}): Key extends MessageKey ? (DoRenderMarkdown extends true ? JSX.Element : string) : undefined {
|
||||
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
|
||||
|
||||
let str = kcMessages[kcLanguageTag as any as "en"][key as MessageKey] ?? kcMessages["en"][key as MessageKey];
|
||||
|
||||
if (str === undefined) {
|
||||
return undefined as any;
|
||||
}
|
||||
|
||||
str = (() => {
|
||||
const startIndex = str
|
||||
.match(/{[0-9]+}/g)
|
||||
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||
.map(indexStr => parseInt(indexStr))
|
||||
.sort((a, b) => a - b)[0];
|
||||
|
||||
if (startIndex === undefined) {
|
||||
return str;
|
||||
}
|
||||
|
||||
args.forEach((arg, i) => {
|
||||
if (arg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
str = str.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||
});
|
||||
|
||||
return str;
|
||||
})();
|
||||
|
||||
return (
|
||||
doRenderMarkdown ? (
|
||||
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
|
||||
{str}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
str
|
||||
)
|
||||
) as any;
|
||||
}
|
||||
|
||||
function resolveMsgAdvanced<Key extends string, DoRenderMarkdown extends boolean>(props: {
|
||||
key: Key;
|
||||
args: (string | undefined)[];
|
||||
kcLanguageTag: string;
|
||||
doRenderMarkdown: DoRenderMarkdown;
|
||||
}): DoRenderMarkdown extends true ? JSX.Element : string {
|
||||
const { key, args, kcLanguageTag, doRenderMarkdown } = props;
|
||||
|
||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||
|
||||
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||
|
||||
const out = resolveMsg({
|
||||
"key": keyUnwrappedFromCurlyBraces,
|
||||
args,
|
||||
kcLanguageTag,
|
||||
doRenderMarkdown,
|
||||
});
|
||||
|
||||
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||
}
|
||||
|
||||
/**
|
||||
* When the language is switched the page is reloaded, this may appear
|
||||
* as a bug as you might notice that the language successfully switch before
|
||||
* reload.
|
||||
* However we need to tell Keycloak that the user have changed the language
|
||||
* during login so we can retrieve the "local" field of the JWT encoded accessToken.
|
||||
* https://user-images.githubusercontent.com/6702424/138096682-351bb61f-f24e-4caf-91b7-cca8cfa2cb58.mov
|
||||
*
|
||||
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied")
|
||||
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||
*
|
||||
*/
|
||||
export function useKcMessage() {
|
||||
const { kcLanguageTag } = useKcLanguageTag();
|
||||
@ -24,46 +99,17 @@ export function useKcMessage() {
|
||||
|
||||
useEvt(ctx => evtTermsUpdated.attach(ctx, forceUpdate), []);
|
||||
|
||||
const msgStr = useCallback(
|
||||
(key: MessageKey, ...args: (string | undefined)[]): string => {
|
||||
let str: string = kcMessages[kcLanguageTag as any as "en"][key] ?? kcMessages["en"][key];
|
||||
|
||||
args.forEach((arg, i) => {
|
||||
if (arg === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
|
||||
});
|
||||
|
||||
return str;
|
||||
},
|
||||
return useGuaranteedMemo(
|
||||
() => ({
|
||||
"msgStr": (key: MessageKey, ...args: (string | undefined)[]): string =>
|
||||
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
|
||||
"msg": (key: MessageKey, ...args: (string | undefined)[]): JSX.Element =>
|
||||
resolveMsg({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
|
||||
"advancedMsg": <Key extends string>(key: Key, ...args: (string | undefined)[]): JSX.Element =>
|
||||
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": true }),
|
||||
"advancedMsgStr": <Key extends string>(key: Key, ...args: (string | undefined)[]): string =>
|
||||
resolveMsgAdvanced({ key, args, kcLanguageTag, "doRenderMarkdown": false }),
|
||||
}),
|
||||
[kcLanguageTag, trigger],
|
||||
);
|
||||
|
||||
const msg = useCallback<(...args: Parameters<typeof msgStr>) => JSX.Element>(
|
||||
(key, ...args) => (
|
||||
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
|
||||
{msgStr(key, ...args)}
|
||||
</ReactMarkdown>
|
||||
),
|
||||
[msgStr],
|
||||
);
|
||||
|
||||
const advancedMsg = useCallback(
|
||||
(key: string): string | undefined => {
|
||||
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||
|
||||
const resolvedKey = match === null ? key : match[1];
|
||||
|
||||
const out =
|
||||
id<Record<string, string | undefined>>(kcMessages[kcLanguageTag])[resolvedKey] ??
|
||||
id<Record<string, string | undefined>>(kcMessages["en"])[resolvedKey];
|
||||
|
||||
return out !== undefined ? out : match === null ? key : undefined;
|
||||
},
|
||||
[msgStr],
|
||||
);
|
||||
|
||||
return { msg, msgStr, advancedMsg };
|
||||
}
|
||||
|
@ -14,5 +14,6 @@ export * from "./components/Error";
|
||||
export * from "./components/LoginResetPassword";
|
||||
export * from "./components/LoginVerifyEmail";
|
||||
export * from "./keycloakJsAdapter";
|
||||
export * from "./useFormValidationSlice";
|
||||
|
||||
export * from "./tools/assert";
|
||||
|
64
src/lib/tools/Array.prototype.every.ts
Normal file
64
src/lib/tools/Array.prototype.every.ts
Normal file
@ -0,0 +1,64 @@
|
||||
if (!Array.prototype.every) {
|
||||
Array.prototype.every = function (callbackfn: any, thisArg: any) {
|
||||
"use strict";
|
||||
var T, k;
|
||||
|
||||
if (this == null) {
|
||||
throw new TypeError("this is null or not defined");
|
||||
}
|
||||
|
||||
// 1. Let O be the result of calling ToObject passing the this
|
||||
// value as the argument.
|
||||
var O = Object(this);
|
||||
|
||||
// 2. Let lenValue be the result of calling the Get internal method
|
||||
// of O with the argument "length".
|
||||
// 3. Let len be ToUint32(lenValue).
|
||||
var len = O.length >>> 0;
|
||||
|
||||
// 4. If IsCallable(callbackfn) is false, throw a TypeError exception.
|
||||
if (typeof callbackfn !== "function" && Object.prototype.toString.call(callbackfn) !== "[object Function]") {
|
||||
throw new TypeError();
|
||||
}
|
||||
|
||||
// 5. If thisArg was supplied, let T be thisArg; else let T be undefined.
|
||||
if (arguments.length > 1) {
|
||||
T = thisArg;
|
||||
}
|
||||
|
||||
// 6. Let k be 0.
|
||||
k = 0;
|
||||
|
||||
// 7. Repeat, while k < len
|
||||
while (k < len) {
|
||||
var kValue;
|
||||
|
||||
// a. Let Pk be ToString(k).
|
||||
// This is implicit for LHS operands of the in operator
|
||||
// b. Let kPresent be the result of calling the HasProperty internal
|
||||
// method of O with argument Pk.
|
||||
// This step can be combined with c
|
||||
// c. If kPresent is true, then
|
||||
if (k in O) {
|
||||
var testResult;
|
||||
// i. Let kValue be the result of calling the Get internal method
|
||||
// of O with argument Pk.
|
||||
kValue = O[k];
|
||||
|
||||
// ii. Let testResult be the result of calling the Call internal method
|
||||
// of callbackfn with T as the this value if T is not undefined
|
||||
// else is the result of calling callbackfn
|
||||
// and argument list containing kValue, k, and O.
|
||||
if (T) testResult = callbackfn.call(T, kValue, k, O);
|
||||
else testResult = callbackfn(kValue, k, O);
|
||||
|
||||
// iii. If ToBoolean(testResult) is false, return false.
|
||||
if (!testResult) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
k++;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
9
src/lib/tools/HTMLElement.prototype.prepend.ts
Normal file
9
src/lib/tools/HTMLElement.prototype.prepend.ts
Normal file
@ -0,0 +1,9 @@
|
||||
if (!HTMLElement.prototype.prepend) {
|
||||
HTMLElement.prototype.prepend = function (childNode) {
|
||||
if (typeof childNode === "string") {
|
||||
throw new Error("Error with HTMLElement.prototype.appendFirst polyfill");
|
||||
}
|
||||
|
||||
this.insertBefore(childNode, this.firstChild);
|
||||
};
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { deepClone } from "./deepClone";
|
||||
|
||||
//Warning: Be mindful that because of array this is not idempotent.
|
||||
export function deepAssign(params: { target: Record<string, unknown>; source: Record<string, unknown> }) {
|
||||
const { target, source } = params;
|
||||
const { target } = params;
|
||||
|
||||
const source = deepClone(params.source);
|
||||
|
||||
Object.keys(source).forEach(key => {
|
||||
var dereferencedSource = source[key];
|
||||
|
@ -1,3 +1,17 @@
|
||||
export function deepClone<T>(arg: T): T {
|
||||
return JSON.parse(JSON.stringify(arg));
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
|
||||
export function deepClone<T>(o: T): T {
|
||||
if (!(o instanceof Object)) {
|
||||
return o;
|
||||
}
|
||||
|
||||
if (typeof o === "function") {
|
||||
return o;
|
||||
}
|
||||
|
||||
if (o instanceof Array) {
|
||||
return o.map(deepClone) as any;
|
||||
}
|
||||
|
||||
return Object.fromEntries(Object.entries(o).map(([key, value]) => [key, deepClone(value)])) as any;
|
||||
}
|
||||
|
2
src/lib/tools/emailRegExp.ts
Normal file
2
src/lib/tools/emailRegExp.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const emailRegexp =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
@ -1,10 +1,12 @@
|
||||
import "./HTMLElement.prototype.prepend";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
|
||||
export function appendHead(
|
||||
export function headInsert(
|
||||
params:
|
||||
| {
|
||||
type: "css";
|
||||
href: string;
|
||||
position: "append" | "prepend";
|
||||
}
|
||||
| {
|
||||
type: "javascript";
|
||||
@ -46,7 +48,23 @@ export function appendHead(
|
||||
})(),
|
||||
);
|
||||
|
||||
document.getElementsByTagName("head")[0].appendChild(htmlElement);
|
||||
document.getElementsByTagName("head")[0][
|
||||
(() => {
|
||||
switch (params.type) {
|
||||
case "javascript":
|
||||
return "appendChild";
|
||||
case "css":
|
||||
return (() => {
|
||||
switch (params.position) {
|
||||
case "append":
|
||||
return "appendChild";
|
||||
case "prepend":
|
||||
return "prepend";
|
||||
}
|
||||
})();
|
||||
}
|
||||
})()
|
||||
](htmlElement);
|
||||
|
||||
return dLoaded.pr;
|
||||
}
|
3
src/lib/tools/pathBasename.ts
Normal file
3
src/lib/tools/pathBasename.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function pathBasename(path: string) {
|
||||
return path.split("/").reverse()[0];
|
||||
}
|
6
src/lib/tools/pathJoin.ts
Normal file
6
src/lib/tools/pathJoin.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export function pathJoin(...path: string[]): string {
|
||||
return path
|
||||
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
|
||||
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
|
||||
.join("/");
|
||||
}
|
476
src/lib/useFormValidationSlice.tsx
Normal file
476
src/lib/useFormValidationSlice.tsx
Normal file
@ -0,0 +1,476 @@
|
||||
import "./tools/Array.prototype.every";
|
||||
import { useMemo, useReducer, Fragment } from "react";
|
||||
import type { KcContextBase, Validators, Attribute } from "./getKcContext/KcContextBase";
|
||||
import { useKcMessage } from "./i18n/useKcMessage";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import { id } from "tsafe/id";
|
||||
import type { MessageKey } from "./i18n/useKcMessage";
|
||||
import { emailRegexp } from "./tools/emailRegExp";
|
||||
|
||||
export type KcContextLike = {
|
||||
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||
attributes: { name: string; value?: string; validators: Validators }[];
|
||||
passwordRequired: boolean;
|
||||
realm: { registrationEmailAsUsername: boolean };
|
||||
};
|
||||
|
||||
export function useGetErrors(params: {
|
||||
kcContext: {
|
||||
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: { name: string; value?: string; validators: Validators }[];
|
||||
};
|
||||
};
|
||||
}) {
|
||||
const {
|
||||
kcContext: {
|
||||
messagesPerField,
|
||||
profile: { attributes },
|
||||
},
|
||||
} = params;
|
||||
|
||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = useKcMessage();
|
||||
|
||||
const getErrors = useConstCallback((params: { name: string; fieldValueByAttributeName: Record<string, { value: string }> }) => {
|
||||
const { name, fieldValueByAttributeName } = params;
|
||||
|
||||
const { value } = fieldValueByAttributeName[name];
|
||||
|
||||
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
|
||||
|
||||
block: {
|
||||
if (defaultValue !== value) {
|
||||
break block;
|
||||
}
|
||||
|
||||
let doesErrorExist: boolean;
|
||||
|
||||
try {
|
||||
doesErrorExist = messagesPerField.existsError(name);
|
||||
} catch {
|
||||
break block;
|
||||
}
|
||||
|
||||
if (!doesErrorExist) {
|
||||
break block;
|
||||
}
|
||||
|
||||
const errorMessageStr = messagesPerField.get(name);
|
||||
|
||||
return [
|
||||
{
|
||||
"validatorName": undefined,
|
||||
errorMessageStr,
|
||||
"errorMessage": <span key={0}>{errorMessageStr}</span>,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const errors: {
|
||||
errorMessage: JSX.Element;
|
||||
errorMessageStr: string;
|
||||
validatorName: keyof Validators | undefined;
|
||||
}[] = [];
|
||||
|
||||
scope: {
|
||||
const validatorName = "length";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (max !== undefined && value.length > parseInt(max)) {
|
||||
const msgArgs = ["error-invalid-length-too-long", max] as const;
|
||||
|
||||
errors.push({
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
validatorName,
|
||||
});
|
||||
}
|
||||
|
||||
if (min !== undefined && value.length < parseInt(min)) {
|
||||
const msgArgs = ["error-invalid-length-too-short", min] as const;
|
||||
|
||||
errors.push({
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
validatorName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "_compareToOther";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, name: otherName, shouldBe, "error-message": errorMessageKey } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { value: otherValue } = fieldValueByAttributeName[otherName];
|
||||
|
||||
const isValid = (() => {
|
||||
switch (shouldBe) {
|
||||
case "different":
|
||||
return otherValue !== value;
|
||||
case "equal":
|
||||
return otherValue === value;
|
||||
}
|
||||
})();
|
||||
|
||||
if (isValid) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArg = [
|
||||
errorMessageKey ??
|
||||
id<MessageKey>(
|
||||
(() => {
|
||||
switch (shouldBe) {
|
||||
case "equal":
|
||||
return "shouldBeEqual";
|
||||
case "different":
|
||||
return "shouldBeDifferent";
|
||||
}
|
||||
})(),
|
||||
),
|
||||
otherName,
|
||||
name,
|
||||
shouldBe,
|
||||
] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArg)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArg),
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "pattern";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (new RegExp(pattern).test(value)) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [errorMessageKey ?? id<MessageKey>("shouldMatchPattern"), pattern] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArgs),
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
if ([...errors].reverse()[0]?.validatorName === "pattern") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const validatorName = "email";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (emailRegexp.test(value)) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
});
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "integer";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
|
||||
|
||||
if (ignoreEmptyValue && value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const intValue = parseInt(value);
|
||||
|
||||
if (isNaN(intValue)) {
|
||||
const msgArgs = ["mustBeAnInteger"] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (max !== undefined && intValue > parseInt(max)) {
|
||||
const msgArgs = ["error-number-out-of-range-too-big", max] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (min !== undefined && intValue < parseInt(min)) {
|
||||
const msgArgs = ["error-number-out-of-range-too-small", min] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{msg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": msgStr(...msgArgs),
|
||||
});
|
||||
|
||||
break scope;
|
||||
}
|
||||
}
|
||||
|
||||
scope: {
|
||||
const validatorName = "options";
|
||||
|
||||
const validator = validators[validatorName];
|
||||
|
||||
if (validator === undefined) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (value === "") {
|
||||
break scope;
|
||||
}
|
||||
|
||||
if (validator.options.indexOf(value) >= 0) {
|
||||
break scope;
|
||||
}
|
||||
|
||||
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
|
||||
|
||||
errors.push({
|
||||
validatorName,
|
||||
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||
"errorMessageStr": advancedMsgStr(...msgArgs),
|
||||
});
|
||||
}
|
||||
|
||||
//TODO: Implement missing validators.
|
||||
|
||||
return errors;
|
||||
});
|
||||
|
||||
return { getErrors };
|
||||
}
|
||||
|
||||
export function useFormValidationSlice(params: {
|
||||
kcContext: {
|
||||
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
realm: { registrationEmailAsUsername: boolean };
|
||||
};
|
||||
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||
passwordValidators?: Validators;
|
||||
}) {
|
||||
const {
|
||||
kcContext,
|
||||
passwordValidators = {
|
||||
"length": {
|
||||
"ignore.empty.value": true,
|
||||
"min": "4",
|
||||
},
|
||||
},
|
||||
} = params;
|
||||
|
||||
const attributesWithPassword = useMemo(
|
||||
() =>
|
||||
!kcContext.passwordRequired
|
||||
? kcContext.profile.attributes
|
||||
: (() => {
|
||||
const name = kcContext.realm.registrationEmailAsUsername ? "email" : "username";
|
||||
|
||||
return kcContext.profile.attributes.reduce<Attribute[]>(
|
||||
(prev, curr) => [
|
||||
...prev,
|
||||
...(curr.name !== name
|
||||
? [curr]
|
||||
: [
|
||||
curr,
|
||||
id<Attribute>({
|
||||
"name": "password",
|
||||
"displayName": id<`\${${MessageKey}}`>("${password}"),
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"validators": passwordValidators,
|
||||
"annotations": {},
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "new-password",
|
||||
}),
|
||||
id<Attribute>({
|
||||
"name": "password-confirm",
|
||||
"displayName": id<`\${${MessageKey}}`>("${passwordConfirm}"),
|
||||
"required": true,
|
||||
"readOnly": false,
|
||||
"validators": {
|
||||
"_compareToOther": {
|
||||
"name": "password",
|
||||
"ignore.empty.value": true,
|
||||
"shouldBe": "equal",
|
||||
"error-message": id<`\${${MessageKey}}`>("${invalidPasswordConfirmMessage}"),
|
||||
},
|
||||
},
|
||||
"annotations": {},
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "new-password",
|
||||
}),
|
||||
]),
|
||||
],
|
||||
[],
|
||||
);
|
||||
})(),
|
||||
[kcContext, passwordValidators],
|
||||
);
|
||||
|
||||
const { getErrors } = useGetErrors({
|
||||
"kcContext": {
|
||||
"messagesPerField": kcContext.messagesPerField,
|
||||
"profile": {
|
||||
"attributes": attributesWithPassword,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const initialInternalState = useMemo(
|
||||
() =>
|
||||
Object.fromEntries(
|
||||
attributesWithPassword
|
||||
.map(attribute => ({
|
||||
attribute,
|
||||
"errors": getErrors({
|
||||
"name": attribute.name,
|
||||
"fieldValueByAttributeName": Object.fromEntries(
|
||||
attributesWithPassword.map(({ name, value }) => [name, { "value": value ?? "" }]),
|
||||
),
|
||||
}),
|
||||
}))
|
||||
.map(({ attribute, errors }) => [
|
||||
attribute.name,
|
||||
{
|
||||
"value": attribute.value ?? "",
|
||||
errors,
|
||||
"doDisplayPotentialErrorMessages": errors.length !== 0,
|
||||
},
|
||||
]),
|
||||
),
|
||||
[attributesWithPassword],
|
||||
);
|
||||
|
||||
type InternalState = typeof initialInternalState;
|
||||
|
||||
const [formValidationInternalState, formValidationReducer] = useReducer(
|
||||
(
|
||||
state: InternalState,
|
||||
params:
|
||||
| {
|
||||
action: "update value";
|
||||
name: string;
|
||||
newValue: string;
|
||||
}
|
||||
| {
|
||||
action: "focus lost";
|
||||
name: string;
|
||||
},
|
||||
): InternalState => ({
|
||||
...state,
|
||||
[params.name]: {
|
||||
...state[params.name],
|
||||
...(() => {
|
||||
switch (params.action) {
|
||||
case "focus lost":
|
||||
return { "doDisplayPotentialErrorMessages": true };
|
||||
case "update value":
|
||||
return {
|
||||
"value": params.newValue,
|
||||
"errors": getErrors({
|
||||
"name": params.name,
|
||||
"fieldValueByAttributeName": {
|
||||
...state,
|
||||
[params.name]: { "value": params.newValue },
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
})(),
|
||||
},
|
||||
}),
|
||||
initialInternalState,
|
||||
);
|
||||
|
||||
const formValidationState = useMemo(
|
||||
() => ({
|
||||
"fieldStateByAttributeName": Object.fromEntries(
|
||||
Object.entries(formValidationInternalState).map(([name, { value, errors, doDisplayPotentialErrorMessages }]) => [
|
||||
name,
|
||||
{ value, "displayableErrors": doDisplayPotentialErrorMessages ? errors : [] },
|
||||
]),
|
||||
),
|
||||
"isFormSubmittable": Object.entries(formValidationInternalState).every(
|
||||
([name, { value, errors }]) =>
|
||||
errors.length === 0 && (value !== "" || !attributesWithPassword.find(attribute => attribute.name === name)!.required),
|
||||
),
|
||||
}),
|
||||
[formValidationInternalState, attributesWithPassword],
|
||||
);
|
||||
|
||||
return { formValidationState, formValidationReducer, attributesWithPassword };
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { getKcContext } from "../../lib/getKcContext";
|
||||
import type { KcContextBase } from "../../lib/getKcContext";
|
||||
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
|
||||
import type { ExtendsKcContextBase } from "../../lib/getKcContext";
|
||||
import { same } from "evt/tools/inDepth";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
|
225
yarn.lock
225
yarn.lock
@ -30,6 +30,17 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.4"
|
||||
|
||||
"@emotion/cache@*":
|
||||
version "11.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
|
||||
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
|
||||
dependencies:
|
||||
"@emotion/memoize" "^0.7.4"
|
||||
"@emotion/sheet" "^1.1.0"
|
||||
"@emotion/utils" "^1.0.0"
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
stylis "4.0.13"
|
||||
|
||||
"@emotion/cache@^11.4.0":
|
||||
version "11.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0"
|
||||
@ -64,7 +75,7 @@
|
||||
"@emotion/weak-memoize" "^0.2.5"
|
||||
hoist-non-react-statics "^3.3.1"
|
||||
|
||||
"@emotion/serialize@^1.0.2":
|
||||
"@emotion/serialize@*", "@emotion/serialize@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
|
||||
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
|
||||
@ -75,27 +86,22 @@
|
||||
"@emotion/utils" "^1.0.0"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@emotion/server@^11.4.0":
|
||||
version "11.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/server/-/server-11.4.0.tgz#3ae1d74cb31c7d013c3c76e88c0c4439076e9f66"
|
||||
integrity sha512-IHovdWA3V0DokzxLtUNDx4+hQI82zUXqQFcVz/om2t44O0YSc+NHB+qifnyAOoQwt3SXcBTgaSntobwUI9gnfA==
|
||||
dependencies:
|
||||
"@emotion/utils" "^1.0.0"
|
||||
html-tokenize "^2.0.0"
|
||||
multipipe "^1.0.2"
|
||||
through "^2.3.8"
|
||||
|
||||
"@emotion/sheet@^1.0.0", "@emotion/sheet@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.0.2.tgz#1d9ffde531714ba28e62dac6a996a8b1089719d0"
|
||||
integrity sha512-QQPB1B70JEVUHuNtzjHftMGv6eC3Y9wqavyarj4x4lg47RACkeSfNo5pxIOKizwS9AEFLohsqoaxGQj4p0vSIw==
|
||||
|
||||
"@emotion/sheet@^1.1.0":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.1.0.tgz#56d99c41f0a1cda2726a05aa6a20afd4c63e58d2"
|
||||
integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g==
|
||||
|
||||
"@emotion/unitless@^0.7.5":
|
||||
version "0.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
|
||||
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
|
||||
|
||||
"@emotion/utils@^1.0.0":
|
||||
"@emotion/utils@*", "@emotion/utils@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
|
||||
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
|
||||
@ -220,11 +226,6 @@ braces@^3.0.1:
|
||||
dependencies:
|
||||
fill-range "^7.0.1"
|
||||
|
||||
buffer-from@~0.1.1:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-0.1.2.tgz#15f4b9bcef012044df31142c14333caf6e0260d0"
|
||||
integrity sha512-RiWIenusJsmI2KcvqQABB83tLxCByE3upSP8QU3rJDMVFGPWLvPQJt/O1Su9moRWeH7d+Q2HYb68f6+v+tw2vg==
|
||||
|
||||
callsites@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
|
||||
@ -452,7 +453,7 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.2.0.tgz#9a0b6c2782ed6a1c7323d42267183df9bd8b1d57"
|
||||
integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==
|
||||
|
||||
domhandler@4.2.2, domhandler@^4.0, domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2:
|
||||
domhandler@^4.0, domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.2.2:
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.2.2.tgz#e825d721d19a86b8c201a35264e226c678ee755f"
|
||||
integrity sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==
|
||||
@ -468,13 +469,6 @@ domutils@^2.5.2, domutils@^2.6.0, domutils@^2.7.0, domutils@^2.8.0:
|
||||
domelementtype "^2.2.0"
|
||||
domhandler "^4.2.0"
|
||||
|
||||
duplexer2@^0.1.2:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
|
||||
integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=
|
||||
dependencies:
|
||||
readable-stream "^2.0.2"
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
|
||||
@ -558,10 +552,10 @@ event-emitter@^0.3.5:
|
||||
d "1"
|
||||
es5-ext "~0.10.14"
|
||||
|
||||
evt@2.0.0-beta.38:
|
||||
version "2.0.0-beta.38"
|
||||
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.38.tgz#9886a08889cddba1984a236efd6d352f9d6a6539"
|
||||
integrity sha512-b35iBAVlDHVHOqgWzjSBJZu3C0GRZ/2cIHfNFuMSWwLT/WJO2n/4x7hg6BpaNJlSRywBAtf8KcU1GUyVfEhVIA==
|
||||
evt@2.0.0-beta.39:
|
||||
version "2.0.0-beta.39"
|
||||
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.39.tgz#3c859a83b35940f7eecfb5f148f03b7cbf3fee51"
|
||||
integrity sha512-XxJkaHrFWBrzjTbnr5LJYXkGkADsAXReZfq2lFu3Kf1iCEw5/5ibrdXu3bQdWW6xkZ8qwAHT3STU9zYcCl09BA==
|
||||
dependencies:
|
||||
minimal-polyfills "^2.2.1"
|
||||
run-exclusive "^2.2.14"
|
||||
@ -665,24 +659,6 @@ hoist-non-react-statics@^3.3.1:
|
||||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
html-dom-parser@1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/html-dom-parser/-/html-dom-parser-1.0.2.tgz#bb5ff844f214657d899aa4fb7b0a9e7d15607e96"
|
||||
integrity sha512-Jq4oVkVSn+10ut3fyc2P/Fs1jqTo0l45cP6Q8d2ef/9jfkYwulO0QXmyLI0VUiZrXF4czpGgMEJRa52CQ6Fk8Q==
|
||||
dependencies:
|
||||
domhandler "4.2.2"
|
||||
htmlparser2 "6.1.0"
|
||||
|
||||
html-react-parser@^1.2.7:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/html-react-parser/-/html-react-parser-1.4.0.tgz#bf264f38b9fdf4d94e2120f6a39586c15cb81bd0"
|
||||
integrity sha512-v8Kxy+7L90ZFSM690oJWBNRzZWZOQquYPpQt6kDQPzQyZptXgOJ69kHSi7xdqNdm1mOfsDPwF4K9Bo/dS5gRTQ==
|
||||
dependencies:
|
||||
domhandler "4.2.2"
|
||||
html-dom-parser "1.0.2"
|
||||
react-property "2.0.0"
|
||||
style-to-js "1.1.0"
|
||||
|
||||
html-to-react@^1.3.4:
|
||||
version "1.4.7"
|
||||
resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.4.7.tgz#a58129c1b77c6d4e047a647372bd194e25420b89"
|
||||
@ -693,18 +669,7 @@ html-to-react@^1.3.4:
|
||||
lodash.camelcase "^4.3.0"
|
||||
ramda "^0.27.1"
|
||||
|
||||
html-tokenize@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-tokenize/-/html-tokenize-2.0.1.tgz#c3b2ea6e2837d4f8c06693393e9d2a12c960be5f"
|
||||
integrity sha512-QY6S+hZ0f5m1WT8WffYN+Hg+xm/w5I8XeUcAq/ZYP5wVC8xbKi4Whhru3FtrAebD5EhBW8rmFzkDI6eCAuFe2w==
|
||||
dependencies:
|
||||
buffer-from "~0.1.1"
|
||||
inherits "~2.0.1"
|
||||
minimist "~1.2.5"
|
||||
readable-stream "~1.0.27-1"
|
||||
through2 "~0.4.1"
|
||||
|
||||
htmlparser2@6.1.0, htmlparser2@^6.1.0:
|
||||
htmlparser2@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-6.1.0.tgz#c4d762b6c3371a05dbe65e94ae43a9f845fb8fb7"
|
||||
integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==
|
||||
@ -771,16 +736,6 @@ inherits@2, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3:
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
inherits@2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
|
||||
|
||||
inline-style-parser@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
|
||||
integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==
|
||||
|
||||
is-alphabetical@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-1.0.4.tgz#9e7d6b94916be22153745d184c298cbf986a686d"
|
||||
@ -1023,11 +978,6 @@ minimatch@^3.0.3, minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@~1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
@ -1038,14 +988,6 @@ ms@2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
multipipe@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-1.0.2.tgz#cc13efd833c9cda99f224f868461b8e1a3fd939d"
|
||||
integrity sha1-zBPv2DPJzamfIk+GhGG44aP9k50=
|
||||
dependencies:
|
||||
duplexer2 "^0.1.2"
|
||||
object-assign "^4.1.0"
|
||||
|
||||
next-tick@1, next-tick@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
|
||||
@ -1083,16 +1025,11 @@ nth-check@^2.0.0:
|
||||
dependencies:
|
||||
boolbase "^1.0.0"
|
||||
|
||||
object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
|
||||
|
||||
object-keys@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-0.4.0.tgz#28a6aae7428dd2c3a92f3d95f21335dd204e0336"
|
||||
integrity sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=
|
||||
|
||||
once@^1.3.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
@ -1174,6 +1111,11 @@ parse5@^6.0.1:
|
||||
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
|
||||
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
|
||||
|
||||
path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-exists@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
|
||||
@ -1194,14 +1136,6 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
path@^0.12.7:
|
||||
version "0.12.7"
|
||||
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
|
||||
integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=
|
||||
dependencies:
|
||||
process "^0.11.1"
|
||||
util "^0.10.3"
|
||||
|
||||
picomatch@^2.2.3:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
|
||||
@ -1221,12 +1155,12 @@ please-upgrade-node@^3.2.0:
|
||||
dependencies:
|
||||
semver-compare "^1.0.0"
|
||||
|
||||
powerhooks@^0.9.6:
|
||||
version "0.9.6"
|
||||
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.9.6.tgz#45bdd7e7713d0a620b1b099cf2685e5f56cebd8f"
|
||||
integrity sha512-vXGcC5Ty3e5wxnRP37c7rnTE/UY86VVLwAj3tqAMvC9xf1C9wOmu2Q7xTj/4FGK1oGvgqbTiiWuxd+WK4C7kEQ==
|
||||
powerhooks@^0.14.0:
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.14.0.tgz#44dd201f470761362a139ae2cb51eaa658ed5e3e"
|
||||
integrity sha512-jWrRHyqev7Lh3MId7h1mNxs+fgegr8liHN17AaHxMgrXI6KxJ14B3Pe1It4FIRWJ4Z1dxsA734FerFGZ3Vgdkg==
|
||||
dependencies:
|
||||
evt "2.0.0-beta.38"
|
||||
evt "2.0.0-beta.39"
|
||||
memoizee "^0.4.15"
|
||||
resize-observer-polyfill "^1.5.1"
|
||||
tsafe "^0.8.1"
|
||||
@ -1241,11 +1175,6 @@ process-nextick-args@~2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
|
||||
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
|
||||
|
||||
process@^0.11.1:
|
||||
version "0.11.10"
|
||||
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
|
||||
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
|
||||
|
||||
prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
@ -1288,11 +1217,6 @@ react-markdown@^5.0.3:
|
||||
unist-util-visit "^2.0.0"
|
||||
xtend "^4.0.1"
|
||||
|
||||
react-property@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-property/-/react-property-2.0.0.tgz#2156ba9d85fa4741faf1918b38efc1eae3c6a136"
|
||||
integrity sha512-kzmNjIgU32mO4mmH5+iUyrqlpFQhF8K2k7eZ4fdLSOPFrD1XgEuSBv9LDEgxRXTMBqMd8ppT0x6TIzqE5pdGdw==
|
||||
|
||||
react@^17.0.1:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
@ -1301,7 +1225,17 @@ react@^17.0.1:
|
||||
loose-envify "^1.1.0"
|
||||
object-assign "^4.1.1"
|
||||
|
||||
readable-stream@^2.0.2, readable-stream@~2.3.6:
|
||||
readable-stream@~1.0.31:
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
readable-stream@~2.3.6:
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
|
||||
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
|
||||
@ -1314,16 +1248,6 @@ readable-stream@^2.0.2, readable-stream@~2.3.6:
|
||||
string_decoder "~1.1.1"
|
||||
util-deprecate "~1.0.1"
|
||||
|
||||
readable-stream@~1.0.17, readable-stream@~1.0.27-1, readable-stream@~1.0.31:
|
||||
version "1.0.34"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
|
||||
integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
|
||||
dependencies:
|
||||
core-util-is "~1.0.0"
|
||||
inherits "~2.0.1"
|
||||
isarray "0.0.1"
|
||||
string_decoder "~0.10.x"
|
||||
|
||||
regenerator-runtime@^0.13.4:
|
||||
version "0.13.9"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
|
||||
@ -1492,19 +1416,10 @@ strip-final-newline@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad"
|
||||
integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==
|
||||
|
||||
style-to-js@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/style-to-js/-/style-to-js-1.1.0.tgz#631cbb20fce204019b3aa1fcb5b69d951ceac4ac"
|
||||
integrity sha512-1OqefPDxGrlMwcbfpsTVRyzwdhr4W0uxYQzeA2F1CBc8WG04udg2+ybRnvh3XYL4TdHQrCahLtax2jc8xaE6rA==
|
||||
dependencies:
|
||||
style-to-object "0.3.0"
|
||||
|
||||
style-to-object@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.3.0.tgz#b1b790d205991cc783801967214979ee19a76e46"
|
||||
integrity sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==
|
||||
dependencies:
|
||||
inline-style-parser "0.1.1"
|
||||
stylis@4.0.13:
|
||||
version "4.0.13"
|
||||
resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.0.13.tgz#f5db332e376d13cc84ecfe5dace9a2a51d954c91"
|
||||
integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==
|
||||
|
||||
stylis@^4.0.3:
|
||||
version "4.0.10"
|
||||
@ -1540,14 +1455,6 @@ through2@^2.0.1:
|
||||
readable-stream "~2.3.6"
|
||||
xtend "~4.0.1"
|
||||
|
||||
through2@~0.4.1:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/through2/-/through2-0.4.2.tgz#dbf5866031151ec8352bb6c4db64a2292a840b9b"
|
||||
integrity sha1-2/WGYDEVHsg1K7bE22SiKSqEC5s=
|
||||
dependencies:
|
||||
readable-stream "~1.0.17"
|
||||
xtend "~2.1.1"
|
||||
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||
@ -1583,6 +1490,11 @@ tsafe@^0.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.8.1.tgz#9af7e1540bc04313a82d60c98056a5017c8b086b"
|
||||
integrity sha512-EfPjxQHzndQAV/uh0SMGP26Wg3dCuaw8dRv2VPEuGHen5qzg2oqsMvZw2wkQFkiMisZq2fm95m5lheimW2Fpvg==
|
||||
|
||||
tsafe@^0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.9.0.tgz#8394e5fdf81e690c97e2b8be4180a079a4a19bfb"
|
||||
integrity sha512-wmbu8pI/xmW69b13HoS8WbTcSlRTDjIut9ACblBjVZVTk0vsMRXdoh1k1jMu5EzKNohBavKHhqNOOsccSR7XCA==
|
||||
|
||||
tslib@^1.9.0:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
@ -1593,13 +1505,14 @@ tslib@^2.2.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
tss-react@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-1.1.0.tgz#bbbf12b3d30eb02e1a39ac6dba4ca05a9c6bb674"
|
||||
integrity sha512-0UUQaMCbefyXsHesnScLwmoo6lD5sdAVR1h5dgIvCOFTk0i5A5a68K2B9gm89hQFfIKPl7fzolCsJ+G9RE/vpw==
|
||||
tss-react@^3.5.2:
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-3.5.2.tgz#1b5db1f4a71fe62c939eed5368ea7809ca2ad0a9"
|
||||
integrity sha512-IgGizaOhbntrWdh2EISJYnhYEEWXXWf3sn5aa8LYf7e59NCM5mg0PxJuEoLx9pLG5WlJF80zTRN/Y1Jnjw9LYA==
|
||||
dependencies:
|
||||
"@emotion/server" "^11.4.0"
|
||||
html-react-parser "^1.2.7"
|
||||
"@emotion/cache" "*"
|
||||
"@emotion/serialize" "*"
|
||||
"@emotion/utils" "*"
|
||||
|
||||
type-fest@^0.21.3:
|
||||
version "0.21.3"
|
||||
@ -1677,13 +1590,6 @@ util-deprecate@~1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
|
||||
|
||||
util@^0.10.3:
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
|
||||
integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
|
||||
dependencies:
|
||||
inherits "2.0.3"
|
||||
|
||||
vfile-message@^2.0.0:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"
|
||||
@ -1742,13 +1648,6 @@ xtend@^4.0.1, xtend@~4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xtend@~2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-2.1.2.tgz#6efecc2a4dad8e6962c4901b337ce7ba87b5d28b"
|
||||
integrity sha1-bv7MKk2tjmlixJAbM3znuoe10os=
|
||||
dependencies:
|
||||
object-keys "~0.4.0"
|
||||
|
||||
y18n@^5.0.5:
|
||||
version "5.0.8"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||
|
Reference in New Issue
Block a user