Compare commits

...

67 Commits

Author SHA1 Message Date
7a3c74020d Update changelog v4.5.1 2022-01-18 20:15:20 +00:00
7509170dd0 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2022-01-18 21:08:30 +01:00
cd17a97916 Bump version (changelog ignore) 2022-01-18 21:07:43 +01:00
d5e690f964 fix previous version 2022-01-18 21:07:24 +01:00
a19bd20b6b Update changelog v4.5.0 2022-01-18 17:59:48 +00:00
f78526dfff Bump version (changelog ignore) 2022-01-18 18:53:13 +01:00
11d6a2020f Read public/CNAME for domain name in --externel-assets mode 2022-01-18 18:52:52 +01:00
fabd48a22c Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2022-01-01 18:49:39 +01:00
e2ea98b5ef Update instructions for 4.4.0 (changelog ignore) 2022-01-01 18:49:30 +01:00
4473ab0704 Update changelog v4.4.0 2022-01-01 17:23:44 +00:00
1f68cc305a Bump version (changelog ignore) 2022-01-01 18:23:10 +01:00
ec2543551f Merge pull request #73 from lazToum/main
(feature) added login-page-expired.ftl
2022-01-01 18:21:14 +01:00
7b0bedc755 added login-page-expired.ftl 2022-01-01 18:44:05 +02:00
ab054ca515 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-12-28 02:27:10 +01:00
1b49c7804c Add update instruction for 4.3.0 2021-12-28 02:27:04 +01:00
764a288b1a Update changelog v4.3.0 2021-12-27 21:33:37 +00:00
fc6910bc2c Bump version (changelog ignore) 2021-12-27 22:31:03 +01:00
91dd1dcddc Merge pull request #72 from praiz/main
feat(*): added login-update-password
2021-12-27 22:26:53 +01:00
97e6aaca65 feat(*): added login-update-password 2021-12-28 00:08:25 +03:00
af5ff1ecfb Update changelog v4.2.21 2021-12-27 18:32:50 +00:00
c9b53b0d3a Bump version (changelog ignore) 2021-12-27 19:29:59 +01:00
d05a62e1ea update dependencies 2021-12-27 19:29:31 +01:00
a83eec31d8 Feat link behind badges (changelog ignore) 2021-12-23 14:01:52 +01:00
729503fe31 Fix borken link to onyxia #71 (changelog ignore) 2021-12-23 12:28:45 +01:00
7137ff4257 Bump version (changelog ignore) 2021-12-21 16:47:40 +01:00
6db11a7433 Update changelog v4.2.19 2021-12-21 15:47:35 +00:00
8666aa62dd Merge pull request #70 from VBustamante/patch-1 2021-12-21 16:44:20 +01:00
eedcd7a2a6 Added realm name field to KcContext mocks object 2021-12-21 12:27:28 -03:00
e3e8fb663a Bump version (changelog ignore) 2021-12-21 14:39:58 +01:00
6e663210ee Merge pull request #69 from VBustamante/patch-1
Adding name field to realm in KcContext type
2021-12-21 14:39:19 +01:00
42cd0fe2f0 Adding name field to realm in KcContext type 2021-12-21 09:45:23 -03:00
daac05c1ad Update confirmed working webpack version (changelog ignore) 2021-12-18 18:13:58 +01:00
1d63c393a3 Update changelog v4.2.18 2021-12-17 18:38:15 +00:00
e8a3751b32 Bump version (changelog ignore) 2021-12-17 19:31:11 +01:00
cb8a41d5be Improve css url() import (fix CRA 5) 2021-12-17 19:30:44 +01:00
a8d4f7e23c Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-12-17 00:00:10 +01:00
93bcdac3be Add notice about Webpack 5 (changelog ignore) 2021-12-17 00:00:01 +01:00
32d0388556 Update changelog v4.2.17 2021-12-16 22:16:24 +00:00
633a32ffd6 Bump version (changelog ignore) 2021-12-16 23:13:41 +01:00
cb8b165c8e Fix path.join polyfill 2021-12-16 23:13:18 +01:00
57134359b9 Update changelog v4.2.16 2021-12-16 20:00:18 +00:00
377436e46a Merge branch 'main' of https://github.com/InseeFrLab/keycloakify into main 2021-12-16 20:56:48 +01:00
51129aaeff Bump version (changelog ignore) 2021-12-16 20:56:41 +01:00
a2bd5050ff add missing reference to path in src/lib (changelog ignore) 2021-12-16 20:54:13 +01:00
128c416ce7 Update changelog v4.2.15 2021-12-16 19:48:20 +00:00
7184773521 Bump version (changelog ignore) 2021-12-16 20:45:04 +01:00
1138313028 use custom polyfill for path.join (fix webpack 5 build) 2021-12-16 20:44:39 +01:00
46bd319ebe Fix small error in readme (changelog ignore) 2021-12-12 21:41:23 +01:00
cfcc48259c Update changelog v4.2.14 2021-12-12 19:49:27 +00:00
785ce7a8ab Bump version (changelog ignore) 2021-12-12 20:46:58 +01:00
ad5de216b0 Merge pull request #65 from InseeFrLab/doge_ftl_errors
Prevent ftl errors in Keycloak log
2021-12-12 20:45:55 +01:00
26b80d6af7 Encourage users to report errors in logs 2021-12-12 20:44:03 +01:00
a8623d8066 Fix ftl error related to url.loginAction in saml-post-form.ftl 2021-12-12 20:17:50 +01:00
86ab9f72a5 Ftl prevent error with updateProfileCtx 2021-12-12 19:35:28 +01:00
b3892dab8d Ftl prevent error with auth.attemptedUsername 2021-12-12 19:19:17 +01:00
57a5d034dd Fix ftl error as comment formatting 2021-12-12 19:06:12 +01:00
cee9569581 Refactor: Create ftl function are_same_path (changelog ignore) 2021-12-12 18:59:39 +01:00
159429da6e Remove extra semicollon in ftl (changelog ignore) 2021-12-12 17:39:39 +01:00
a292cb0b4b Merge remote-tracking branch 'origin/main' into doge_ftl_errors 2021-12-12 14:12:31 +01:00
d70985d8d2 Update README, remove all instruction about errors in logs 2021-12-12 14:10:00 +01:00
484f95f5d2 Bump beta version (changelog ignore) 2021-12-12 12:53:11 +01:00
6e0553af9b Avoid error in Keycloak logs, fix long template loading time 2021-12-12 05:38:21 +01:00
cb18d3d765 Bump version (changelog ignore) 2021-12-12 05:29:59 +01:00
f316f38ae5 Update CI workflow (changelog ignore) 2021-12-12 05:29:59 +01:00
5f07cb374b Update changelog v4.2.13 2021-12-12 05:29:59 +01:00
96d31e07c3 Update about future fixes (changelog ignore) 2021-12-11 20:26:37 +01:00
99a5efe36c Add missing collon in README sample code
Add miss ','
2021-12-09 21:16:05 +01:00
20 changed files with 650 additions and 337 deletions

