Compare commits
99 Commits
Author | SHA1 | Date | |
---|---|---|---|
2688aefdfb | |||
eaa6582e67 | |||
a136bc619d | |||
8780c516fa | |||
b4bdec7970 | |||
671aeadf29 | |||
341d985610 | |||
76b9a78182 | |||
021c0a9429 | |||
7b3462d158 | |||
c180dee414 | |||
769da5c5ca | |||
1a4dd79240 | |||
5cf290b033 | |||
aec3da25b3 | |||
66d7cb563d | |||
551e9c041e | |||
fffb6d5b5e | |||
ac0bfeb360 | |||
7c30059ca3 | |||
fdb9ae6c40 | |||
3c82ffc0ab | |||
5dd3103aba | |||
84fc81f531 | |||
a20cbc62a5 | |||
e6a93e2838 | |||
3cff54561f | |||
e50a6a7876 | |||
b887ec839b | |||
465daa19a0 | |||
6c2b761d95 | |||
0e8f95ce19 | |||
6a17f343c6 | |||
1a45fb0039 | |||
75032898d6 | |||
88a4c97428 | |||
82e7a7edae | |||
eac28f97b8 | |||
e160882db9 | |||
2bc07e77fd | |||
c9b2db625c | |||
e3b41c9bd1 | |||
4aaee35d9c | |||
beedbc695a | |||
7123edc986 | |||
3008a754ce | |||
70e3fb8de6 | |||
9cf75da732 | |||
2cd266caff | |||
28036f1da5 | |||
0dacf2fe30 | |||
32f5ef5e5c | |||
98d91bbde7 | |||
7a92a75d83 | |||
f5556a02fc | |||
1050f4d928 | |||
9276b08f4b | |||
a501af669c | |||
85343fcefe | |||
b11dfde6e6 | |||
38d2108f02 | |||
2b67544517 | |||
0d443ca88e | |||
71f7a5819d | |||
5f4abee615 | |||
f5ee949006 | |||
7e85085558 | |||
55a0b27f16 | |||
eb0e814f94 | |||
b7fe20c5a5 | |||
2b23d03ca5 | |||
7075be20c8 | |||
3ce8b06246 | |||
ee5c29f30f | |||
242dad3ea0 | |||
d8701925df | |||
e2d669ce31 | |||
af93664c71 | |||
daa3efa534 | |||
2c7c8397f0 | |||
821ba2cbe2 | |||
a17ddb02fa | |||
b89557e8d8 | |||
cad1f8b957 | |||
f82cc788bf | |||
06f9cd3e68 | |||
5113a838e7 | |||
645a84c82a | |||
925fc43d0f | |||
8e33d24c63 | |||
984ef63661 | |||
a8daf175ea | |||
055263a3da | |||
9990b0ab05 | |||
423397ce3e | |||
954567712c | |||
9f52eb8123 | |||
744b198fb4 | |||
15eab797c3 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,3 +1,3 @@
|
|||||||
src/lib/i18n/generated_messages/* linguist-documentation
|
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
||||||
|
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
||||||
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
||||||
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
|
21
.github/workflows/ci.yaml
vendored
21
.github/workflows/ci.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
runs-on: macos-10.15
|
runs-on: macos-10.15
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [ '14', '13', '12' ]
|
node: [ '15', '14', '13' ]
|
||||||
name: Test with Node v${{ matrix.node }}
|
name: Test with Node v${{ matrix.node }}
|
||||||
steps:
|
steps:
|
||||||
- name: Tell if project is using npm or yarn
|
- name: Tell if project is using npm or yarn
|
||||||
@ -25,14 +25,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@v2.1.3
|
- uses: actions/setup-node@v2.1.3
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node }}
|
node-version: ${{ matrix.node }}
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
||||||
run: |
|
run: |
|
||||||
yarn install --frozen-lockfile
|
|
||||||
yarn build
|
yarn build
|
||||||
yarn test
|
yarn test
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
|
||||||
npm run build
|
npm run build
|
||||||
npm test
|
npm test
|
||||||
check_if_version_upgraded:
|
check_if_version_upgraded:
|
||||||
@ -55,11 +54,10 @@ jobs:
|
|||||||
needs: check_if_version_upgraded
|
needs: check_if_version_upgraded
|
||||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
||||||
steps:
|
steps:
|
||||||
- uses: garronej/github_actions_toolkit@v2.2
|
- uses: garronej/github_actions_toolkit@v2.4
|
||||||
with:
|
with:
|
||||||
action_name: update_changelog
|
action_name: update_changelog
|
||||||
branch: ${{ github.ref }}
|
branch: ${{ github.ref }}
|
||||||
commit_author_email: ts_ci@github.com
|
|
||||||
|
|
||||||
create_github_release:
|
create_github_release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -104,16 +102,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@v2.1.3
|
- uses: actions/setup-node@v2.1.3
|
||||||
with:
|
with:
|
||||||
node-version: '15'
|
node-version: '15'
|
||||||
|
registry-url: https://registry.npmjs.org/
|
||||||
|
- uses: bahmutov/npm-install@v1
|
||||||
- run: |
|
- run: |
|
||||||
PACKAGE_MANAGER=npm
|
PACKAGE_MANAGER=npm
|
||||||
if [ -f "./yarn.lock" ]; then
|
if [ -f "./yarn.lock" ]; then
|
||||||
PACKAGE_MANAGER=yarn
|
PACKAGE_MANAGER=yarn
|
||||||
fi
|
fi
|
||||||
if [ "$PACKAGE_MANAGER" = "yarn" ]; then
|
|
||||||
yarn install --frozen-lockfile
|
|
||||||
else
|
|
||||||
npm ci
|
|
||||||
fi
|
|
||||||
$PACKAGE_MANAGER run build
|
$PACKAGE_MANAGER run build
|
||||||
- run: npx -y -p denoify@0.6.5 denoify_enable_short_npm_import_path
|
- run: npx -y -p denoify@0.6.5 denoify_enable_short_npm_import_path
|
||||||
env:
|
env:
|
||||||
@ -124,13 +119,11 @@ jobs:
|
|||||||
echo "This version is already published"
|
echo "This version is already published"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
if [ "$NPM_TOKEN" = "" ]; then
|
if [ "$NODE_AUTH_TOKEN" = "" ]; then
|
||||||
echo "Can't publish on NPM, You must first create a secret called NPM_TOKEN that contains your NPM auth token. https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets"
|
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
|
false
|
||||||
fi
|
fi
|
||||||
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > .npmrc
|
|
||||||
npm publish
|
npm publish
|
||||||
rm .npmrc
|
|
||||||
env:
|
env:
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||||
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
118
CHANGELOG.md
118
CHANGELOG.md
@ -1,3 +1,121 @@
|
|||||||
|
### **2.0.10** (2021-07-16)
|
||||||
|
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
### **2.0.9** (2021-07-14)
|
||||||
|
|
||||||
|
- Fix #21
|
||||||
|
|
||||||
|
### **2.0.8** (2021-07-12)
|
||||||
|
|
||||||
|
- Fix previous release
|
||||||
|
- #20: Add def for clientId and name on kcContext.client
|
||||||
|
|
||||||
|
### **2.0.6** (2021-07-08)
|
||||||
|
|
||||||
|
- Merge pull request #18 from asashay/add-custom-props-to-theme-properties
|
||||||
|
|
||||||
|
Add possibility to add custom properties to theme.properties file
|
||||||
|
- add possibility to add custom properties to theme.properties file
|
||||||
|
|
||||||
|
### **2.0.5** (2021-07-05)
|
||||||
|
|
||||||
|
- Fix broken url for big stylesheet #16
|
||||||
|
|
||||||
|
### **2.0.4** (2021-07-03)
|
||||||
|
|
||||||
|
- Fix: #7
|
||||||
|
|
||||||
|
### **2.0.3** (2021-06-30)
|
||||||
|
|
||||||
|
- Escape double quote in ftl to js conversion #15
|
||||||
|
- Update readme
|
||||||
|
|
||||||
|
### **2.0.2** (2021-06-28)
|
||||||
|
|
||||||
|
- Updagte README for implementing non incuded pages
|
||||||
|
|
||||||
|
### **2.0.1** (2021-06-28)
|
||||||
|
|
||||||
|
- Update documentation for v2
|
||||||
|
|
||||||
|
# **2.0.0** (2021-06-28)
|
||||||
|
|
||||||
|
- Fix last bugs before relasing v2
|
||||||
|
- Implement a mechanism to overload kcContext
|
||||||
|
- Give the option in template to pull the default assets or not
|
||||||
|
- Enable possiblity to support custom pages (without forking keycloakify)
|
||||||
|
- Implement a getter for kcContext
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
# **2.0.0** (2021-06-28)
|
||||||
|
|
||||||
|
- Fix last bugs before relasing v2
|
||||||
|
- Implement a mechanism to overload kcContext
|
||||||
|
- Give the option in template to pull the default assets or not
|
||||||
|
- Enable possiblity to support custom pages (without forking keycloakify)
|
||||||
|
- Implement a getter for kcContext
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
### **1.2.1** (2021-06-22)
|
||||||
|
|
||||||
|
- Remove unessesary log
|
||||||
|
|
||||||
|
## **1.2.0** (2021-06-22)
|
||||||
|
|
||||||
|
- Generate kcContext automatically :rocket:
|
||||||
|
|
||||||
|
### **1.1.6** (2021-06-21)
|
||||||
|
|
||||||
|
- Fix: Alert messages sometimes includes HTML that is not rendered
|
||||||
|
- Update dist
|
||||||
|
|
||||||
|
### **1.1.5** (2021-06-15)
|
||||||
|
|
||||||
|
- #11: Provide socials in the register
|
||||||
|
|
||||||
|
### **1.1.4** (2021-06-15)
|
||||||
|
|
||||||
|
- Merge pull request #12 from InseeFrLab/email-typo
|
||||||
|
|
||||||
|
Fix typo on email
|
||||||
|
- Fix typo on email
|
||||||
|
|
||||||
|
### **1.1.3** (2021-06-14)
|
||||||
|
|
||||||
|
- Add missing key in Login for providers
|
||||||
|
|
||||||
|
### **1.1.2** (2021-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### **1.1.1** (2021-06-14)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## **1.1.0** (2021-06-14)
|
||||||
|
|
||||||
|
- Add login-idp-link-confirm.ftl
|
||||||
|
- Fix login-update-profile.ftl
|
||||||
|
- Add login-update-profile.ftl page
|
||||||
|
- Fix default background bug
|
||||||
|
- Remove unused 'markdown' dependency
|
||||||
|
- Fix warning related to powerhooks_useGlobalState_kcLanguageTag
|
||||||
|
- Update README.md
|
||||||
|
|
||||||
|
### **1.0.4** (2021-05-28)
|
||||||
|
|
||||||
|
- Instructions for custom themes with custom components
|
||||||
|
|
||||||
|
### **1.0.3** (2021-05-23)
|
||||||
|
|
||||||
|
- Instuction about how to integrate with non CRA projects
|
||||||
|
- Add mention to awesome list
|
||||||
|
|
||||||
|
### **1.0.2** (2021-05-01)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### **1.0.1** (2021-05-01)
|
### **1.0.1** (2021-05-01)
|
||||||
|
|
||||||
- Fix: LoginOtp (and not otc)
|
- Fix: LoginOtp (and not otc)
|
||||||
|
103
README.md
103
README.md
@ -9,6 +9,10 @@
|
|||||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
<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/dw/keycloakify">
|
||||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||||
|
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||||
|
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||||
|
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||||
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -16,6 +20,10 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
**NEW in v2**
|
||||||
|
- 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
|
# Motivations
|
||||||
|
|
||||||
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
|
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
|
||||||
@ -41,7 +49,7 @@ Here is `keycloakify` for you 🍸
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif">
|
<img src="https://user-images.githubusercontent.com/6702424/114332075-c5e37900-9b45-11eb-910b-48a05b3d90d9.gif">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
*NOTE: No autocomplete here just because it was an incognito window.*
|
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
|
||||||
|
|
||||||
If you already have a Keycloak custom theme, it can be easily ported to Keycloakify.
|
If you already have a Keycloak custom theme, it can be easily ported to Keycloakify.
|
||||||
|
|
||||||
@ -49,16 +57,17 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
|
|||||||
|
|
||||||
|
|
||||||
- [Motivations](#motivations)
|
- [Motivations](#motivations)
|
||||||
|
- [Requirements](#requirements)
|
||||||
|
- [My framework doesn’t seem to be supported, what can I do?](#my-framework-doesnt-seem-to-be-supported-what-can-i-do)
|
||||||
- [How to use](#how-to-use)
|
- [How to use](#how-to-use)
|
||||||
- [Setting up the build tool](#setting-up-the-build-tool)
|
- [Setting up the build tool](#setting-up-the-build-tool)
|
||||||
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
|
- [Changing just the look of the default Keycloak theme](#changing-just-the-look-of-the-default-keycloak-theme)
|
||||||
- [Changing the look **and** feel](#changing-the-look-and-feel)
|
- [Advanced pages configuration](#advanced-pages-configuration)
|
||||||
- [Hot reload](#hot-reload)
|
- [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)
|
- [Enable loading in a blink of an eye of login pages ⚡ (--external-assets)](#enable-loading-in-a-blink-of-an-eye-of-login-pages----external-assets)
|
||||||
- [Support for Terms and conditions](#support-for-terms-and-conditions)
|
- [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)
|
- [Some pages still have the default theme. Why?](#some-pages-still-have-the-default-theme-why)
|
||||||
- [GitHub Actions](#github-actions)
|
- [GitHub Actions](#github-actions)
|
||||||
- [Requirements](#requirements)
|
|
||||||
- [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-thesrc-dir)
|
||||||
@ -68,9 +77,30 @@ If you already have a Keycloak custom theme, it can be easily ported to Keycloak
|
|||||||
- [Kickstart video](#kickstart-video)
|
- [Kickstart video](#kickstart-video)
|
||||||
- [Email domain whitelist](#email-domain-whitelist)
|
- [Email domain whitelist](#email-domain-whitelist)
|
||||||
|
|
||||||
# How to use
|
# Requirements
|
||||||
|
|
||||||
**TL;DR**: [Here](https://github.com/garronej/keycloakify-demo-app) is a Hello World React project with Keycloakify set up.
|
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)
|
||||||
|
- Tests ongoing with [14.0.0](https://hub.docker.com/layers/jboss/keycloak/14.0.0/images/sha256-ca713e87ad163da71ab329010de2464a41ff030a25ae0aef15c1c290252f3d7f?context=explore)
|
||||||
|
|
||||||
|
This tool will be maintained to stay compatible with Keycloak v11 and up, however, the default pages you will get
|
||||||
|
(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) .
|
||||||
|
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)
|
||||||
|
## My framework doesn’t seem to be supported, what can I do?
|
||||||
|
|
||||||
|
Currently Keycloakify is only compatible with `create-react-app` apps.
|
||||||
|
It doesn’t mean that you can't use Keycloakify if you are using Next.js, Express or any other
|
||||||
|
framework that involves SSR but your Keycloak theme will need to be a standalone project.
|
||||||
|
Find specific instructions about how to get started [**here**](https://github.com/garronej/keycloakify-demo-app#keycloak-theme-only).
|
||||||
|
|
||||||
|
To share your styles between your main app and your login pages you will need to externalize your design system by making it a
|
||||||
|
separate module. Checkout [ts_ci](https://github.com/garronej/ts_ci), it can help with that.
|
||||||
|
# How to use
|
||||||
## Setting up the build tool
|
## Setting up the build tool
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -104,10 +134,12 @@ import { App } from "./<wherever>/App";
|
|||||||
import {
|
import {
|
||||||
KcApp,
|
KcApp,
|
||||||
defaultKcProps,
|
defaultKcProps,
|
||||||
kcContext
|
getKcContext
|
||||||
} from "keycloakify";
|
} from "keycloakify";
|
||||||
import { css } from "tss-react";
|
import { css } from "tss-react";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
const myClassName = css({ "color": "red" });
|
const myClassName = css({ "color": "red" });
|
||||||
|
|
||||||
reactDom.render(
|
reactDom.render(
|
||||||
@ -131,10 +163,12 @@ import { App } from "./<wherever>/App";
|
|||||||
import {
|
import {
|
||||||
KcApp,
|
KcApp,
|
||||||
defaultKcProps,
|
defaultKcProps,
|
||||||
kcContext
|
getKcContext
|
||||||
} from "keycloakify";
|
} from "keycloakify";
|
||||||
import { css } from "tss-react";
|
import { css } from "tss-react";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
const myClassName = css({ "color": "red" });
|
const myClassName = css({ "color": "red" });
|
||||||
|
|
||||||
reactDom.render(
|
reactDom.render(
|
||||||
@ -169,15 +203,25 @@ and the result you can expect:
|
|||||||
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
|
<img src="https://github.com/InseeFrLab/keycloakify/releases/download/v0.3.8/keycloakify_after.gif">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
### Changing the look **and** feel
|
### Advanced pages configuration
|
||||||
|
|
||||||
If you want to really re-implement the pages, the best approach is to
|
If you want to go beyond only customizing the CSS you can re-implement some of the
|
||||||
create your own version of the [`<KcApp />`](https://github.com/garronej/keycloakify/blob/develop/src/lib/components/KcApp.tsx).
|
pages or event add new ones.
|
||||||
Copy/past some of [the components](https://github.com/garronej/keycloakify/tree/develop/src/lib/components) provided by this module and start hacking around.
|
|
||||||
|
|
||||||
You can find an example of such customization [here](https://github.com/InseeFrLab/onyxia-ui/tree/master/src/app/components/KcApp).
|
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).
|
||||||
|
The web app is in production [here](https://datalab.sspcloud.fr).
|
||||||
|
|
||||||
And you can test the result in production by trying the login register page of [Onyxia](https://datalab.sspcloud.fr)
|
Main takeaways are:
|
||||||
|
- You must declare your custom pages in the package.json. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/package.json#L17-L22)
|
||||||
|
- (TS only) You must declare theses page in the type argument of the getter
|
||||||
|
function for the `kcContext` in order to have the correct typings. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L16-L21)
|
||||||
|
- (TS only) If you use Keycloak plugins that defines non standard `.ftl` values
|
||||||
|
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
|
||||||
|
that define `authorizedMailDomains` in `register.ftl`) you should
|
||||||
|
declare theses value to get the type. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L6-L13)
|
||||||
|
- You should provide sample data for all the non standard value if you want to be able
|
||||||
|
to debug the page outside of keycloak. [example](https://github.com/garronej/keycloakify-demo-app/blob/4eb2a9f63e9823e653b2d439495bda55e5ecc134/src/KcApp/kcContext.ts#L28-L43)
|
||||||
|
|
||||||
WARNING: If you chose to go this way use:
|
WARNING: If you chose to go this way use:
|
||||||
```json
|
```json
|
||||||
@ -185,21 +229,24 @@ WARNING: If you chose to go this way use:
|
|||||||
"keycloakify": "~X.Y.Z"
|
"keycloakify": "~X.Y.Z"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
in your `package.json` instead of `^X.Y.Z`. A minor release might break your app.
|
in your `package.json` instead of `^X.Y.Z`. A minor update of Keycloakify might break your app.
|
||||||
|
|
||||||
### Hot reload
|
### Hot reload
|
||||||
|
|
||||||
Rebuild the theme each time you make a change to see the result is not practical.
|
Rebuild the theme each time you make a change to see the result is not practical.
|
||||||
If you want to test your login screens outside of Keycloak, in [storybook](https://storybook.js.org/)
|
If you want to test your login screens outside of Keycloak you can mock a given `kcContext`:
|
||||||
for example you can use `kcContextMocks`.
|
|
||||||
|
|
||||||
```tsx
|
```tsx
|
||||||
import {
|
import {
|
||||||
KcApp,
|
KcApp,
|
||||||
defaultKcProps,
|
defaultKcProps,
|
||||||
kcContextMocks
|
getKcContext
|
||||||
} from "keycloakify";
|
} from "keycloakify";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({
|
||||||
|
"mockPageId": "login.ftl"
|
||||||
|
});
|
||||||
|
|
||||||
reactDom.render(
|
reactDom.render(
|
||||||
<KcApp
|
<KcApp
|
||||||
kcContext={kcContextMocks.kcLoginContext}
|
kcContext={kcContextMocks.kcLoginContext}
|
||||||
@ -241,12 +288,10 @@ Then to load your own therms of services using [like this](https://github.com/ga
|
|||||||
|
|
||||||
# Some pages still have the default theme. Why?
|
# Some pages still have the default theme. Why?
|
||||||
|
|
||||||
This project only support the most common user facing pages of Keycloak login.
|
This project only support out of the box the most common user facing pages of Keycloak login.
|
||||||
[Here is](https://user-images.githubusercontent.com/6702424/116784128-d4f97f00-aa92-11eb-92c9-b024c2521aa3.png) the complete list of pages.
|
[Here](https://user-images.githubusercontent.com/6702424/116787906-227fe700-aaa7-11eb-92ee-22e7673717c2.png) is the complete list of pages (you get them after running `yarn test`)
|
||||||
And [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
|
and [here](https://github.com/InseeFrLab/keycloakify/tree/main/src/lib/components) are the pages currently implemented by this module.
|
||||||
If you need to customize pages that are not supported yet you can submit an issue about it and wait for me get it implemented.
|
If you need to customize pages that are not supported yet or if you need to implement some non standard `.ftl` pages please refer to [Advanced pages configuration](#advanced-pages-configuration).
|
||||||
If you can't wait, PR are welcome! [Here](https://github.com/InseeFrLab/keycloakify/commit/0163459ad6b1ad0afcc34fae5f3cc28dbcf8b4a7) is the commit that adds support
|
|
||||||
for the `login-otp.ftl` page. You can use it as a model for implementing other pages.
|
|
||||||
|
|
||||||
# GitHub Actions
|
# GitHub Actions
|
||||||
|
|
||||||
@ -255,18 +300,6 @@ for the `login-otp.ftl` page. You can use it as a model for implementing other p
|
|||||||
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
[Here is a demo repo](https://github.com/garronej/keycloakify-demo-app) to show how to automate
|
||||||
the building and publishing of the theme (the .jar file).
|
the building and publishing of the theme (the .jar file).
|
||||||
|
|
||||||
# Requirements
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
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) .
|
|
||||||
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.
|
|
||||||
|
|
||||||
**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)
|
||||||
|
|
||||||
|
17
package.json
17
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "1.0.1",
|
"version": "2.0.10",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -12,7 +12,7 @@
|
|||||||
"clean": "rimraf dist/",
|
"clean": "rimraf dist/",
|
||||||
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"test": "node dist/test",
|
"test": "node dist/test/bin && node dist/test/lib",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js"
|
"generate-messages": "node dist/bin/generate-i18n-messages.js"
|
||||||
},
|
},
|
||||||
@ -46,17 +46,18 @@
|
|||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"typescript": "^4.2.3"
|
"typescript": "^4.2.3",
|
||||||
|
"ts-toolbelt": "^9.6.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"evt": "2.0.0-beta.15",
|
"evt": "2.0.0-beta.21",
|
||||||
"markdown": "^0.5.0",
|
"minimal-polyfills": "^2.2.1",
|
||||||
"minimal-polyfills": "^2.1.6",
|
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
"powerhooks": "^0.0.36",
|
"powerhooks": "^0.5.0",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"scripting-tools": "^0.19.13",
|
||||||
"tss-react": "^0.0.12"
|
"tss-react": "^0.3.3",
|
||||||
|
"tsafe": "^0.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
116
src/bin/build-keycloak-theme/build-keycloak-theme.ts
Normal file
116
src/bin/build-keycloak-theme/build-keycloak-theme.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||||
|
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||||
|
import type { ParsedPackageJson } from "./generateJavaStackFiles";
|
||||||
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
||||||
|
import { URL } from "url";
|
||||||
|
|
||||||
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
|
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
||||||
|
|
||||||
|
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
||||||
|
|
||||||
|
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||||
|
|
||||||
|
export function main() {
|
||||||
|
|
||||||
|
console.log("🔏 Building the keycloak theme...⌚");
|
||||||
|
|
||||||
|
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
|
||||||
|
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
|
||||||
|
|
||||||
|
generateKeycloakThemeResources({
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||||
|
"themeName": parsedPackageJson.name,
|
||||||
|
...(() => {
|
||||||
|
|
||||||
|
const url = (() => {
|
||||||
|
|
||||||
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
return homepage === undefined ?
|
||||||
|
undefined :
|
||||||
|
new URL(homepage);
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
|
return {
|
||||||
|
"urlPathname":
|
||||||
|
url === undefined ?
|
||||||
|
"/" :
|
||||||
|
url.pathname.replace(/([^/])$/, "$1/"),
|
||||||
|
"urlOrigin": !doUseExternalAssets ? undefined : (() => {
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
console.error("ERROR: You must specify 'homepage' in your package.json");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return url.origin;
|
||||||
|
|
||||||
|
})()
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
})(),
|
||||||
|
extraPagesId,
|
||||||
|
extraThemeProperties
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
|
parsedPackageJson,
|
||||||
|
keycloakThemeBuildingDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
child_process.execSync(
|
||||||
|
"mvn package",
|
||||||
|
{ "cwd": keycloakThemeBuildingDirPath }
|
||||||
|
);
|
||||||
|
|
||||||
|
generateDebugFiles({
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
"packageJsonName": parsedPackageJson.name
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log([
|
||||||
|
'',
|
||||||
|
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||||
|
`It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image. (Tested with 11.0.3)`,
|
||||||
|
'',
|
||||||
|
'Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:',
|
||||||
|
'',
|
||||||
|
'value.yaml: ',
|
||||||
|
' extraInitContainers: |',
|
||||||
|
' - name: realm-ext-provider',
|
||||||
|
' image: curlimages/curl',
|
||||||
|
' imagePullPolicy: IfNotPresent',
|
||||||
|
' command:',
|
||||||
|
' - sh',
|
||||||
|
' args:',
|
||||||
|
' - -c',
|
||||||
|
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
|
||||||
|
' volumeMounts:',
|
||||||
|
' - name: extensions',
|
||||||
|
' mountPath: /extensions',
|
||||||
|
' ',
|
||||||
|
' extraVolumeMounts: |',
|
||||||
|
' - name: extensions',
|
||||||
|
' mountPath: /opt/jboss/keycloak/standalone/deployments',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:',
|
||||||
|
'',
|
||||||
|
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, 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 ${parsedPackageJson.name}.`,
|
||||||
|
`More details: https://www.keycloak.org/getting-started/getting-started-docker`,
|
||||||
|
'',
|
||||||
|
'Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈',
|
||||||
|
'',
|
||||||
|
].join("\n"));
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
|
||||||
|
|
||||||
|
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
|
/** Files for being able to run a hot reload keycloak container */
|
||||||
|
export function generateDebugFiles(
|
||||||
|
params: {
|
||||||
|
packageJsonName: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
const { packageJsonName, keycloakThemeBuildingDirPath } = params;
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
"FROM jboss/keycloak:11.0.3",
|
||||||
|
"",
|
||||||
|
"USER root",
|
||||||
|
"",
|
||||||
|
"WORKDIR /",
|
||||||
|
"",
|
||||||
|
"ADD configuration /opt/jboss/keycloak/standalone/configuration/",
|
||||||
|
"",
|
||||||
|
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const dockerImage = `${packageJsonName}/keycloak-hot-reload`;
|
||||||
|
const containerName = "keycloak-testing-container";
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
"#!/bin/bash",
|
||||||
|
"",
|
||||||
|
`cd ${keycloakThemeBuildingDirPath}`,
|
||||||
|
"",
|
||||||
|
`docker rm ${containerName} || true`,
|
||||||
|
"",
|
||||||
|
`docker build . -t ${dockerImage}`,
|
||||||
|
"",
|
||||||
|
"docker run \\",
|
||||||
|
" -p 8080:8080 \\",
|
||||||
|
` --name ${containerName} \\`,
|
||||||
|
" -e KEYCLOAK_USER=admin \\",
|
||||||
|
" -e KEYCLOAK_PASSWORD=admin \\",
|
||||||
|
` -v ${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", packageJsonName)
|
||||||
|
}:/opt/jboss/keycloak/themes/${packageJsonName}:rw \\`,
|
||||||
|
` -it ${dockerImage}:latest`,
|
||||||
|
""
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
),
|
||||||
|
{ "mode": 0o755 }
|
||||||
|
);
|
||||||
|
|
||||||
|
const standaloneHaFilePath = pathJoin(keycloakThemeBuildingDirPath, "configuration", "standalone-ha.xml");
|
||||||
|
|
||||||
|
try { fs.mkdirSync(pathDirname(standaloneHaFilePath)); } catch { }
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
standaloneHaFilePath,
|
||||||
|
fs.readFileSync(pathJoin(__dirname, pathBasename(standaloneHaFilePath)))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
@ -1,74 +1 @@
|
|||||||
|
export * from "./generateDebugFiles";
|
||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, dirname as pathDirname, basename as pathBasename } from "path";
|
|
||||||
|
|
||||||
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
|
||||||
|
|
||||||
/** Files for being able to run a hot reload keycloak container */
|
|
||||||
export function generateDebugFiles(
|
|
||||||
params: {
|
|
||||||
packageJsonName: string;
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
const { packageJsonName, keycloakThemeBuildingDirPath } = params;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
"FROM jboss/keycloak:11.0.3",
|
|
||||||
"",
|
|
||||||
"USER root",
|
|
||||||
"",
|
|
||||||
"WORKDIR /",
|
|
||||||
"",
|
|
||||||
"ADD configuration /opt/jboss/keycloak/standalone/configuration/",
|
|
||||||
"",
|
|
||||||
'ENTRYPOINT [ "/opt/jboss/tools/docker-entrypoint.sh" ]',
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const dockerImage = `${packageJsonName}/keycloak-hot-reload`;
|
|
||||||
const containerName = "keycloak-testing-container";
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakThemeBuildingDirPath, containerLaunchScriptBasename),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
"#!/bin/bash",
|
|
||||||
"",
|
|
||||||
`cd ${keycloakThemeBuildingDirPath}`,
|
|
||||||
"",
|
|
||||||
`docker rm ${containerName} || true`,
|
|
||||||
"",
|
|
||||||
`docker build . -t ${dockerImage}`,
|
|
||||||
"",
|
|
||||||
"docker run \\",
|
|
||||||
" -p 8080:8080 \\",
|
|
||||||
` --name ${containerName} \\`,
|
|
||||||
" -e KEYCLOAK_USER=admin \\",
|
|
||||||
" -e KEYCLOAK_PASSWORD=admin \\",
|
|
||||||
` -v ${pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", packageJsonName)
|
|
||||||
}:/opt/jboss/keycloak/themes/${packageJsonName}:rw \\`,
|
|
||||||
` -it ${dockerImage}:latest`,
|
|
||||||
""
|
|
||||||
].join("\n"),
|
|
||||||
"utf8"
|
|
||||||
),
|
|
||||||
{ "mode": 0o755 }
|
|
||||||
);
|
|
||||||
|
|
||||||
const standaloneHaFilePath = pathJoin(keycloakThemeBuildingDirPath, "configuration", "standalone-ha.xml");
|
|
||||||
|
|
||||||
try { fs.mkdirSync(pathDirname(standaloneHaFilePath)); } catch { }
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
standaloneHaFilePath,
|
|
||||||
fs.readFileSync(pathJoin(__dirname, pathBasename(standaloneHaFilePath)))
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
|
|
||||||
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;
|
|
||||||
|
|
||||||
var unes = {
|
|
||||||
'&': '&',
|
|
||||||
'&': '&',
|
|
||||||
'<': '<',
|
|
||||||
'<': '<',
|
|
||||||
'>': '>',
|
|
||||||
'>': '>',
|
|
||||||
''': "'",
|
|
||||||
''': "'",
|
|
||||||
'"': '"',
|
|
||||||
'"': '"'
|
|
||||||
};
|
|
||||||
var cape = function (m) { return unes[m]; };
|
|
||||||
|
|
||||||
Object.defineProperty(
|
|
||||||
String,
|
|
||||||
"htmlUnescape",
|
|
||||||
{
|
|
||||||
"value": function (un) {
|
|
||||||
return String.prototype.replace.call(un, es, cape);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
@ -1,263 +1,192 @@
|
|||||||
<script>const _=
|
<script>const _=
|
||||||
{
|
<#macro objectToJson object depth>
|
||||||
"url": {
|
<@compress>
|
||||||
"loginAction": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.loginAction?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"resourcesPath": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.resourcesPath?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"resourcesCommonPath": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.resourcesCommonPath?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"loginRestartFlowUrl": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.loginRestartFlowUrl?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"loginUrl": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.loginUrl?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"realm": {
|
|
||||||
"displayName": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${realm.displayName!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"displayNameHtml": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${realm.displayNameHtml!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"internationalizationEnabled": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.internationalizationEnabled?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"registrationEmailAsUsername": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.registrationEmailAsUsername?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"locale": (function (){
|
|
||||||
|
|
||||||
|
<#local isHash = false>
|
||||||
<#attempt>
|
<#attempt>
|
||||||
<#if realm.internationalizationEnabled>
|
<#local isHash = object?is_hash || object?is_hash_ex>
|
||||||
|
|
||||||
return {
|
|
||||||
"supported": (function(){
|
|
||||||
|
|
||||||
var out= [];
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list locale.supported as lng>
|
|
||||||
out.push({
|
|
||||||
"url": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${lng.url?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"label": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${lng.label}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"languageTag": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${lng.languageTag}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
});
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"current": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${locale.current}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
<#recover>
|
||||||
|
/* can't evaluate if object is hash */
|
||||||
|
undefined
|
||||||
|
<#return>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
<#if isHash>
|
||||||
|
|
||||||
})(),
|
<#local keys = "">
|
||||||
"auth": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if auth?has_content>
|
|
||||||
|
|
||||||
var out= {
|
|
||||||
"showUsername": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${auth.showUsername()?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"showResetCredentials": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${auth.showResetCredentials()?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"showTryAnotherWayLink": (function(){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${auth.showTryAnotherWayLink()?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
<#if auth.showUsername() && !auth.showResetCredentials()>
|
<#local keys = object?keys>
|
||||||
Object.assign(
|
|
||||||
out,
|
|
||||||
{
|
|
||||||
"attemptedUsername": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${auth.attemptedUsername}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
<#recover>
|
||||||
|
/* can't list keys of object */
|
||||||
|
undefined
|
||||||
|
<#return>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
|
||||||
return out;
|
{${'\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 4>
|
||||||
|
/* Avoid calling recustively too many times depth: ${depth}, key: ${key} */
|
||||||
|
<#continue>
|
||||||
|
</#if>
|
||||||
|
|
||||||
|
"${key}": <@objectToJson object=value depth=depth+1/>,
|
||||||
|
|
||||||
|
</#list>
|
||||||
|
|
||||||
|
}${'\n'}
|
||||||
|
|
||||||
|
<#return>
|
||||||
|
|
||||||
</#if>
|
</#if>
|
||||||
|
|
||||||
|
|
||||||
|
<#local isMethod = "">
|
||||||
|
<#attempt>
|
||||||
|
<#local isMethod = object?is_method>
|
||||||
<#recover>
|
<#recover>
|
||||||
|
/* can't test if object is a method */
|
||||||
|
undefined
|
||||||
|
<#return>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
|
||||||
})(),
|
<#if isMethod>
|
||||||
"scripts": (function(){
|
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>
|
||||||
|
|
||||||
var out = [];
|
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
<#if scripts??>
|
"${object?replace('"', '\\"')?no_esc}"
|
||||||
<#attempt>
|
<#recover>
|
||||||
<#list scripts as script>
|
/* couldn't convert into string non hash, non method, non boolean, non enumerable object */
|
||||||
out.push((function (){
|
undefined;
|
||||||
|
<#return>
|
||||||
|
</#attempt>
|
||||||
|
|
||||||
|
|
||||||
|
</@compress>
|
||||||
|
</#macro>
|
||||||
|
|
||||||
|
(()=>{
|
||||||
|
|
||||||
|
//Removing all the undefined
|
||||||
|
const obj = JSON.parse(JSON.stringify(<@objectToJson object=.data_model depth=0 />));
|
||||||
|
|
||||||
|
//Freemarker values that can't be automatically converted into a JavaScript object.
|
||||||
|
Object.deepAssign(
|
||||||
|
obj,
|
||||||
|
{
|
||||||
|
"messagesPerField": {
|
||||||
|
"printIfExists": function (key, x) {
|
||||||
|
switch(key){
|
||||||
|
case "userLabel": return (function (){
|
||||||
<#attempt>
|
<#attempt>
|
||||||
return "${script}";
|
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "username": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "email": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "firstName": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "lastName": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "password": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
case "password-confirm": return (function (){
|
||||||
|
<#attempt>
|
||||||
|
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"msg": function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
})());
|
return obj;
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
})()
|
||||||
|
|
||||||
})(),
|
|
||||||
"message": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if message?has_content>
|
|
||||||
|
|
||||||
return {
|
|
||||||
"type": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${message.type}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"summary": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return String.htmlUnescape("${message.summary}");
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"isAppInitiatedAction": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if isAppInitiatedAction??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
@ -1,23 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"client": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if client??>
|
|
||||||
return {
|
|
||||||
"baseUrl": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${(client.baseUrl!'')?no_esc}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</script>
|
|
170
src/bin/build-keycloak-theme/generateFtl/generateFtl.ts
Normal file
170
src/bin/build-keycloak-theme/generateFtl/generateFtl.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import cheerio from "cheerio";
|
||||||
|
import {
|
||||||
|
replaceImportsFromStaticInJsCode,
|
||||||
|
replaceImportsInInlineCssCode,
|
||||||
|
generateCssCodeToDefineGlobals
|
||||||
|
} from "../replaceImportFromStatic";
|
||||||
|
import fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
|
||||||
|
export const pageIds = [
|
||||||
|
"login.ftl", "register.ftl", "info.ftl",
|
||||||
|
"error.ftl", "login-reset-password.ftl",
|
||||||
|
"login-verify-email.ftl", "terms.ftl",
|
||||||
|
"login-otp.ftl", "login-update-profile.ftl",
|
||||||
|
"login-idp-link-confirm.ftl"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
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;
|
||||||
|
urlPathname: string;
|
||||||
|
urlOrigin: undefined | string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(indexHtmlCode);
|
||||||
|
|
||||||
|
$("script:not([src])").each((...[, element]) => {
|
||||||
|
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": $(element).html()!,
|
||||||
|
urlOrigin
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedJsCode);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
$("style").each((...[, element]) => {
|
||||||
|
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
"cssCode": $(element).html()!,
|
||||||
|
"urlPathname": params.urlPathname,
|
||||||
|
urlOrigin
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedCssCode);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
([
|
||||||
|
["link", "href"],
|
||||||
|
["script", "src"],
|
||||||
|
] as const).forEach(([selector, attrName]) =>
|
||||||
|
$(selector).each((...[, element]) => {
|
||||||
|
|
||||||
|
const href = $(element).attr(attrName);
|
||||||
|
|
||||||
|
if (href === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).attr(
|
||||||
|
attrName,
|
||||||
|
urlOrigin !== undefined ?
|
||||||
|
href.replace(/^\//, `${urlOrigin}/`) :
|
||||||
|
href.replace(
|
||||||
|
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
|
||||||
|
"${url.resourcesPath}/build/"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
//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],
|
||||||
|
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
|
||||||
|
[
|
||||||
|
'<#if scripts??>',
|
||||||
|
' <#list scripts as script>',
|
||||||
|
' <script src="${script}" type="text/javascript"></script>',
|
||||||
|
' </#list>',
|
||||||
|
'</#if>'
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
|
||||||
|
|
||||||
|
$("head").prepend(
|
||||||
|
[
|
||||||
|
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
|
||||||
|
'',
|
||||||
|
'<style>',
|
||||||
|
generateCssCodeToDefineGlobals({
|
||||||
|
cssGlobalsToDefine,
|
||||||
|
urlPathname
|
||||||
|
}).cssCodeToPrependInHead,
|
||||||
|
'</style>',
|
||||||
|
''
|
||||||
|
]),
|
||||||
|
"<script>",
|
||||||
|
loadAdjacentFile("Object.deepAssign.js"),
|
||||||
|
"</script>",
|
||||||
|
'<script>',
|
||||||
|
` window.${ftlValuesGlobalName}= Object.assign(`,
|
||||||
|
` {},`,
|
||||||
|
` ${objectKeys(ftlPlaceholders)[0]}`,
|
||||||
|
' );',
|
||||||
|
'</script>',
|
||||||
|
'',
|
||||||
|
pageSpecificCodePlaceholder,
|
||||||
|
'',
|
||||||
|
objectKeys(ftlPlaceholders)[1]
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
|
||||||
|
const partiallyFixedIndexHtmlCode = $.html();
|
||||||
|
|
||||||
|
function generateFtlFilesCode(
|
||||||
|
params: {
|
||||||
|
pageId: string;
|
||||||
|
}
|
||||||
|
): { ftlCode: string; } {
|
||||||
|
|
||||||
|
const { pageId } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||||
|
|
||||||
|
let ftlCode = $.html()
|
||||||
|
.replace(
|
||||||
|
pageSpecificCodePlaceholder,
|
||||||
|
[
|
||||||
|
'<script>',
|
||||||
|
` Object.deepAssign(`,
|
||||||
|
` window.${ftlValuesGlobalName},`,
|
||||||
|
` { "pageId": "${pageId}" }`,
|
||||||
|
' );',
|
||||||
|
'</script>'
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
objectKeys(ftlPlaceholders)
|
||||||
|
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id]));
|
||||||
|
|
||||||
|
return { ftlCode };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return { generateFtlFilesCode };
|
||||||
|
|
||||||
|
|
||||||
|
}
|
@ -1,192 +1 @@
|
|||||||
|
export * from "./generateFtl";
|
||||||
|
|
||||||
import cheerio from "cheerio";
|
|
||||||
import {
|
|
||||||
replaceImportsFromStaticInJsCode,
|
|
||||||
replaceImportsInInlineCssCode,
|
|
||||||
generateCssCodeToDefineGlobals
|
|
||||||
} from "../replaceImportFromStatic";
|
|
||||||
import fs from "fs";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
|
||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export const pageIds = [
|
|
||||||
"login.ftl", "register.ftl", "info.ftl",
|
|
||||||
"error.ftl", "login-reset-password.ftl",
|
|
||||||
"login-verify-email.ftl", "terms.ftl",
|
|
||||||
"login-otp.ftl"
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type PageId = typeof pageIds[number];
|
|
||||||
|
|
||||||
function loadAdjacentFile(fileBasename: string) {
|
|
||||||
return fs.readFileSync(pathJoin(__dirname, fileBasename))
|
|
||||||
.toString("utf8");
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadFtlFile(ftlFileBasename: PageId | "common.ftl") {
|
|
||||||
try {
|
|
||||||
|
|
||||||
return loadAdjacentFile(ftlFileBasename)
|
|
||||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1];
|
|
||||||
|
|
||||||
} catch {
|
|
||||||
return "{}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(
|
|
||||||
params: {
|
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
|
||||||
indexHtmlCode: string;
|
|
||||||
urlPathname: string;
|
|
||||||
urlOrigin: undefined | string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
|
|
||||||
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
|
||||||
|
|
||||||
const $ = cheerio.load(indexHtmlCode);
|
|
||||||
|
|
||||||
$("script:not([src])").each((...[, element]) => {
|
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
|
||||||
"jsCode": $(element).html()!,
|
|
||||||
urlOrigin
|
|
||||||
});
|
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
$("style").each((...[, element]) => {
|
|
||||||
|
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
|
||||||
"cssCode": $(element).html()!,
|
|
||||||
"urlPathname": params.urlPathname,
|
|
||||||
urlOrigin
|
|
||||||
});
|
|
||||||
|
|
||||||
$(element).text(fixedCssCode);
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
([
|
|
||||||
["link", "href"],
|
|
||||||
["script", "src"],
|
|
||||||
] as const).forEach(([selector, attrName]) =>
|
|
||||||
$(selector).each((...[, element]) => {
|
|
||||||
|
|
||||||
const href = $(element).attr(attrName);
|
|
||||||
|
|
||||||
if (href === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(element).attr(
|
|
||||||
attrName,
|
|
||||||
urlOrigin !== undefined ?
|
|
||||||
href.replace(/^\//, `${urlOrigin}/`) :
|
|
||||||
href.replace(
|
|
||||||
new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`),
|
|
||||||
"${url.resourcesPath}/build/"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
|
||||||
const ftlCommonPlaceholders = {
|
|
||||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': loadFtlFile("common.ftl"),
|
|
||||||
'<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->':
|
|
||||||
[
|
|
||||||
'<#if scripts??>',
|
|
||||||
' <#list scripts as script>',
|
|
||||||
' <script src="${script}" type="text/javascript"></script>',
|
|
||||||
' </#list>',
|
|
||||||
'</#if>'
|
|
||||||
].join("\n")
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageSpecificCodePlaceholder = "<!-- dIddLqMeOedErIdLsPdNdI9dSl42sw -->";
|
|
||||||
|
|
||||||
$("head").prepend(
|
|
||||||
[
|
|
||||||
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
|
|
||||||
'',
|
|
||||||
'<style>',
|
|
||||||
generateCssCodeToDefineGlobals({
|
|
||||||
cssGlobalsToDefine,
|
|
||||||
urlPathname
|
|
||||||
}).cssCodeToPrependInHead,
|
|
||||||
'</style>',
|
|
||||||
''
|
|
||||||
]),
|
|
||||||
...["Object.deepAssign.js", "String.htmlUnescape.js"].map(
|
|
||||||
fileBasename => [
|
|
||||||
"<script>",
|
|
||||||
loadAdjacentFile(fileBasename),
|
|
||||||
"</script>"
|
|
||||||
].join("\n")
|
|
||||||
),
|
|
||||||
'<script>',
|
|
||||||
` window.${ftlValuesGlobalName}= Object.assign(`,
|
|
||||||
` {},`,
|
|
||||||
` ${objectKeys(ftlCommonPlaceholders)[0]}`,
|
|
||||||
' );',
|
|
||||||
'</script>',
|
|
||||||
'',
|
|
||||||
pageSpecificCodePlaceholder,
|
|
||||||
'',
|
|
||||||
objectKeys(ftlCommonPlaceholders)[1]
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const partiallyFixedIndexHtmlCode = $.html();
|
|
||||||
|
|
||||||
function generateFtlFilesCode(
|
|
||||||
params: {
|
|
||||||
pageId: PageId;
|
|
||||||
}
|
|
||||||
): { ftlCode: string; } {
|
|
||||||
|
|
||||||
const { pageId } = params;
|
|
||||||
|
|
||||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
|
||||||
|
|
||||||
const ftlPlaceholders = {
|
|
||||||
'{ "x": "kxOlLqMeOed9sdLdIdOxd444" }': loadFtlFile(pageId),
|
|
||||||
...ftlCommonPlaceholders
|
|
||||||
};
|
|
||||||
|
|
||||||
let ftlCode = $.html()
|
|
||||||
.replace(
|
|
||||||
pageSpecificCodePlaceholder,
|
|
||||||
[
|
|
||||||
'<script>',
|
|
||||||
` Object.deepAssign(`,
|
|
||||||
` window.${ftlValuesGlobalName},`,
|
|
||||||
` { "pageId": "${pageId}" }`,
|
|
||||||
' );',
|
|
||||||
` Object.deepAssign(`,
|
|
||||||
` window.${ftlValuesGlobalName},`,
|
|
||||||
` ${objectKeys(ftlPlaceholders)[0]}`,
|
|
||||||
' );',
|
|
||||||
'</script>'
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
|
|
||||||
objectKeys(ftlPlaceholders)
|
|
||||||
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id]));
|
|
||||||
|
|
||||||
return { ftlCode };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return { generateFtlFilesCode };
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"messageHeader": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messageHeader!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"requiredActions": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if requiredActions??>
|
|
||||||
|
|
||||||
var out =[];
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list requiredActions>
|
|
||||||
<#attempt>
|
|
||||||
<#items as reqActionItem>
|
|
||||||
out.push((function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${reqActionItem}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})());
|
|
||||||
</#items>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"skipLink": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if skipLink??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"pageRedirectUri": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${(pageRedirectUri!'')?no_esc}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"actionUri": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${(actionUri!'')?no_esc}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"client": {
|
|
||||||
"baseUrl": (function(){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${(client.baseUrl!'')?no_esc}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,37 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"otpLogin": {
|
|
||||||
"userOtpCredentials": (function(){
|
|
||||||
|
|
||||||
var out = [];
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list otpLogin.userOtpCredentials as otpCredential>
|
|
||||||
out.push({
|
|
||||||
"id": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${otpCredential.id}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"userLabel": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${otpCredential.userLabel}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
});
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,14 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"realm": {
|
|
||||||
"loginWithEmailAllowed": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.loginWithEmailAllowed?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,160 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"url": {
|
|
||||||
"loginResetCredentialsUrl": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${url.loginResetCredentialsUrl?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"registrationUrl": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${url.registrationUrl?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"realm": {
|
|
||||||
"loginWithEmailAllowed": (function(){
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.loginWithEmailAllowed?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"rememberMe": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.rememberMe?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"password": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.password?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"resetPasswordAllowed": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.resetPasswordAllowed?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"registrationAllowed": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return ${realm.registrationAllowed?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"auth": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if auth?has_content>
|
|
||||||
|
|
||||||
return {
|
|
||||||
"selectedCredential": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${auth.selectedCredential!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})()
|
|
||||||
};
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"social": {
|
|
||||||
"displayInfo": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return ${social.displayInfo?c};
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"providers": (()=>{
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if social.providers??>
|
|
||||||
|
|
||||||
var out= [];
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list social.providers as p>
|
|
||||||
out.push({
|
|
||||||
"loginUrl": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${p.loginUrl?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"alias": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${p.alias}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"providerId": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${p.providerId}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"displayName": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${p.displayName}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})()
|
|
||||||
});
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"usernameEditDisabled": (function () {
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if usernameEditDisabled??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"login": {
|
|
||||||
"username": (function (){
|
|
||||||
<#attempt>
|
|
||||||
return "${login.username!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
})(),
|
|
||||||
"rememberMe": (function (){
|
|
||||||
<#attempt>
|
|
||||||
<#if login.rememberMe??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
return false;
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"registrationDisabled": (function (){
|
|
||||||
<#attempt>
|
|
||||||
<#if registrationDisabled??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
return false;
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -1,189 +0,0 @@
|
|||||||
<script>const _=
|
|
||||||
{
|
|
||||||
"url": {
|
|
||||||
"registrationAction": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${url.registrationAction?no_esc}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
},
|
|
||||||
"messagesPerField": {
|
|
||||||
"printIfExists": function (key, x) {
|
|
||||||
switch(key){
|
|
||||||
case "userLabel": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('userLabel','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "username": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('username','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "email": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('email','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "firstName": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('firstName','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "lastName": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('lastName','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "password": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('password','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
case "password-confirm": return (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${messagesPerField.printIfExists('password-confirm','1')}" ? x : undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"register": {
|
|
||||||
"formData": {
|
|
||||||
"firstName": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${register.formData.firstName!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"displayName": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${register.formData.displayName!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"lastName": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${register.formData.lastName!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"email": (function(){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${register.formData.email!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"username": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${register.formData.username!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"passwordRequired": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if passwordRequired??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"recaptchaRequired": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if passwordRequired??>
|
|
||||||
return true;
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"recaptchaSiteKey": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${recaptchaSiteKey!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"authorizedMailDomains": (function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${authorizedMailDomains!''}" || undefined;
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})(),
|
|
||||||
"authorizedMailDomains": (function(){
|
|
||||||
|
|
||||||
var out = undefined;
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#if authorizedMailDomains??>
|
|
||||||
|
|
||||||
out = [];
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
<#list authorizedMailDomains as authorizedMailDomain>
|
|
||||||
out.push((function (){
|
|
||||||
|
|
||||||
<#attempt>
|
|
||||||
return "${authorizedMailDomain}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
})());
|
|
||||||
</#list>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
</#if>
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
|
|
||||||
return out;
|
|
||||||
|
|
||||||
})(),
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -10,7 +10,7 @@ import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
|||||||
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
|
import { builtinThemesUrl } from "../install-builtin-keycloak-themes";
|
||||||
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "../tools/downloadAndUnzip";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/kcContextMocks/urlResourcesPath";
|
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/getKcContext/kcContextMocks/urlResourcesPath";
|
||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
|
|
||||||
|
|
||||||
@ -22,10 +22,15 @@ export function generateKeycloakThemeResources(
|
|||||||
urlPathname: string;
|
urlPathname: string;
|
||||||
//If urlOrigin is not undefined then it means --externals-assets
|
//If urlOrigin is not undefined then it means --externals-assets
|
||||||
urlOrigin: undefined | string;
|
urlOrigin: undefined | string;
|
||||||
|
extraPagesId: string[];
|
||||||
|
extraThemeProperties: string[];
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath, urlPathname, urlOrigin } = params;
|
const {
|
||||||
|
themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath,
|
||||||
|
urlPathname, urlOrigin, extraPagesId, extraThemeProperties
|
||||||
|
} = params;
|
||||||
|
|
||||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
||||||
|
|
||||||
@ -92,7 +97,7 @@ export function generateKeycloakThemeResources(
|
|||||||
urlOrigin
|
urlOrigin
|
||||||
});
|
});
|
||||||
|
|
||||||
pageIds.forEach(pageId => {
|
[...pageIds, ...extraPagesId].forEach(pageId => {
|
||||||
|
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
@ -162,7 +167,10 @@ export function generateKeycloakThemeResources(
|
|||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
pathJoin(themeDirPath, "theme.properties"),
|
pathJoin(themeDirPath, "theme.properties"),
|
||||||
Buffer.from("parent=keycloak", "utf8")
|
Buffer.from(
|
||||||
|
"parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,115 +1,10 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
export * from "./build-keycloak-theme";
|
||||||
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
import { main } from "./build-keycloak-theme";
|
||||||
import type { ParsedPackageJson } from "./generateJavaStackFiles";
|
|
||||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
|
||||||
import { URL } from "url";
|
|
||||||
|
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
|
||||||
|
|
||||||
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
|
||||||
|
|
||||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
|
||||||
|
|
||||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
|
||||||
|
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
||||||
console.log("🔏 Building the keycloak theme...⌚");
|
main();
|
||||||
|
|
||||||
generateKeycloakThemeResources({
|
}
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
|
||||||
"themeName": parsedPackageJson.name,
|
|
||||||
...(() => {
|
|
||||||
|
|
||||||
const url = (() => {
|
|
||||||
|
|
||||||
const { homepage } = parsedPackageJson;
|
|
||||||
|
|
||||||
return homepage === undefined ?
|
|
||||||
undefined :
|
|
||||||
new URL(homepage);
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
|
||||||
"urlPathname":
|
|
||||||
url === undefined ?
|
|
||||||
"/" :
|
|
||||||
url.pathname.replace(/([^/])$/, "$1/"),
|
|
||||||
"urlOrigin": !doUseExternalAssets ? undefined : (() => {
|
|
||||||
|
|
||||||
if (url === undefined) {
|
|
||||||
console.error("ERROR: You must specify 'homepage' in your package.json");
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.origin;
|
|
||||||
|
|
||||||
})()
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
})()
|
|
||||||
});
|
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
|
||||||
parsedPackageJson,
|
|
||||||
keycloakThemeBuildingDirPath
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.execSync(
|
|
||||||
"mvn package",
|
|
||||||
{ "cwd": keycloakThemeBuildingDirPath }
|
|
||||||
);
|
|
||||||
|
|
||||||
generateDebugFiles({
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
"packageJsonName": parsedPackageJson.name
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log([
|
|
||||||
'',
|
|
||||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
|
||||||
`It is to be placed in "/opt/jboss/keycloak/standalone/deployments" in the container running a jboss/keycloak Docker image. (Tested with 11.0.3)`,
|
|
||||||
'',
|
|
||||||
'Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:',
|
|
||||||
'',
|
|
||||||
'value.yaml: ',
|
|
||||||
' extraInitContainers: |',
|
|
||||||
' - name: realm-ext-provider',
|
|
||||||
' image: curlimages/curl',
|
|
||||||
' imagePullPolicy: IfNotPresent',
|
|
||||||
' command:',
|
|
||||||
' - sh',
|
|
||||||
' args:',
|
|
||||||
' - -c',
|
|
||||||
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
|
|
||||||
' volumeMounts:',
|
|
||||||
' - name: extensions',
|
|
||||||
' mountPath: /extensions',
|
|
||||||
' ',
|
|
||||||
' extraVolumeMounts: |',
|
|
||||||
' - name: extensions',
|
|
||||||
' mountPath: /opt/jboss/keycloak/standalone/deployments',
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
'To test your theme locally, with hot reloading, you can spin up a Keycloak container image with the theme loaded by running:',
|
|
||||||
'',
|
|
||||||
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, 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 ${parsedPackageJson.name}.`,
|
|
||||||
`More details: https://www.keycloak.org/getting-started/getting-started-docker`,
|
|
||||||
'',
|
|
||||||
'Once your container is up and configured 👉 http://localhost:8080/auth/realms/myrealm/account 👈',
|
|
||||||
'',
|
|
||||||
].join("\n"));
|
|
||||||
|
|
||||||
}
|
|
@ -5,19 +5,40 @@ import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
|||||||
export function replaceImportsFromStaticInJsCode(
|
export function replaceImportsFromStaticInJsCode(
|
||||||
params: {
|
params: {
|
||||||
jsCode: string;
|
jsCode: string;
|
||||||
urlOrigin: undefined | string;
|
urlOrigin: undefined | string;
|
||||||
}
|
}
|
||||||
): { fixedJsCode: string; } {
|
): { fixedJsCode: string; } {
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE:
|
||||||
|
|
||||||
|
When we have urlOrigin defined it means that
|
||||||
|
we are building with --external-assets
|
||||||
|
so we have to make sur that the fixed js code will run
|
||||||
|
inside and outside keycloak.
|
||||||
|
|
||||||
|
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||||
|
will always run in keycloak context.
|
||||||
|
*/
|
||||||
|
|
||||||
const { jsCode, urlOrigin } = params;
|
const { jsCode, urlOrigin } = params;
|
||||||
|
|
||||||
const fixedJsCode = jsCode.replace(
|
const fixedJsCode =
|
||||||
/([a-z]+\.[a-z]+)\+"static\//g,
|
jsCode
|
||||||
(...[, group]) =>
|
.replace(
|
||||||
urlOrigin === undefined ?
|
/([a-z]+\.[a-z]+)\+"static\//g,
|
||||||
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
|
(...[, group]) =>
|
||||||
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
|
urlOrigin === undefined ?
|
||||||
);
|
`window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` :
|
||||||
|
`("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/".chunk.css",([a-z])+=([a-z]+\.[a-z]+)\+([a-z]+),/,
|
||||||
|
(...[, group1, group2, group3]) =>
|
||||||
|
urlOrigin === undefined ?
|
||||||
|
`".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},` :
|
||||||
|
`".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`
|
||||||
|
);
|
||||||
|
|
||||||
return { fixedJsCode };
|
return { fixedJsCode };
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
export const builtinThemesUrl =
|
export const builtinThemesUrl =
|
||||||
"https://github.com/garronej/keycloakify/releases/download/v0.0.1/keycloak_11.0.3_builtin_themes_with_light_mods.zip";
|
"https://github.com/garronej/keycloakify/releases/download/v0.0.1/keycloak_11.0.3_builtin_themes.zip";
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
import { crawl } from "./crawl";
|
import { crawl } from "./crawl";
|
||||||
import { id } from "evt/tools/typeSafety/id";
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
type TransformSourceCode =
|
type TransformSourceCode =
|
||||||
(params: {
|
(params: {
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import { assert } from "../tools/assert";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import type { KcContext } from "../KcContext";
|
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
|
|
||||||
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContext.Error; } & KcProps) => {
|
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error; } & KcProps) => {
|
||||||
|
|
||||||
const { msg } = useKcMessage();
|
const { msg } = useKcMessage();
|
||||||
|
|
||||||
assert(kcContext.message !== undefined);
|
|
||||||
|
|
||||||
const { message, client } = kcContext;
|
const { message, client } = kcContext;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={msg("errorTitle")}
|
headerNode={msg("errorTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
|
@ -3,10 +3,10 @@ import { memo } from "react";
|
|||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import { assert } from "../tools/assert";
|
import { assert } from "../tools/assert";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
|
|
||||||
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContext.Info; } & KcProps) => {
|
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info; } & KcProps) => {
|
||||||
|
|
||||||
const { msg } = useKcMessage();
|
const { msg } = useKcMessage();
|
||||||
|
|
||||||
@ -25,6 +25,7 @@ export const Info = memo(({ kcContext, ...props }: { kcContext: KcContext.Info;
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={
|
headerNode={
|
||||||
messageHeader !== undefined ?
|
messageHeader !== undefined ?
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import { Login } from "./Login";
|
import { Login } from "./Login";
|
||||||
import { Register } from "./Register";
|
import { Register } from "./Register";
|
||||||
@ -9,9 +9,11 @@ import { Error } from "./Error";
|
|||||||
import { LoginResetPassword } from "./LoginResetPassword";
|
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 { LoginUpdateProfile } from "./LoginUpdateProfile";
|
||||||
|
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
|
||||||
|
|
||||||
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } & KcProps ) => {
|
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase; } & KcProps) => {
|
||||||
switch (kcContext.pageId) {
|
switch (kcContext.pageId) {
|
||||||
case "login.ftl": return <Login {...{ kcContext, ...props }} />;
|
case "login.ftl": return <Login {...{ kcContext, ...props }} />;
|
||||||
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
|
case "register.ftl": return <Register {...{ kcContext, ...props }} />;
|
||||||
@ -19,7 +21,9 @@ export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContext; } &
|
|||||||
case "error.ftl": return <Error {...{ kcContext, ...props }} />;
|
case "error.ftl": return <Error {...{ kcContext, ...props }} />;
|
||||||
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
|
case "login-reset-password.ftl": return <LoginResetPassword {...{ kcContext, ...props }} />;
|
||||||
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />;
|
case "login-verify-email.ftl": return <LoginVerifyEmail {...{ kcContext, ...props }} />;
|
||||||
case "terms.ftl": return <Terms {...{ kcContext, ...props }}/>;
|
case "terms.ftl": return <Terms {...{ kcContext, ...props }} />;
|
||||||
case "login-otp.ftl": return <LoginOtp {...{ kcContext, ...props }}/>;
|
case "login-otp.ftl": return <LoginOtp {...{ kcContext, ...props }} />;
|
||||||
|
case "login-update-profile.ftl": return <LoginUpdateProfile {...{ kcContext, ...props }} />;
|
||||||
|
case "login-idp-link-confirm.ftl": return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
|
||||||
}
|
}
|
||||||
});
|
});
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
||||||
import { doExtends } from "evt/tools/typeSafety/doExtends";
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
|
||||||
/** Class names can be provided as an array or separated by whitespace */
|
/** Class names can be provided as an array or separated by whitespace */
|
||||||
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };
|
export type KcPropsGeneric<CssClasses extends string> = { [key in CssClasses]: readonly string[] | string | undefined; };
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
import { useState, memo } from "react";
|
import { useState, memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
import { useConstCallback } from "powerhooks";
|
import { useConstCallback } from "powerhooks";
|
||||||
|
|
||||||
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContext.Login; } & KcProps) => {
|
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login; } & KcProps) => {
|
||||||
|
|
||||||
const { msg, msgStr } = useKcMessage();
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
@ -26,6 +26,7 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContext.Login
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayInfo={social.displayInfo}
|
displayInfo={social.displayInfo}
|
||||||
displayWide={realm.password && social.providers !== undefined}
|
displayWide={realm.password && social.providers !== undefined}
|
||||||
headerNode={msg("doLogIn")}
|
headerNode={msg("doLogIn")}
|
||||||
@ -120,7 +121,7 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContext.Login
|
|||||||
<ul className={cx(props.kcFormSocialAccountListClass, social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass)}>
|
<ul className={cx(props.kcFormSocialAccountListClass, social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass)}>
|
||||||
{
|
{
|
||||||
social.providers.map(p =>
|
social.providers.map(p =>
|
||||||
<li className={cx(props.kcFormSocialAccountListLinkClass)}>
|
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}>
|
||||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
|
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
|
||||||
<span>{p.displayName}</span>
|
<span>{p.displayName}</span>
|
||||||
</a>
|
</a>
|
||||||
|
59
src/lib/components/LoginIdpLinkConfirm.tsx
Normal file
59
src/lib/components/LoginIdpLinkConfirm.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
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 { cx } from "tss-react";
|
||||||
|
|
||||||
|
export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; } & KcProps) => {
|
||||||
|
|
||||||
|
const { msg } = useKcMessage();
|
||||||
|
|
||||||
|
const { url, idpAlias } = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
|
headerNode={msg("confirmLinkIdpTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={cx(
|
||||||
|
props.kcButtonClass,
|
||||||
|
props.kcButtonDefaultClass,
|
||||||
|
props.kcButtonBlockClass,
|
||||||
|
props.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
id="updateProfile"
|
||||||
|
value="updateProfile"
|
||||||
|
>
|
||||||
|
{msg("confirmLinkIdpReviewProfile")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={cx(
|
||||||
|
props.kcButtonClass,
|
||||||
|
props.kcButtonDefaultClass,
|
||||||
|
props.kcButtonBlockClass,
|
||||||
|
props.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
id="linkAccount"
|
||||||
|
value="linkAccount"
|
||||||
|
>
|
||||||
|
{msg("confirmLinkIdpContinue", idpAlias)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
|||||||
import { useEffect, memo } from "react";
|
import { useEffect, memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { appendHead } from "../tools/appendHead";
|
import { appendHead } from "../tools/appendHead";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
|
|
||||||
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginOtp; } & KcProps) => {
|
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp; } & KcProps) => {
|
||||||
|
|
||||||
const { otpLogin, url } = kcContext;
|
const { otpLogin, url } = kcContext;
|
||||||
|
|
||||||
@ -43,6 +43,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContext.Lo
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
headerNode={msg("doLogIn")}
|
headerNode={msg("doLogIn")}
|
||||||
formNode={
|
formNode={
|
||||||
|
|
||||||
@ -58,7 +59,7 @@ export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContext.Lo
|
|||||||
<div className={cx(props.kcInputWrapperClass)}>
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
{
|
{
|
||||||
otpLogin.userOtpCredentials.map(otpCredential =>
|
otpLogin.userOtpCredentials.map(otpCredential =>
|
||||||
<div className={cx(props.kcSelectOTPListClass)}>
|
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}>
|
||||||
<input type="hidden" value="${otpCredential.id}" />
|
<input type="hidden" value="${otpCredential.id}" />
|
||||||
<div className={cx(props.kcSelectOTPListItemClass)}>
|
<div className={cx(props.kcSelectOTPListItemClass)}>
|
||||||
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
|
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
|
||||||
|
@ -2,11 +2,11 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
|
|
||||||
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginResetPassword; } & KcProps) => {
|
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword; } & KcProps) => {
|
||||||
|
|
||||||
const { msg, msgStr } = useKcMessage();
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: Kc
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={msg("emailForgotTitle")}
|
headerNode={msg("emailForgotTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
|
131
src/lib/components/LoginUpdateProfile.tsx
Normal file
131
src/lib/components/LoginUpdateProfile.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
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 { cx } from "tss-react";
|
||||||
|
|
||||||
|
export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; } & KcProps) => {
|
||||||
|
|
||||||
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
|
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
|
headerNode={msg("loginProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
{user.editUsernameAllowed &&
|
||||||
|
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
||||||
|
{msg("username")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
defaultValue={user.username ?? ""}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="email" className={cx(props.kcLabelClass)}>
|
||||||
|
{msg("email")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
defaultValue={user.email ?? ""}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
|
||||||
|
{msg("firstName")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="firstName"
|
||||||
|
name="firstName"
|
||||||
|
defaultValue={user.firstName ?? ""}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>
|
||||||
|
{msg("lastName")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="lastName"
|
||||||
|
name="lastName"
|
||||||
|
defaultValue={user.lastName ?? ""}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||||
|
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
|
{
|
||||||
|
isAppInitiatedAction ?
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<input
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
|
|
||||||
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContext.LoginVerifyEmail; } & KcProps) => {
|
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; } & KcProps) => {
|
||||||
|
|
||||||
const { msg } = useKcMessage();
|
const { msg } = useKcMessage();
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcCo
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={msg("emailVerifyTitle")}
|
headerNode={msg("emailVerifyTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
|
|
||||||
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContext.Register; } & KcProps) => {
|
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register; } & KcProps) => {
|
||||||
|
|
||||||
const { msg, msgStr } = useKcMessage();
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
@ -22,11 +22,12 @@ export const Register = memo(({ kcContext, ...props }: { kcContext: KcContext.Re
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
headerNode={msg("registerTitle")}
|
headerNode={msg("registerTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists('firstName', props.kcFormGroupErrorClass))}>
|
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{msg("firstName")}</label>
|
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>{msg("firstName")}</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,7 @@ import { useReducer, useEffect, memo } from "react";
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
|
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { assert } from "../tools/assert";
|
import { assert } from "../tools/assert";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
||||||
@ -25,7 +25,11 @@ export type TemplateProps = {
|
|||||||
showUsernameNode?: ReactNode;
|
showUsernameNode?: ReactNode;
|
||||||
formNode: ReactNode;
|
formNode: ReactNode;
|
||||||
infoNode?: ReactNode;
|
infoNode?: ReactNode;
|
||||||
} & { kcContext: KcContext; } & KcTemplateProps;
|
/** If you write your own page you probably want
|
||||||
|
* to avoid pulling the default theme assets.
|
||||||
|
*/
|
||||||
|
doFetchDefaultThemeResources: boolean;
|
||||||
|
} & { kcContext: KcContextBase; } & KcTemplateProps;
|
||||||
|
|
||||||
export const Template = memo((props: TemplateProps) => {
|
export const Template = memo((props: TemplateProps) => {
|
||||||
|
|
||||||
@ -39,7 +43,8 @@ export const Template = memo((props: TemplateProps) => {
|
|||||||
showUsernameNode = null,
|
showUsernameNode = null,
|
||||||
formNode,
|
formNode,
|
||||||
infoNode = null,
|
infoNode = null,
|
||||||
kcContext
|
kcContext,
|
||||||
|
doFetchDefaultThemeResources
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
useEffect(() => { console.log("Rendering this page with react using keycloakify") }, []);
|
useEffect(() => { console.log("Rendering this page with react using keycloakify") }, []);
|
||||||
@ -84,6 +89,11 @@ export const Template = memo((props: TemplateProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
|
if (!doFetchDefaultThemeResources) {
|
||||||
|
setExtraCssLoaded();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let isUnmounted = false;
|
let isUnmounted = false;
|
||||||
const cleanups: (() => void)[] = [];
|
const cleanups: (() => void)[] = [];
|
||||||
|
|
||||||
@ -269,7 +279,10 @@ export const Template = memo((props: TemplateProps) => {
|
|||||||
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>}
|
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>}
|
||||||
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>}
|
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>}
|
||||||
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>}
|
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>}
|
||||||
<span className="kc-feedback-text">{message.summary}</span>
|
<span
|
||||||
|
className="kc-feedback-text"
|
||||||
|
dangerouslySetInnerHTML={{ "__html": message.summary }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
{formNode}
|
{formNode}
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import { Template } from "./Template";
|
import { Template } from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
|
|
||||||
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContext.Terms; } & KcProps) => {
|
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms; } & KcProps) => {
|
||||||
|
|
||||||
const { msg, msgStr } = useKcMessage();
|
const { msg, msgStr } = useKcMessage();
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContext.Terms
|
|||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
{...{ kcContext, ...props }}
|
{...{ kcContext, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
displayMessage={false}
|
displayMessage={false}
|
||||||
headerNode={msg("termsTitle")}
|
headerNode={msg("termsTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
|
|
||||||
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
|
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
|
||||||
import type { PageId } from "../bin/build-keycloak-theme/generateFtl";
|
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
||||||
import { id } from "evt/tools/typeSafety/id";
|
import { doExtends } from "tsafe/doExtends";
|
||||||
import type { KcLanguageTag } from "./i18n/KcLanguageTag";
|
import type { MessageKey } from "../i18n/useKcMessage";
|
||||||
import { doExtends } from "evt/tools/typeSafety/doExtends";
|
import type { LanguageLabel } from "../i18n/KcLanguageTag";
|
||||||
import type { MessageKey } from "./i18n/useKcMessage";
|
|
||||||
import type { LanguageLabel } from "./i18n/KcLanguageTag";
|
|
||||||
|
|
||||||
type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
|
type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
|
||||||
StrEnum extends `${Prefix}${infer U}` ? U : never;
|
StrEnum extends `${Prefix}${infer U}` ? U : never;
|
||||||
@ -14,12 +12,13 @@ type ExtractAfterStartingWith<Prefix extends string, StrEnum> =
|
|||||||
* Some values might be undefined on some pages.
|
* Some values might be undefined on some pages.
|
||||||
* (ex: url.loginAction is undefined on error.ftl)
|
* (ex: url.loginAction is undefined on error.ftl)
|
||||||
*/
|
*/
|
||||||
export type KcContext =
|
export type KcContextBase =
|
||||||
KcContext.Login | KcContext.Register | KcContext.Info |
|
KcContextBase.Login | KcContextBase.Register | KcContextBase.Info |
|
||||||
KcContext.Error | KcContext.LoginResetPassword | KcContext.LoginVerifyEmail |
|
KcContextBase.Error | KcContextBase.LoginResetPassword | KcContextBase.LoginVerifyEmail |
|
||||||
KcContext.Terms | KcContext.LoginOtp;
|
KcContextBase.Terms | KcContextBase.LoginOtp | KcContextBase.LoginUpdateProfile |
|
||||||
|
KcContextBase.LoginIdpLinkConfirm;
|
||||||
|
|
||||||
export declare namespace KcContext {
|
export declare namespace KcContextBase {
|
||||||
|
|
||||||
export type Common = {
|
export type Common = {
|
||||||
url: {
|
url: {
|
||||||
@ -58,6 +57,10 @@ export declare namespace KcContext {
|
|||||||
type: "success" | "warning" | "error" | "info";
|
type: "success" | "warning" | "error" | "info";
|
||||||
summary: string;
|
summary: string;
|
||||||
};
|
};
|
||||||
|
client: {
|
||||||
|
clientId: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
isAppInitiatedAction: boolean;
|
isAppInitiatedAction: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -124,11 +127,15 @@ export declare namespace KcContext {
|
|||||||
passwordRequired: boolean;
|
passwordRequired: boolean;
|
||||||
recaptchaRequired: boolean;
|
recaptchaRequired: boolean;
|
||||||
recaptchaSiteKey?: string;
|
recaptchaSiteKey?: string;
|
||||||
/**
|
social: {
|
||||||
* Defined when you use the keycloak-mail-whitelisting keycloak plugin
|
displayInfo: boolean;
|
||||||
* (https://github.com/micedre/keycloak-mail-whitelisting)
|
providers?: {
|
||||||
*/
|
loginUrl: string;
|
||||||
authorizedMailDomains?: string[];
|
alias: string;
|
||||||
|
providerId: string;
|
||||||
|
displayName: string;
|
||||||
|
}[]
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Info = Common & {
|
export type Info = Common & {
|
||||||
@ -147,7 +154,8 @@ export declare namespace KcContext {
|
|||||||
pageId: "error.ftl";
|
pageId: "error.ftl";
|
||||||
client?: {
|
client?: {
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
}
|
},
|
||||||
|
message: NonNullable<Common["message"]>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginResetPassword = Common & {
|
export type LoginResetPassword = Common & {
|
||||||
@ -172,11 +180,33 @@ export declare namespace KcContext {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginUpdateProfile = Common & {
|
||||||
|
pageId: "login-update-profile.ftl";
|
||||||
|
user: {
|
||||||
|
editUsernameAllowed: boolean;
|
||||||
|
username?: string;
|
||||||
|
email?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
};
|
||||||
|
messagesPerField: {
|
||||||
|
printIfExists<T>(
|
||||||
|
key: "username" | "email" | "firstName" | "lastName",
|
||||||
|
x: T
|
||||||
|
): T | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginIdpLinkConfirm = Common & {
|
||||||
|
pageId: "login-idp-link-confirm.ftl";
|
||||||
|
idpAlias: string;
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doExtends<KcContext["pageId"], PageId>();
|
doExtends<KcContextBase["pageId"], PageId>();
|
||||||
doExtends<PageId, KcContext["pageId"]>();
|
doExtends<PageId, KcContextBase["pageId"]>();
|
||||||
|
|
||||||
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);
|
|
||||||
|
|
||||||
|
|
86
src/lib/getKcContext/getKcContext.ts
Normal file
86
src/lib/getKcContext/getKcContext.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
|
||||||
|
import type { KcContextBase } 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
|
||||||
|
>;
|
||||||
|
|
||||||
|
export function getKcContext<KcContextExtended extends { pageId: string; } = never>(
|
||||||
|
params?: {
|
||||||
|
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||||
|
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
|
||||||
|
}
|
||||||
|
): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined; } {
|
||||||
|
|
||||||
|
const {
|
||||||
|
mockPageId,
|
||||||
|
mockData
|
||||||
|
} = params ?? {};
|
||||||
|
|
||||||
|
if (mockPageId !== undefined) {
|
||||||
|
|
||||||
|
//TODO maybe trow if no mock fo custom page
|
||||||
|
|
||||||
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
kcContextDefaultMock === undefined &&
|
||||||
|
partialKcContextCustomMock === undefined
|
||||||
|
) {
|
||||||
|
|
||||||
|
console.warn([
|
||||||
|
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||||
|
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||||
|
`Please check the documentation of the getKcContext function`
|
||||||
|
].join("\n"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const kcContext: any = {};
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock !== undefined ?
|
||||||
|
kcContextDefaultMock :
|
||||||
|
{ "pageId": mockPageId, ...kcContextCommonMock, }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"kcContext":
|
||||||
|
typeof window === "undefined" ?
|
||||||
|
undefined :
|
||||||
|
(window as any)[ftlValuesGlobalName]
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
2
src/lib/getKcContext/index.ts
Normal file
2
src/lib/getKcContext/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export type { KcContextBase } from "./KcContextBase";
|
||||||
|
export { getKcContext } from "./getKcContext";
|
1
src/lib/getKcContext/kcContextMocks/index.ts
Normal file
1
src/lib/getKcContext/kcContextMocks/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./kcContextMocks";
|
261
src/lib/getKcContext/kcContextMocks/kcContextMocks.ts
Normal file
261
src/lib/getKcContext/kcContextMocks/kcContextMocks.ts
Normal file
@ -0,0 +1,261 @@
|
|||||||
|
|
||||||
|
import type { KcContextBase } from "../KcContextBase";
|
||||||
|
import { getEvtKcLanguage } from "../../i18n/useKcLanguageTag";
|
||||||
|
import { getKcLanguageTagLabel } from "../../i18n/KcLanguageTag";
|
||||||
|
//NOTE: Aside because we want to be able to import them from node
|
||||||
|
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
|
||||||
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
|
export const kcContextCommonMock: KcContextBase.Common = {
|
||||||
|
"url": {
|
||||||
|
"loginAction": "#",
|
||||||
|
"resourcesPath": pathJoin(PUBLIC_URL, resourcesPath),
|
||||||
|
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonPath),
|
||||||
|
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
|
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
|
},
|
||||||
|
"realm": {
|
||||||
|
"displayName": "myrealm",
|
||||||
|
"displayNameHtml": "myrealm",
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"registrationEmailAsUsername": true,
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"supported": [
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||||
|
"languageTag": "de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||||
|
"languageTag": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||||
|
"languageTag": "ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||||
|
"languageTag": "sv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||||
|
"languageTag": "pt-BR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||||
|
"languageTag": "lt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||||
|
"languageTag": "en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||||
|
"languageTag": "it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||||
|
"languageTag": "fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||||
|
"languageTag": "zh-CN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||||
|
"languageTag": "es"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||||
|
"languageTag": "cs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||||
|
"languageTag": "ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||||
|
"languageTag": "sk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||||
|
"languageTag": "pl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||||
|
"languageTag": "ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||||
|
"languageTag": "nl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||||
|
"languageTag": "tr"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
//"current": null as any
|
||||||
|
"current": "English"
|
||||||
|
},
|
||||||
|
"auth": {
|
||||||
|
"showUsername": false,
|
||||||
|
"showResetCredentials": false,
|
||||||
|
"showTryAnotherWayLink": false
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"clientId": "myApp"
|
||||||
|
},
|
||||||
|
"scripts": [],
|
||||||
|
"message": {
|
||||||
|
"type": "success",
|
||||||
|
"summary": "This is a test message"
|
||||||
|
},
|
||||||
|
"isAppInitiatedAction": false,
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Object.defineProperty(
|
||||||
|
kcContextCommonMock.locale!,
|
||||||
|
"current",
|
||||||
|
{
|
||||||
|
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
|
||||||
|
"enumerable": true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginUrl = {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
||||||
|
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const kcContextMocks: KcContextBase[] = [
|
||||||
|
id<KcContextBase.Login>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login.ftl",
|
||||||
|
"url": loginUrl,
|
||||||
|
"realm": {
|
||||||
|
...kcContextCommonMock.realm,
|
||||||
|
"loginWithEmailAllowed": true,
|
||||||
|
"rememberMe": true,
|
||||||
|
"password": true,
|
||||||
|
"resetPasswordAllowed": true,
|
||||||
|
"registrationAllowed": true
|
||||||
|
},
|
||||||
|
"auth": kcContextCommonMock.auth!,
|
||||||
|
"social": {
|
||||||
|
"displayInfo": true
|
||||||
|
},
|
||||||
|
"usernameEditDisabled": false,
|
||||||
|
"login": {
|
||||||
|
"rememberMe": false
|
||||||
|
},
|
||||||
|
"registrationDisabled": false,
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.Register>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "register.ftl",
|
||||||
|
"url": {
|
||||||
|
...loginUrl,
|
||||||
|
"registrationAction": "http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
|
||||||
|
},
|
||||||
|
"messagesPerField": {
|
||||||
|
"printIfExists": (...[, x]) => x
|
||||||
|
},
|
||||||
|
"scripts": [],
|
||||||
|
"isAppInitiatedAction": false,
|
||||||
|
"register": {
|
||||||
|
"formData": {}
|
||||||
|
},
|
||||||
|
"passwordRequired": true,
|
||||||
|
"recaptchaRequired": false,
|
||||||
|
"social": {
|
||||||
|
"displayInfo": true
|
||||||
|
},
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.Info>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "info.ftl",
|
||||||
|
"messageHeader": "<Message header>",
|
||||||
|
"requiredActions": undefined,
|
||||||
|
"skipLink": false,
|
||||||
|
"actionUri": "#",
|
||||||
|
"client": {
|
||||||
|
"clientId": "myApp",
|
||||||
|
"baseUrl": "#"
|
||||||
|
}
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.Error>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "error.ftl",
|
||||||
|
"client": {
|
||||||
|
"clientId": "myApp",
|
||||||
|
"baseUrl": "#"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "error",
|
||||||
|
"summary": "This is the error message"
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
id<KcContextBase.LoginResetPassword>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login-reset-password.ftl",
|
||||||
|
"realm": {
|
||||||
|
...kcContextCommonMock.realm,
|
||||||
|
"loginWithEmailAllowed": false
|
||||||
|
}
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.LoginVerifyEmail>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login-verify-email.ftl"
|
||||||
|
}),
|
||||||
|
id<KcContextBase.Terms>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "terms.ftl"
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.LoginOtp>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login-otp.ftl",
|
||||||
|
"otpLogin": {
|
||||||
|
"userOtpCredentials": [
|
||||||
|
{
|
||||||
|
"id": "id1",
|
||||||
|
"userLabel": "label1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "id2",
|
||||||
|
"userLabel": "label2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}),
|
||||||
|
id<KcContextBase.LoginUpdateProfile>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login-update-profile.ftl",
|
||||||
|
"user": {
|
||||||
|
"editUsernameAllowed": true,
|
||||||
|
"username": "anUsername",
|
||||||
|
"email": "foo@example.com",
|
||||||
|
"firstName": "aFirstName",
|
||||||
|
"lastName": "aLastName"
|
||||||
|
},
|
||||||
|
"messagesPerField": {
|
||||||
|
"printIfExists": () => undefined
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
id<KcContextBase.LoginIdpLinkConfirm>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "login-idp-link-confirm.ftl",
|
||||||
|
"idpAlias": "FranceConnect"
|
||||||
|
})
|
||||||
|
];
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
import { kcMessages } from "./kcMessages/login";
|
import { kcMessages } from "./kcMessages/login";
|
||||||
|
|
||||||
export type KcLanguageTag = keyof typeof kcMessages;
|
export type KcLanguageTag = keyof typeof kcMessages;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { kcMessages } from "../generated_kcMessages/login";
|
import { kcMessages } from "../generated_kcMessages/login";
|
||||||
import { Evt } from "evt";
|
import { Evt } from "evt";
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
|
||||||
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
export const evtTermsUpdated = Evt.asNonPostable(Evt.create<void>());
|
||||||
|
|
||||||
|
@ -1,21 +1,40 @@
|
|||||||
|
|
||||||
import { createUseGlobalState } from "powerhooks";
|
import { createUseGlobalState } from "powerhooks";
|
||||||
import { kcContext } from "../KcContext";
|
import { getKcContext } from "../getKcContext";
|
||||||
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
||||||
|
import type { StatefulEvt } from "powerhooks";
|
||||||
|
import { KcLanguageTag } from "./KcLanguageTag";
|
||||||
|
|
||||||
|
|
||||||
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
|
//export const { useKcLanguageTag, evtKcLanguageTag } = createUseGlobalState(
|
||||||
const wrap = createUseGlobalState(
|
const wrap = createUseGlobalState(
|
||||||
"kcLanguageTag",
|
"kcLanguageTag",
|
||||||
() => getBestMatchAmongKcLanguageTag(
|
() => {
|
||||||
kcContext?.locale?.current ??
|
|
||||||
navigator.language
|
|
||||||
),
|
const { kcContext } = getKcContext();
|
||||||
{ "persistance": "cookie" }
|
|
||||||
|
const languageLike =
|
||||||
|
kcContext?.locale?.current ??
|
||||||
|
(
|
||||||
|
typeof navigator === "undefined" ?
|
||||||
|
undefined :
|
||||||
|
navigator.language
|
||||||
|
);
|
||||||
|
|
||||||
|
if (languageLike === undefined) {
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
||||||
|
return getBestMatchAmongKcLanguageTag(languageLike);
|
||||||
|
|
||||||
|
},
|
||||||
|
{ "persistance": "localStorage" }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const { useKcLanguageTag } = wrap;
|
export const { useKcLanguageTag } = wrap;
|
||||||
|
|
||||||
export function getEvtKcLanguage() {
|
export function getEvtKcLanguage(): StatefulEvt<KcLanguageTag> {
|
||||||
return wrap.evtKcLanguageTag;
|
return wrap.evtKcLanguageTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,13 @@ import ReactMarkdown from "react-markdown";
|
|||||||
|
|
||||||
export type MessageKey = keyof typeof kcMessages["en"];
|
export type MessageKey = keyof typeof kcMessages["en"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When the language is switched the page is reloaded, this may appear
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
export function useKcMessage() {
|
export function useKcMessage() {
|
||||||
|
|
||||||
const { kcLanguageTag } = useKcLanguageTag();
|
const { kcLanguageTag } = useKcLanguageTag();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export * from "./KcContext";
|
export * from "./getKcContext";
|
||||||
|
|
||||||
export * from "./i18n/KcLanguageTag";
|
export * from "./i18n/KcLanguageTag";
|
||||||
export * from "./i18n/useKcLanguageTag";
|
export * from "./i18n/useKcLanguageTag";
|
||||||
@ -17,5 +17,3 @@ export * from "./keycloakJsAdapter";
|
|||||||
|
|
||||||
export * from "./tools/assert";
|
export * from "./tools/assert";
|
||||||
|
|
||||||
|
|
||||||
export * as kcContextMocks from "./kcContextMocks";
|
|
@ -1,230 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
import type { KcContext } from "../KcContext";
|
|
||||||
import { getEvtKcLanguage } from "../i18n/useKcLanguageTag";
|
|
||||||
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
|
|
||||||
//NOTE: Aside because we want to be able to import them from node
|
|
||||||
import { resourcesCommonPath, resourcesPath } from "./urlResourcesPath";
|
|
||||||
|
|
||||||
const kcCommonContext: KcContext.Common = {
|
|
||||||
"url": {
|
|
||||||
"loginAction": "#",
|
|
||||||
"resourcesPath": `${process.env["PUBLIC_URL"]}/${resourcesPath}`,
|
|
||||||
"resourcesCommonPath": `${process.env["PUBLIC_URL"]}/${resourcesCommonPath}`,
|
|
||||||
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
|
||||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
|
||||||
},
|
|
||||||
"realm": {
|
|
||||||
"displayName": "myrealm",
|
|
||||||
"displayNameHtml": "myrealm",
|
|
||||||
"internationalizationEnabled": true,
|
|
||||||
"registrationEmailAsUsername": true,
|
|
||||||
},
|
|
||||||
"locale": {
|
|
||||||
"supported": [
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
|
||||||
"languageTag": "de"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
|
||||||
"languageTag": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
|
||||||
"languageTag": "ru"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
|
||||||
"languageTag": "sv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
|
||||||
"languageTag": "pt-BR"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
|
||||||
"languageTag": "lt"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
|
||||||
"languageTag": "en"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
|
||||||
"languageTag": "it"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
|
||||||
"languageTag": "fr"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
|
||||||
"languageTag": "zh-CN"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
|
||||||
"languageTag": "es"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
|
||||||
"languageTag": "cs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
|
||||||
"languageTag": "ja"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
|
||||||
"languageTag": "sk"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
|
||||||
"languageTag": "pl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
|
||||||
"languageTag": "ca"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
|
||||||
"languageTag": "nl"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
|
||||||
"languageTag": "tr"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"current": null as any
|
|
||||||
},
|
|
||||||
"auth": {
|
|
||||||
"showUsername": false,
|
|
||||||
"showResetCredentials": false,
|
|
||||||
"showTryAnotherWayLink": false
|
|
||||||
},
|
|
||||||
"scripts": [],
|
|
||||||
"message": {
|
|
||||||
"type": "success",
|
|
||||||
"summary": "This is a test message"
|
|
||||||
},
|
|
||||||
"isAppInitiatedAction": false,
|
|
||||||
};
|
|
||||||
|
|
||||||
Object.defineProperty(
|
|
||||||
kcCommonContext.locale!,
|
|
||||||
"current",
|
|
||||||
{
|
|
||||||
"get": () => getKcLanguageTagLabel(getEvtKcLanguage().state),
|
|
||||||
"enumerable": true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const kcLoginContext: KcContext.Login = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "login.ftl",
|
|
||||||
"url": {
|
|
||||||
...kcCommonContext.url,
|
|
||||||
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
|
||||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
|
|
||||||
},
|
|
||||||
"realm": {
|
|
||||||
...kcCommonContext.realm,
|
|
||||||
"loginWithEmailAllowed": true,
|
|
||||||
"rememberMe": true,
|
|
||||||
"password": true,
|
|
||||||
"resetPasswordAllowed": true,
|
|
||||||
"registrationAllowed": true
|
|
||||||
},
|
|
||||||
"auth": kcCommonContext.auth!,
|
|
||||||
"social": {
|
|
||||||
"displayInfo": true
|
|
||||||
},
|
|
||||||
"usernameEditDisabled": false,
|
|
||||||
"login": {
|
|
||||||
"rememberMe": false
|
|
||||||
},
|
|
||||||
"registrationDisabled": false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcRegisterContext: KcContext.Register = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"url": {
|
|
||||||
...kcLoginContext.url,
|
|
||||||
"registrationAction": "http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
|
|
||||||
},
|
|
||||||
"messagesPerField": {
|
|
||||||
"printIfExists": (...[, x]) => x
|
|
||||||
},
|
|
||||||
"scripts": [],
|
|
||||||
"isAppInitiatedAction": false,
|
|
||||||
"pageId": "register.ftl",
|
|
||||||
"register": {
|
|
||||||
"formData": {}
|
|
||||||
},
|
|
||||||
"passwordRequired": true,
|
|
||||||
"recaptchaRequired": false,
|
|
||||||
"authorizedMailDomains": [
|
|
||||||
"example.com",
|
|
||||||
"another-example.com",
|
|
||||||
"*.yet-another-example.com",
|
|
||||||
"*.example.com",
|
|
||||||
"hello-world.com"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcInfoContext: KcContext.Info = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "info.ftl",
|
|
||||||
"messageHeader": "<Message header>",
|
|
||||||
"requiredActions": undefined,
|
|
||||||
"skipLink": false,
|
|
||||||
"actionUri": "#",
|
|
||||||
"client": {
|
|
||||||
"baseUrl": "#"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcErrorContext: KcContext.Error = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "error.ftl",
|
|
||||||
"client": {
|
|
||||||
"baseUrl": "#"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcLoginResetPasswordContext: KcContext.LoginResetPassword = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "login-reset-password.ftl",
|
|
||||||
"realm": {
|
|
||||||
...kcCommonContext.realm,
|
|
||||||
"loginWithEmailAllowed": false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcLoginVerifyEmailContext: KcContext.LoginVerifyEmail = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "login-verify-email.ftl"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcTermsContext: KcContext.Terms = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "terms.ftl"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kcLoginOtpContext: KcContext.LoginOtp = {
|
|
||||||
...kcCommonContext,
|
|
||||||
"pageId": "login-otp.ftl",
|
|
||||||
"otpLogin": {
|
|
||||||
"userOtpCredentials": [
|
|
||||||
{
|
|
||||||
"id": "id1",
|
|
||||||
"userLabel": "label1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "id2",
|
|
||||||
"userLabel": "label2"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -62,7 +62,7 @@ export function createKeycloakAdapter(
|
|||||||
"success": { "value": () => { } },
|
"success": { "value": () => { } },
|
||||||
"error": { "value": () => { } }
|
"error": { "value": () => { } }
|
||||||
}
|
}
|
||||||
);
|
) as any;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"login": options => {
|
"login": options => {
|
||||||
|
35
src/lib/tools/AndByDiscriminatingKey.ts
Normal file
35
src/lib/tools/AndByDiscriminatingKey.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
|
||||||
|
export type AndByDiscriminatingKey<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
U1 extends Record<DiscriminatingKey, string>,
|
||||||
|
U2 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
|
||||||
|
|
||||||
|
export declare namespace AndByDiscriminatingKey {
|
||||||
|
|
||||||
|
export type Tf1<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
U1,
|
||||||
|
U1Again extends Record<DiscriminatingKey, string>,
|
||||||
|
U2 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
U1 extends Pick<U2, DiscriminatingKey> ?
|
||||||
|
Tf2<DiscriminatingKey, U1, U2, U1Again> :
|
||||||
|
U1;
|
||||||
|
|
||||||
|
export type Tf2<
|
||||||
|
DiscriminatingKey extends string,
|
||||||
|
SingletonU1 extends Record<DiscriminatingKey, string>,
|
||||||
|
U2,
|
||||||
|
U1 extends Record<DiscriminatingKey, string>
|
||||||
|
> =
|
||||||
|
U2 extends Pick<SingletonU1, DiscriminatingKey> ?
|
||||||
|
U2 & SingletonU1 :
|
||||||
|
U2 extends Pick<U1, DiscriminatingKey> ?
|
||||||
|
never :
|
||||||
|
U2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
4
src/lib/tools/DeepPartial.ts
Normal file
4
src/lib/tools/DeepPartial.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export type DeepPartial<T> = {
|
||||||
|
[P in keyof T]?: DeepPartial<T[P]>;
|
||||||
|
};
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
export { assert } from "evt/tools/typeSafety/assert";
|
export { assert } from "tsafe/assert";
|
59
src/lib/tools/deepAssign.ts
Normal file
59
src/lib/tools/deepAssign.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
|
||||||
|
//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;
|
||||||
|
|
||||||
|
Object.keys(source).forEach(key => {
|
||||||
|
var dereferencedSource = source[key];
|
||||||
|
|
||||||
|
if (
|
||||||
|
target[key] === undefined ||
|
||||||
|
!(dereferencedSource instanceof Object)
|
||||||
|
) {
|
||||||
|
|
||||||
|
Object.defineProperty(
|
||||||
|
target,
|
||||||
|
key,
|
||||||
|
{
|
||||||
|
"enumerable": true,
|
||||||
|
"writable": true,
|
||||||
|
"configurable": true,
|
||||||
|
"value": dereferencedSource
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dereferencedTarget = target[key];
|
||||||
|
|
||||||
|
if (dereferencedSource instanceof Array) {
|
||||||
|
|
||||||
|
assert(is<unknown[]>(dereferencedTarget));
|
||||||
|
assert(is<unknown[]>(dereferencedSource));
|
||||||
|
|
||||||
|
dereferencedSource.forEach(entry => dereferencedTarget.push(entry));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(is<Record<string, unknown>>(dereferencedTarget));
|
||||||
|
assert(is<Record<string, unknown>>(dereferencedSource));
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": dereferencedTarget,
|
||||||
|
"source": dereferencedSource
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
4
src/lib/tools/deepClone.ts
Normal file
4
src/lib/tools/deepClone.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
export function deepClone<T>(arg: T): T {
|
||||||
|
return JSON.parse(JSON.stringify(arg));
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { generateKeycloakThemeResources } from "../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
import { generateKeycloakThemeResources } from "../../bin/build-keycloak-theme/generateKeycloakThemeResources";
|
||||||
import {
|
import {
|
||||||
setupSampleReactProject,
|
setupSampleReactProject,
|
||||||
sampleReactProjectDirPath
|
sampleReactProjectDirPath
|
||||||
@ -13,6 +13,8 @@ generateKeycloakThemeResources({
|
|||||||
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
||||||
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
||||||
"urlPathname": "/keycloakify-demo-app/",
|
"urlPathname": "/keycloakify-demo-app/",
|
||||||
"urlOrigin": undefined
|
"urlOrigin": undefined,
|
||||||
|
"extraPagesId": ["my-custom-page.ftl"],
|
||||||
|
"extraThemeProperties": ["env=test"]
|
||||||
});
|
});
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
|||||||
} from "./setupSampleReactProject";
|
} from "./setupSampleReactProject";
|
||||||
import * as st from "scripting-tools";
|
import * as st from "scripting-tools";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { getProjectRoot } from "../bin/tools/getProjectRoot";
|
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
|
||||||
|
|
||||||
setupSampleReactProject();
|
setupSampleReactProject();
|
||||||
|
|
@ -3,7 +3,7 @@ import {
|
|||||||
replaceImportsFromStaticInJsCode,
|
replaceImportsFromStaticInJsCode,
|
||||||
replaceImportsInCssCode,
|
replaceImportsInCssCode,
|
||||||
generateCssCodeToDefineGlobals
|
generateCssCodeToDefineGlobals
|
||||||
} from "../bin/build-keycloak-theme/replaceImportFromStatic";
|
} from "../../bin/build-keycloak-theme/replaceImportFromStatic";
|
||||||
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
"jsCode": `
|
"jsCode": `
|
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
import { getProjectRoot } from "../bin/tools/getProjectRoot";
|
import { getProjectRoot } from "../../bin/tools/getProjectRoot";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { downloadAndUnzip } from "../bin/tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "../../bin/tools/downloadAndUnzip";
|
||||||
|
|
||||||
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");
|
export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_react_project");
|
||||||
|
|
250
src/test/lib/getKcContext.ts
Normal file
250
src/test/lib/getKcContext.ts
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
|
||||||
|
import { getKcContext } from "../../lib/getKcContext";
|
||||||
|
import type { KcContextBase } from "../../lib/getKcContext";
|
||||||
|
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
|
||||||
|
import { same } from "evt/tools/inDepth";
|
||||||
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "../../lib/getKcContext/kcContextMocks";
|
||||||
|
import { deepClone } from "../../lib/tools/deepClone";
|
||||||
|
import type { Any } from "ts-toolbelt";
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const authorizedMailDomains = [
|
||||||
|
"example.com",
|
||||||
|
"another-example.com",
|
||||||
|
"*.yet-another-example.com",
|
||||||
|
"*.example.com",
|
||||||
|
"hello-world.com"
|
||||||
|
];
|
||||||
|
|
||||||
|
const displayName = "this is an overwritten common value";
|
||||||
|
|
||||||
|
const aNonStandardValue1 = "a non standard value 1";
|
||||||
|
const aNonStandardValue2 = "a non standard value 2";
|
||||||
|
|
||||||
|
type KcContextExtended = {
|
||||||
|
pageId: "register.ftl";
|
||||||
|
authorizedMailDomains: string[];
|
||||||
|
} | {
|
||||||
|
pageId: "info.ftl";
|
||||||
|
aNonStandardValue1: string;
|
||||||
|
} | {
|
||||||
|
pageId: "my-extra-page-1.ftl";
|
||||||
|
} | {
|
||||||
|
pageId: "my-extra-page-2.ftl";
|
||||||
|
aNonStandardValue2: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKcContextProxy = (
|
||||||
|
params: {
|
||||||
|
mockPageId: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
|
||||||
|
const { mockPageId } = params;
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext<KcContextExtended>({
|
||||||
|
mockPageId,
|
||||||
|
"mockData": [
|
||||||
|
{
|
||||||
|
"pageId": "login.ftl",
|
||||||
|
"realm": { displayName }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "info.ftl",
|
||||||
|
aNonStandardValue1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "register.ftl",
|
||||||
|
authorizedMailDomains
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pageId": "my-extra-page-2.ftl",
|
||||||
|
aNonStandardValue2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "login.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase.Login>, 1>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
//NOTE: deepClone for printIfExists or other functions...
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
mock.realm.displayName = displayName;
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const pageId = "info.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase.Info & { pageId: typeof pageId; aNonStandardValue1: string; }>, 1>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
Object.assign(mock, { aNonStandardValue1 });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const pageId = "register.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase.Register & { pageId: typeof pageId; authorizedMailDomains: string[]; }>, 1>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
|
||||||
|
|
||||||
|
Object.assign(mock, { authorizedMailDomains });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "my-extra-page-2.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; aNonStandardValue2: string; }>, 1>();
|
||||||
|
|
||||||
|
kcContext.aNonStandardValue2;
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId, aNonStandardValue2 });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "my-extra-page-1.ftl";
|
||||||
|
|
||||||
|
console.log("We expect a warning here =>");
|
||||||
|
|
||||||
|
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
|
||||||
|
|
||||||
|
|
||||||
|
assert(kcContext?.pageId === pageId);
|
||||||
|
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase.Common & { pageId: typeof pageId; }>, 1>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
(() => {
|
||||||
|
|
||||||
|
const mock = deepClone(kcContextCommonMock);
|
||||||
|
|
||||||
|
Object.assign(mock, { pageId });
|
||||||
|
|
||||||
|
return mock;
|
||||||
|
|
||||||
|
})()
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log(`PASS ${pageId}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const pageId = "login.ftl";
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext({
|
||||||
|
"mockPageId": pageId
|
||||||
|
});
|
||||||
|
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase | undefined>, 1>();
|
||||||
|
|
||||||
|
assert(same(
|
||||||
|
deepClone(kcContext),
|
||||||
|
deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)
|
||||||
|
));
|
||||||
|
|
||||||
|
console.log("PASS no extension");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const { kcContext } = getKcContext();
|
||||||
|
|
||||||
|
doExtends<Any.Equals<typeof kcContext, KcContextBase | undefined>, 1>();
|
||||||
|
|
||||||
|
assert(kcContext === undefined);
|
||||||
|
|
||||||
|
console.log("PASS no extension, no mock");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
2
src/test/lib/index.ts
Normal file
2
src/test/lib/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
import "./getKcContext";
|
91
src/test/lib/tools/AndByDiscriminatingKey.type.ts
Normal file
91
src/test/lib/tools/AndByDiscriminatingKey.type.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
|
||||||
|
import { AndByDiscriminatingKey } from "../../../lib/tools/AndByDiscriminatingKey";
|
||||||
|
import { doExtends } from "tsafe/doExtends";
|
||||||
|
|
||||||
|
type Base =
|
||||||
|
{ pageId: "a"; onlyA: string; } |
|
||||||
|
{ pageId: "b"; onlyB: string; } |
|
||||||
|
{ pageId: "only base"; onlyBase: string; };
|
||||||
|
|
||||||
|
type Extension =
|
||||||
|
{ pageId: "a"; onlyExtA: string; } |
|
||||||
|
{ pageId: "b"; onlyExtB: string; } |
|
||||||
|
{ pageId: "only ext"; onlyExt: string; };
|
||||||
|
|
||||||
|
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
||||||
|
|
||||||
|
type Expected =
|
||||||
|
{ pageId: "a"; onlyA: string; onlyExtA: string; } |
|
||||||
|
{ pageId: "b"; onlyB: string; onlyExtB: string; } |
|
||||||
|
{ pageId: "only base"; onlyBase: string; } |
|
||||||
|
{ pageId: "only ext"; onlyExt: string; };
|
||||||
|
|
||||||
|
doExtends<Got, Expected>();
|
||||||
|
doExtends<Expected, Got>();
|
||||||
|
|
||||||
|
const x: Got = null as any;
|
||||||
|
|
||||||
|
if (x.pageId === "a") {
|
||||||
|
|
||||||
|
x.onlyA;
|
||||||
|
x.onlyExtA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (x.pageId === "b") {
|
||||||
|
|
||||||
|
x.onlyB;
|
||||||
|
x.onlyExtB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only base") {
|
||||||
|
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only ext") {
|
||||||
|
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user