2
.gitignore vendored
View File

@ -44,3 +44,5 @@ jspm_packages
/sample_react_project/ /sample_react_project/
/.yarn_home/ /.yarn_home/
.idea

View File

@ -1,3 +1,72 @@
### **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) ### **4.2.13** (2021-12-08)
- Fix broken link about how to import fonts #62 - Fix broken link about how to import fonts #62

111
README.md
View File

@ -5,11 +5,21 @@
<i>🔏 Create Keycloak themes using React 🔏</i> <i>🔏 Create Keycloak themes using React 🔏</i>
<br> <br>
<br> <br>
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=develop"> <a href="https://github.com/garronej/keycloakify/actions">
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify"> <img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
<img src="https://img.shields.io/npm/dw/keycloakify"> </a>
<img src="https://img.shields.io/npm/l/keycloakify"> <a href="https://bundlephobia.com/package/keycloakify">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565"> <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"> <a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/> <img src="https://awesome.re/mentioned-badge.svg"/>
</a> </a>
@ -20,10 +30,9 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png"> <img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p> </p>
**NEW in v4** > **New in v4.4.0**: Feature [`login-page-expired.ftl`](https://user-images.githubusercontent.com/6702424/147856832-38c042a7-9fc8-473f-9595-e00123095ca6.png).
> Every time a page is added it's a breaking change for non CSS-only theme.
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳 > Change [this](https://github.com/garronej/keycloakify-demo-app/blob/812754109c61157741f4a0b222026deb1538a02d/src/KcApp/KcApp.tsx#L18) and [this](https://github.com/garronej/keycloakify-demo-app/blob/812754109c61157741f4a0b222026deb1538a02d/src/KcApp/KcApp.tsx#L39) to update.
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
# Motivations # Motivations
@ -71,13 +80,12 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
- [GitHub Actions](#github-actions) - [GitHub Actions](#github-actions)
- [Limitations](#limitations) - [Limitations](#limitations)
- [`process.env.PUBLIC_URL` not supported.](#processenvpublic_url-not-supported) - [`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) - [Example of setup that **won't** work](#example-of-setup-that-wont-work)
- [Possible workarounds](#possible-workarounds) - [Possible workarounds](#possible-workarounds)
- [Implement context persistence (optional)](#implement-context-persistence-optional) - [Implement context persistence (optional)](#implement-context-persistence-optional)
- [Kickstart video](#kickstart-video) - [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)
- [The pages take too long to load ?](#the-pages-take-too-long-to-load-)
- [Adding custom message (to `i18n/useKcMessage.tsx`)](#adding-custom-message-to-i18nusekcmessagetsx) - [Adding custom message (to `i18n/useKcMessage.tsx`)](#adding-custom-message-to-i18nusekcmessagetsx)
- [Email domain whitelist](#email-domain-whitelist) - [Email domain whitelist](#email-domain-whitelist)
- [Changelog highlights](#changelog-highlights) - [Changelog highlights](#changelog-highlights)
@ -99,7 +107,7 @@ Tested with the following Keycloak versions:
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
(before you customize it) will always be the ones of Keycloak v11. (before you customize it) will always be the ones of Keycloak v11.
This tool assumes you are bundling your app with Webpack (tested with 4.44.2) . This tool assumes you are bundling your app with Webpack (tested with 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 It assumes there is a `build/` directory at the root of your react project directory containing a `index.html` file
and a `build/static/` directory generated by webpack. and a `build/static/` directory generated by webpack.
For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432) For more information see [this issue](https://github.com/InseeFrLab/keycloakify/issues/5#issuecomment-832296432)
@ -107,7 +115,7 @@ For more information see [this issue](https://github.com/InseeFrLab/keycloakify/
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3) **All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3)
- `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `curl`, `unzip` are assumed to be available. - `mvn` ([Maven](https://maven.apache.org/)), `rm`, `mkdir`, `curl`, `unzip` are assumed to be available.
- `docker` must be up and running when running `yarn keycloak`. - `docker` must be up and running when running `start_keycloak_testing_container.sh` (Instructions provided after running `yarn keycloak`).
## My framework doesnt seem to be supported, what can I do? ## My framework doesnt seem to be supported, what can I do?
@ -152,27 +160,23 @@ your index should look something like:
`src/index.tsx` `src/index.tsx`
```tsx ```tsx
import { App } from "./<wherever>/App"; import { App } from "./<wherever>/App";
import { import { KcApp, defaultKcProps, getKcContext } from "keycloakify";
KcApp, import { css } from "tss-react/@emotion/css";
defaultKcProps,
getKcContext
} from "keycloakify";
import { css } from "tss-react/@emotion/css";
const { kcContext } = getKcContext(); const { kcContext } = getKcContext();
const myClassName = css({ "color": "red" }); const myClassName = css({ "color": "red" });
reactDom.render( reactDom.render(
<KcApp <KcApp
kcContext={kcContext} kcContext={kcContext}
{...{ {...{
...defaultKcProps, ...defaultKcProps,
"kcHeaderWrapperClass": myClassName "kcHeaderWrapperClass": myClassName,
}} }}
/> />,
document.getElementById("root") document.getElementById("root"),
); );
``` ```
@ -213,8 +217,8 @@ reactDom.render(
<img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png"> <img src="https://user-images.githubusercontent.com/6702424/114326299-6892fc00-9b34-11eb-8d75-85696e55458f.png">
</p> </p>
Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx) Example of a customization using only CSS: [here](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/components/KcApp.tsx)
(the [index.tsx](https://github.com/InseeFrLab/onyxia-ui/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) ) (the [index.tsx](https://github.com/InseeFrLab/onyxia-web/blob/012639d62327a9a56be80c46e32c32c9497b82db/src/app/index.tsx#L89-L94) )
and the result you can expect: and the result you can expect:
<p align="center"> <p align="center">
@ -229,7 +233,7 @@ If you want to go beyond only customizing the CSS you can re-implement some of t
pages or even add new ones. pages or even add new ones.
If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel). If you want to go this way checkout the demo setup provided [here](https://github.com/garronej/keycloakify-demo-app/tree/look_and_feel).
If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/app/components/KcApp). If you prefer a real life example you can checkout [onyxia-web's source](https://github.com/InseeFrLab/onyxia-web/tree/main/src/ui/components/KcApp).
The web app is in production [here](https://datalab.sspcloud.fr). The web app is in production [here](https://datalab.sspcloud.fr).
Main takeaways are: Main takeaways are:
@ -291,7 +295,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 to first browse unauthenticated before logging in, you will get a significant
performance boost if you jump through those hoops: performance boost if you jump through those hoops:
- Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2) - Provide the url of your app in the `homepage` field of package.json. [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/package.json#L2) 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) - Build the theme using `npx build-keycloak-theme --external-assets` [ex](https://github.com/garronej/keycloakify-demo-app/blob/7847cc70ef374ab26a6cc7953461cf25603e9a6d/.github/workflows/ci.yaml#L21)
- Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app. - Enable [long-term assets caching](https://create-react-app.dev/docs/production-build/#static-file-caching) on the server hosting your app.
- Make sure not to build your app and the keycloak theme separately - Make sure not to build your app and the keycloak theme separately
@ -358,7 +362,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). You won't be able to [import things from your public directory **in your JavaScript code**](https://create-react-app.dev/docs/using-the-public-folder/#adding-assets-outside-of-the-module-system).
(This isn't recommended anyway). (This isn't recommended anyway).
## `@font-face` importing fonts from the `src/` dir ## `@font-face` importing fonts from the `src/` dir
If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-) If you are building the theme with [--external-assets](#enable-loading-in-a-blink-of-a-eye-of-login-pages-)
this limitation doesn't apply, you can import fonts however you see fit. this limitation doesn't apply, you can import fonts however you see fit.
@ -435,42 +439,29 @@ keycloakInstance.init({
If you really want to go the extra miles and avoid having the white 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 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 # Kickstart video
_NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded_ _NOTE: keycloak-react-theming was renamed keycloakify since this video was recorded_
[![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8) [![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8)
# About the errors related to `objectToJson` in Keycloak logs. # 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 ```log
FTL stack trace ("~" means nesting-related): FTL stack trace ("~" means nesting-related):
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 70, column 21] - 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 "objectToJson_please_ignore_errors" at line 36, column 5] - Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
- Reached through: @objectToJson_please_ignore_errors object=value depth=(dep... [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 81, column 27] - 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 "objectToJson_please_ignore_errors" at line 36, column 5] - Reached through: @compress [in template "login.ftl" in macro "ftl_object_to_js_code_declaring_an_object" at line 36, column 5]
- Reached through: @objectToJson_please_ignore_errors object=(.data_model) de... [in template "login.ftl" at line 163, column 43] - 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 to show up in the log. It's just noise, they can be safely ignored.
Unfortunately, there is nothing I know of that can be done to avoid them or even mute them. You can, however, and are encouraged to, report any that you would spot.
They can be, however, safely ignored. Just open an issue about it and I will release a patched version of Keycloakify in the better delays.
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 because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs.
# The pages take too long to load ?
The problem of templates taking a long time to load only happens in the test environment, when you have a console logging all the above-mentioned `.ftl` warnings in real time. Logging all those warnings is what takes time. Once in production page load is way faster.
If you run the docker container locally we acknowledge that the loading time is getting out of hand.
We are [in the process](https://github.com/InseeFrLab/keycloakify/pull/63) of resolving this issue.
In the meantime we recommend [to run the docker container as a background task](https://youtu.be/F29Z1GaH-jk).
# Adding custom message (to `i18n/useKcMessage.tsx`) # Adding custom message (to `i18n/useKcMessage.tsx`)
@ -488,6 +479,10 @@ and `kcRegisterContext["authorizedMailDomains"]` to validate on.
# Changelog highlights # Changelog highlights
> **New in v4.3.0**: Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
> Every time a page is added it's a breaking change for non CSS-only theme.
> Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4 ## v4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳 - Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "4.2.13", "version": "4.5.1",
"description": "Keycloak theme generator for Reacts app", "description": "Keycloak theme generator for Reacts app",
"repository": { "repository": {
"type": "git", "type": "git",
@ -77,11 +77,11 @@
}, },
"dependencies": { "dependencies": {
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"evt": "2.0.0-beta.38", "evt": "2.0.0-beta.39",
"minimal-polyfills": "^2.2.1", "minimal-polyfills": "^2.2.1",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"tsafe": "^0.8.1" "tsafe": "^0.9.0"
} }
} }

View File

@ -4,6 +4,7 @@ import { join as pathJoin, relative as pathRelative, basename as pathBasename }
import * as child_process from "child_process"; import * as child_process from "child_process";
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles"; import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
import { URL } from "url"; import { URL } from "url";
import * as fs from "fs";
type ParsedPackageJson = { type ParsedPackageJson = {
name: string; name: string;
@ -41,7 +42,17 @@ export function main() {
const url = (() => { const url = (() => {
const { homepage } = parsedPackageJson; 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 { return {

View File

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

View File

@ -1,208 +0,0 @@
<script>const _=
<#macro objectToJson_please_ignore_errors 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_please_ignore_errors 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_please_ignore_errors 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_please_ignore_errors object=.data_model depth=0 />))
);
Object.deepAssign(
out,
nonAutomaticallyConvertible
);
return out;
})()
</script>

View File

@ -0,0 +1,290 @@
<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"
)
>
<#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>

View File

@ -16,15 +16,13 @@ export const pageIds = [
"terms.ftl", "terms.ftl",
"login-otp.ftl", "login-otp.ftl",
"login-update-profile.ftl", "login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl", "login-idp-link-confirm.ftl",
"login-page-expired.ftl",
] as const; ] as const;
export type PageId = typeof pageIds[number]; export type PageId = typeof pageIds[number];
function loadAdjacentFile(fileBasename: string) {
return fs.readFileSync(pathJoin(__dirname, fileBasename)).toString("utf8");
}
export function generateFtlFilesCodeFactory(params: { export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>; cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: 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. //FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlPlaceholders = { const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadAdjacentFile("common.ftl").match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1], '{ "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 -->": [ "<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>", "<#if scripts??>",
" <#list scripts as script>", " <#list scripts as script>",
@ -88,8 +89,6 @@ export function generateFtlFilesCodeFactory(params: {
].join("\n"), ].join("\n"),
}; };
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
$("head").prepend( $("head").prepend(
[ [
...(Object.keys(cssGlobalsToDefine).length === 0 ...(Object.keys(cssGlobalsToDefine).length === 0
@ -105,18 +104,10 @@ export function generateFtlFilesCodeFactory(params: {
"", "",
]), ]),
"<script>", "<script>",
loadAdjacentFile("Object.deepAssign.js"), ` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"<script>",
` window.${ftlValuesGlobalName}= Object.assign(`,
` {},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
" );",
"</script>", "</script>",
"", "",
pageSpecificCodePlaceholder, objectKeys(replaceValueBySearchValue)[1],
"",
objectKeys(ftlPlaceholders)[1],
].join("\n"), ].join("\n"),
); );
@ -129,19 +120,13 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(partiallyFixedIndexHtmlCode); const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html().replace( let ftlCode = $.html();
pageSpecificCodePlaceholder,
[
"<script>",
` Object.deepAssign(`,
` window.${ftlValuesGlobalName},`,
` { "pageId": "${pageId}" }`,
" );",
"</script>",
].join("\n"),
);
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 }; return { ftlCode };
} }

View File

@ -37,7 +37,7 @@ export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPath
const { cssCode, urlPathname, urlOrigin } = params; const { cssCode, urlPathname, urlOrigin } = params;
const fixedCssCode = cssCode.replace( 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})`, (...[, 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> = {}; 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), match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
); );

View File

@ -10,8 +10,10 @@ import { LoginResetPassword } from "./LoginResetPassword";
import { LoginVerifyEmail } from "./LoginVerifyEmail"; import { LoginVerifyEmail } from "./LoginVerifyEmail";
import { Terms } from "./Terms"; import { Terms } from "./Terms";
import { LoginOtp } from "./LoginOtp"; import { LoginOtp } from "./LoginOtp";
import { LoginUpdatePassword } from "./LoginUpdatePassword";
import { LoginUpdateProfile } from "./LoginUpdateProfile"; import { LoginUpdateProfile } from "./LoginUpdateProfile";
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm"; import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
import { LoginPageExpired } from "./LoginPageExpired";
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => { export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
switch (kcContext.pageId) { switch (kcContext.pageId) {
@ -33,9 +35,13 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase }
return <Terms {...{ kcContext, ...props }} />; return <Terms {...{ kcContext, ...props }} />;
case "login-otp.ftl": case "login-otp.ftl":
return <LoginOtp {...{ kcContext, ...props }} />; return <LoginOtp {...{ kcContext, ...props }} />;
case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, ...props }} />;
case "login-update-profile.ftl": case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, ...props }} />; return <LoginUpdateProfile {...{ kcContext, ...props }} />;
case "login-idp-link-confirm.ftl": case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />; return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...props }} />;
} }
}); });

View File

@ -4,7 +4,7 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useKcMessage } from "../i18n/useKcMessage"; import { useKcMessage } from "../i18n/useKcMessage";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { join as pathJoin } from "path"; import { pathJoin } from "../tools/pathJoin";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp } & KcProps) => { export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp } & KcProps) => {

View 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>
</>
}
/>
);
});

View 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>
}
/>
);
});

View File

@ -9,7 +9,7 @@ import { getBestMatchAmongKcLanguageTag } from "../i18n/KcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag"; import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks/useCallbackFactory"; import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { join as pathJoin } from "path"; import { pathJoin } from "../tools/pathJoin";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { KcTemplateProps } from "./KcProps"; import type { KcTemplateProps } from "./KcProps";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "tss-react";

View File

@ -21,8 +21,10 @@ export type KcContextBase =
| KcContextBase.LoginVerifyEmail | KcContextBase.LoginVerifyEmail
| KcContextBase.Terms | KcContextBase.Terms
| KcContextBase.LoginOtp | KcContextBase.LoginOtp
| KcContextBase.LoginUpdatePassword
| KcContextBase.LoginUpdateProfile | KcContextBase.LoginUpdateProfile
| KcContextBase.LoginIdpLinkConfirm; | KcContextBase.LoginIdpLinkConfirm
| KcContextBase.LoginPageExpired;
export declare namespace KcContextBase { export declare namespace KcContextBase {
export type Common = { export type Common = {
@ -34,6 +36,7 @@ export declare namespace KcContextBase {
loginUrl: string; loginUrl: string;
}; };
realm: { realm: {
name: string;
displayName?: string; displayName?: string;
displayNameHtml?: string; displayNameHtml?: string;
internationalizationEnabled: boolean; 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 & { export type LoginUpdateProfile = Common & {
pageId: "login-update-profile.ftl"; pageId: "login-update-profile.ftl";
user: { user: {
@ -206,6 +214,10 @@ export declare namespace KcContextBase {
pageId: "login-idp-link-confirm.ftl"; pageId: "login-idp-link-confirm.ftl";
idpAlias: string; idpAlias: string;
}; };
export type LoginPageExpired = Common & {
pageId: "login-page-expired.ftl";
};
} }
export type Attribute = { export type Attribute = {

View File

@ -5,7 +5,7 @@ import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
//NOTE: Aside because we want to be able to import them from node //NOTE: Aside because we want to be able to import them from node
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath"; import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
import { id } from "tsafe/id"; import { id } from "tsafe/id";
import { join as pathJoin } from "path"; import { pathJoin } from "../../tools/pathJoin";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/"; 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", "loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
}, },
"realm": { "realm": {
"name": "myrealm",
"displayName": "myrealm", "displayName": "myrealm",
"displayNameHtml": "myrealm", "displayNameHtml": "myrealm",
"internationalizationEnabled": true, "internationalizationEnabled": true,
@ -345,6 +346,11 @@ export const kcContextMocks: KcContextBase[] = [
], ],
}, },
}), }),
id<KcContextBase.LoginUpdatePassword>({
...kcContextCommonMock,
"pageId": "login-update-password.ftl",
"username": "anUsername",
}),
id<KcContextBase.LoginUpdateProfile>({ id<KcContextBase.LoginUpdateProfile>({
...kcContextCommonMock, ...kcContextCommonMock,
"pageId": "login-update-profile.ftl", "pageId": "login-update-profile.ftl",

View File

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

View File

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

View File

@ -545,6 +545,15 @@ evt@2.0.0-beta.38:
run-exclusive "^2.2.14" run-exclusive "^2.2.14"
tsafe "^0.4.1" tsafe "^0.4.1"
evt@2.0.0-beta.39:
version "2.0.0-beta.39"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.0.0-beta.39.tgz#3c859a83b35940f7eecfb5f148f03b7cbf3fee51"
integrity sha512-XxJkaHrFWBrzjTbnr5LJYXkGkADsAXReZfq2lFu3Kf1iCEw5/5ibrdXu3bQdWW6xkZ8qwAHT3STU9zYcCl09BA==
dependencies:
minimal-polyfills "^2.2.1"
run-exclusive "^2.2.14"
tsafe "^0.4.1"
execa@^5.1.1: execa@^5.1.1:
version "5.1.1" version "5.1.1"
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
@ -1469,6 +1478,11 @@ tsafe@^0.8.1:
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.8.1.tgz#9af7e1540bc04313a82d60c98056a5017c8b086b" resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.8.1.tgz#9af7e1540bc04313a82d60c98056a5017c8b086b"
integrity sha512-EfPjxQHzndQAV/uh0SMGP26Wg3dCuaw8dRv2VPEuGHen5qzg2oqsMvZw2wkQFkiMisZq2fm95m5lheimW2Fpvg== integrity sha512-EfPjxQHzndQAV/uh0SMGP26Wg3dCuaw8dRv2VPEuGHen5qzg2oqsMvZw2wkQFkiMisZq2fm95m5lheimW2Fpvg==
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: tslib@^1.9.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"