Compare commits

..

1 Commits

Author SHA1 Message Date
2053973fe3 Enabling shorter import paths [automatic] 2021-02-27 01:33:54 +00:00
397 changed files with 5065 additions and 43703 deletions

3
.gitattributes vendored
View File

@ -1,3 +0,0 @@
src/lib/i18n/generated_kcMessages/* linguist-documentation
src/bin/keycloakify/index.ts -linguist-detectable
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable

View File

@ -1,4 +0,0 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

25
.github/release.yaml vendored
View File

@ -1,25 +0,0 @@
changelog:
exclude:
labels:
- ignore-for-release
authors:
- octocat
categories:
- title: Breaking Changes 🛠
labels:
- breaking
- title: Exciting New Features 🎉
labels:
- feature
- title: Fixes 🔧
labels:
- fix
- title: Documentation 🔧
labels:
- docs
- title: CI 👷
labels:
- ci
- title: Other Changes
labels:
- '*'

View File

@ -1,139 +0,0 @@
name: ci
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
test_lint:
runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: bahmutov/npm-install@v1
- name: If this step fails run 'yarn format' then commit again.
run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run format:check
test:
runs-on: ${{ matrix.os }}
needs: test_lint
strategy:
matrix:
node: [ '14', '15' ,'16', '17' ]
os: [ windows-latest, ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- name: Tell if project is using npm or yarn
id: step1
uses: garronej/ts-ci@v2.0.2
with:
action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn build
yarn test
- if: steps.step1.outputs.npm_or_yarn == 'npm'
run: |
npm run build
npm test
check_if_version_upgraded:
name: Check if version upgrade
# We run this only if it's a push on the default branch or if it's a PR from a
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is
# defined but GitHub Action don't allow it yet.
if: |
github.event_name == 'push' ||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
runs-on: ubuntu-latest
needs: test
outputs:
from_version: ${{ steps.step1.outputs.from_version }}
to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
steps:
- uses: garronej/ts-ci@v2.0.2
id: step1
with:
action_name: is_package_json_version_upgraded
branch: ${{ github.head_ref || github.ref }}
create_github_release:
runs-on: ubuntu-latest
# We create a release only if the version have been upgraded and we are on the main branch
# or if we are on a branch of the repo that has an PR open on main.
if: |
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
(
github.event_name == 'push' ||
needs.check_if_version_upgraded.outputs.is_pre_release == 'true'
)
needs:
- check_if_version_upgraded
steps:
- uses: softprops/action-gh-release@v1
with:
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
target_commitish: ${{ github.head_ref || github.ref }}
generate_release_notes: true
draft: false
prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_on_npm:
runs-on: ubuntu-latest
needs:
- create_github_release
- check_if_version_upgraded
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- uses: actions/setup-node@v3
with:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run build
- run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
env:
DRY_RUN: "0"
- name: Publishing on NPM
run: |
if [ "$(npm show . version)" = "$VERSION" ]; then
echo "This version is already published"
exit 0
fi
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"
false
fi
EXTRA_ARGS=""
if [ "$IS_PRE_RELEASE" = "true" ]; then
EXTRA_ARGS="--tag next"
fi
npm publish $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}

7
.gitignore vendored
View File

@ -41,12 +41,5 @@ jspm_packages
.DS_Store
/dist
/dist_test
/sample_react_project/
/.yarn_home/
.idea
/keycloak_email
/build_keycloak

View File

@ -1,9 +0,0 @@
node_modules/
/dist/
/dist_test/
/CHANGELOG.md
/.yarn_home/
/src/test/apps/
/src/tools/types/
/sample_react_project
/build_keycloak/

View File

@ -1,11 +0,0 @@
{
"printWidth": 150,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}

88
CHANGELOG.md Normal file
View File

@ -0,0 +1,88 @@
### **0.0.20** (2021-02-27)
- update
- update
### **0.0.19** (2021-02-27)
- update
- update
### **0.0.18** (2021-02-23)
- Bump version number
- Moving on with implementation of the lib
- Update readme
- Readme eddit
- Fixing video link
### **0.0.16** (2021-02-23)
- Bump version
- Give test container credentials
### **0.0.14** (2021-02-23)
- Bump version number
- enable the docker container to be run from the root of the react project
### **0.0.13** (2021-02-23)
- bump version
### **0.0.12** (2021-02-23)
- update readme
### **0.0.11** (2021-02-23)
- Add documentation
### **0.0.10** (2021-02-23)
- Remove extra closing bracket
### **0.0.9** (2021-02-22)
- fix container startup script
- minor update
### **0.0.8** (2021-02-21)
- Include theme properties
### **0.0.7** (2021-02-21)
- fix build
- Fix bundle
### **0.0.6** (2021-02-21)
- Include missing files in the release bundle
### **0.0.5** (2021-02-21)
- Bump version number
- Make the install faster
### **0.0.4** (2021-02-21)
- Fix script visibility
### **0.0.3** (2021-02-21)
- Do not run tests on window
- Add script for downloading base themes
- Generate debug files to be able to test the container
- Fix many little bugs
- refactor
- Almoste there
- Things are starting to take form
- Seems to be working
- First draft
- Remove eslint and prettyer
### **0.0.2** (2021-02-20)
- Update package.json

View File

@ -1,3 +0,0 @@
Looking to contribute? Thank you! PR are more than welcome.
Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.

191
README.md
View File

@ -1,173 +1,94 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
<img src="https://user-images.githubusercontent.com/6702424/80216211-00ef5280-863e-11ea-81de-59f3a3d4b8e4.png">
</p>
<p align="center">
<i>🔏 Create Keycloak themes using React 🔏</i>
<i>🔏 Keycloak theme generator for Reacts app💅</i>
<br>
<br>
<a href="https://github.com/garronej/keycloakify/actions">
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
</a>
<a href="https://bundlephobia.com/package/keycloakify">
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
</a>
<a href="https://www.npmjs.com/package/keycloakify">
<img src="https://img.shields.io/npm/dm/keycloakify">
</a>
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
<img src="https://img.shields.io/npm/l/keycloakify">
</a>
<a href="https://github.com/InseeFrLab/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
</a>
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
<p align="center">
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
</p>
<p align="center"> ---- Project starter / Demo setup ---- </p>
<p align="center">
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
-
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
</p>
<p align="center"> ---- </p>
<img src="https://github.com/garronej/keycloak-react-theming/workflows/ci/badge.svg?branch=develop">
<img src="https://img.shields.io/bundlephobia/minzip/keycloak-react-theming">
<img src="https://img.shields.io/npm/dw/keycloak-react-theming">
<img src="https://img.shields.io/npm/l/keycloak-react-theming">
</p>
<p align="center">
<i>Ultimately this build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
# MOTIVATION
> 🗣 V6 have been released 🎉
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
The problem:
# Changelog highlights
![keycloak_before](https://user-images.githubusercontent.com/6702424/108838381-dbbbcf80-75d3-11eb-8ae8-db41563ef9db.gif)
## 6.8.0
When we redirected to Keycloak the user suffers from a harsh context switch.
On je login/register pages the language is set back to default and the theme is different that the one on the app.
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
individual page (`<Login />`, `<RegisterUserProfile />`, ...) it enables to customize only the header and footer for
example without having to switch to a full-component level customization. [See issue](https://github.com/InseeFrLab/keycloakify/issues/191).
Keycloak does offer a way to customize theses pages but it requires a lot of raw HTML/CSS hacking
to reproduce the look and feel of a specific app. Not mentioning the maintenance cost of such an endeavour.
## 6.7.0
Wouldn't it be great if we could just design the login and register pages as if they where part of our app while
still letting Keycloak handle the heavy lifting of actually authenticating the users?
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/185).
Here is `yarn add keycloak-react-theming` for you 🍸
## 6.6.0
TODO: Insert video after.
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/184).
# How to use
## 6.5.0
## Setting up the build tool
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/183).
Add `keycloak-react-theming` to the dev dependencies of your project `npm install --save-dev keycloak-react-theming` or `yarn add --dev keycloak-react-theming`
then configure your `package.json` build's script to build the keycloak's theme by adding `&& build-keycloak-theme`.
## 6.4.0
Typically you will get:
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
`package.json`:
```json
"devDependencies": {
"keycloak-react-theming": "^0.0.10"
},
"scripts": {
"build": "react-scripts build && build-keycloak-theme"
},
```
## 6.0.0
Then build your app with `yarn run build` or `npm run build`, you will be provided with instructions
about how to load the theme into Keycloak.
- Bundle size drastically reduced, locals and component dynamically loaded.
- First print much quicker, use of React.lazy() everywhere.
- Real i18n API.
- Actual documentation for build options.
[![kickstart_video](https://user-images.githubusercontent.com/6702424/108877866-f146ee80-75ff-11eb-8120-003b3c5f6dd8.png)](https://youtu.be/xTz0Rj7i2v8)
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## Developing your login and register pages in your React app
## 5.8.0
TODO
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/InseeFrLab/keycloakify/issues/141)
# How to implement context persistance
## 5.7.0
If you want dark mode preference, language and others users preferences your can do so
very easily by using [`powerhooks/useGlobalState`](https://github.com/garronej/powerhooks)
- Feat `logout-confirm.ftl`. [PR](https://github.com/InseeFrLab/keycloakify/pull/120)
WARNING: `powerhooks` is still a work in progress.
## 5.6.4
# REQUIREMENTS
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
This tools 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 `static/` directory generated by webpack.
## v5.6.0
**All this is defaults with [`create-react-app`](https://create-react-app.dev)** (tested with 4.0.3=)
Add support for `login-config-totp.ftl` page [#127](https://github.com/InseeFrLab/keycloakify/pull/127).
- For building the theme: `mvn` (Maven) must be installed
- For development, (testing the theme in a local container ): `rm`, `mkdir`, `wget`, `unzip` are assumed to be available.
## v5.3.0
NOTE: This build tool has only be tested on MacOS.
Rename `keycloak_theme_email` to `keycloak_email`.
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
# API Reference
## v5.0.0
## The build tool
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
New i18n system.
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
Part of the lib that runs with node, at build time.
## v4.10.0
- `npx build-keycloak-theme`: Builds the theme, the CWD is assumed to be the root of your react project.
- `npx download-sample-keycloak-themes`: Downloads the keycloak default themes (for development purposes)
Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycloakify/pull/92).
## The fronted lib ( imported into your react
## v4.8.0
Part of the lib that you import in your react project and runs on the browser.
[Email template customization.](#email-template-customization)
## v4.7.4
**M1 Mac** support (for testing locally with a dockerized Keycloak).
## v4.7.2
> WARNING: This is broken.
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
## v4.7.0
Register with user profile enabled: Out of the box `options` validator support.
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
## v4.6.0
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
## v4.5.3
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
with this new method your theme wont break on minor Keycloakify update.
## v4.3.0
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
Every time a page is added it's a breaking change for non CSS-only theme.
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
## v4
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
## v3
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
[when passing params from the app to the login page](https://github.com/InseeFrLab/keycloakify#implement-context-persistence-optional).
## v2.5
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
- Test container now uses Keycloak version `15.0.2`.
## v2
- It's now possible to implement custom `.ftl` pages.
- Support for Keycloak plugins that introduce non standard ftl values.
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
**TODO**

View File

@ -0,0 +1,6 @@
export declare const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
/** Files for being able to run a hot reload keycloak container */
export declare function generateDebugFiles(params: {
packageJsonName: string;
keycloakThemeBuildingDirPath: string;
}): void;

View File

@ -0,0 +1,68 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateDebugFiles = exports.containerLaunchScriptBasename = void 0;
var fs = __importStar(require("fs"));
var path_1 = require("path");
exports.containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
/** Files for being able to run a hot reload keycloak container */
function generateDebugFiles(params) {
var packageJsonName = params.packageJsonName, keycloakThemeBuildingDirPath = params.keycloakThemeBuildingDirPath;
fs.writeFileSync(path_1.join(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"));
var dockerImage = packageJsonName + "/keycloak-hot-reload";
var containerName = "keycloak-testing-container";
fs.writeFileSync(path_1.join(keycloakThemeBuildingDirPath, exports.containerLaunchScriptBasename), Buffer.from([
"#!/bin/bash",
"",
"cd " + keycloakThemeBuildingDirPath,
"",
"docker rm " + containerName + " || true",
"",
"docker build . -t " + dockerImage,
"",
"docker run \\",
" -p 8080:8080 \\",
"\t--name " + containerName + " \\",
" -e KEYCLOAK_USER=admin \\",
" -e KEYCLOAK_PASSWORD=admin \\",
"\t-v " + path_1.join(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", packageJsonName) + ":/opt/jboss/keycloak/themes/" + packageJsonName + ":rw \\",
"\t-it " + dockerImage + ":latest",
""
].join("\n"), "utf8"), { "mode": 493 });
var standaloneHaFilePath = path_1.join(keycloakThemeBuildingDirPath, "configuration", "standalone-ha.xml");
try {
fs.mkdirSync(path_1.dirname(standaloneHaFilePath));
}
catch (_a) { }
fs.writeFileSync(standaloneHaFilePath, fs.readFileSync(path_1.join(__dirname, path_1.basename(standaloneHaFilePath))));
}
exports.generateDebugFiles = generateDebugFiles;
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/bin/build-keycloak-theme/generateDebugFiles/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AACA,qCAAyB;AACzB,6BAA0F;AAE7E,QAAA,6BAA6B,GAAG,qCAAqC,CAAC;AAEnF,kEAAkE;AAClE,SAAgB,kBAAkB,CAC9B,MAGC;IAGO,IAAA,eAAe,GAAmC,MAAM,gBAAzC,EAAE,4BAA4B,GAAK,MAAM,6BAAX,CAAY;IAEjE,EAAE,CAAC,aAAa,CACZ,WAAQ,CAAC,4BAA4B,EAAE,YAAY,CAAC,EACpD,MAAM,CAAC,IAAI,CACP;QACI,4BAA4B;QAC5B,EAAE;QACF,WAAW;QACX,EAAE;QACF,WAAW;QACX,EAAE;QACF,iEAAiE;QACjE,EAAE;QACF,wDAAwD;KAC3D,CAAC,IAAI,CAAC,IAAI,CAAC,EACZ,MAAM,CACT,CACJ,CAAC;IAEF,IAAM,WAAW,GAAM,eAAe,yBAAsB,CAAC;IAC7D,IAAM,aAAa,GAAG,4BAA4B,CAAC;IAEnD,EAAE,CAAC,aAAa,CACZ,WAAQ,CAAC,4BAA4B,EAAE,qCAA6B,CAAC,EACrE,MAAM,CAAC,IAAI,CACP;QACI,aAAa;QACb,EAAE;QACF,QAAM,4BAA8B;QACpC,EAAE;QACF,eAAa,aAAa,aAAU;QACpC,EAAE;QACF,uBAAqB,WAAa;QAClC,EAAE;QACF,eAAe;QACf,kBAAkB;QAClB,cAAW,aAAa,QAAK;QAC7B,4BAA4B;QAC5B,gCAAgC;QAChC,UAAO,WAAQ,CAAC,4BAA4B,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,eAAe,CAAC,oCACpE,eAAe,WAAQ;QACtD,WAAQ,WAAW,YAAS;QAC5B,EAAE;KACL,CAAC,IAAI,CAAC,IAAI,CAAC,EACZ,MAAM,CACT,EACD,EAAE,MAAM,EAAE,GAAK,EAAE,CACpB,CAAC;IAEF,IAAM,oBAAoB,GAAG,WAAQ,CAAC,4BAA4B,EAAE,eAAe,EAAE,mBAAmB,CAAC,CAAC;IAE1G,IAAI;QAAE,EAAE,CAAC,SAAS,CAAC,cAAW,CAAC,oBAAoB,CAAC,CAAC,CAAC;KAAE;IAAC,WAAM,GAAG;IAElE,EAAE,CAAC,aAAa,CACZ,oBAAoB,EACpB,EAAE,CAAC,YAAY,CAAC,WAAQ,CAAC,SAAS,EAAE,eAAY,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAC3E,CAAC;AAEN,CAAC;AAlED,gDAkEC"}

View File

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

View File

@ -0,0 +1,113 @@
<script>const _=
{
"url": {
"loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}"
},
"realm": {
"displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || undefined,
"internationalizationEnabled": ${realm.internationalizationEnabled?c}
},
"locale": (function (){
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
<#if realm.internationalizationEnabled>
var out= [];
<#list locale.supported as lng>
out.push({
"url": "${lng.url}",
"label": "${lng.label}",
"languageTag": "${lng.languageTag}"
});
</#list>
return out;
</#if>
return undefined;
})(),
"current": "${locale.current}"
};
</#if>
return undefined;
})(),
"auth": (function (){
<#if auth?has_content>
var out= {
"showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c}
};
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": "${auth.attemptedUsername}"
}
);
</#if>
return out;
</#if>
return undefined;
})(),
"scripts": (function(){
var out = [];
<#if scripts??>
<#list scripts as script>
out.push("${script}");
</#list>
</#if>
return out;
})(),
"message": (function (){
<#if message?has_content>
return { 
"type": "${message.type}",
"summary": "${kcSanitize(message.summary)?no_esc}"
};
</#if>
return undefined;
})(),
"isAppInitiatedAction": (function (){
<#if isAppInitiatedAction??>
return true;
</#if>
return false;
})()
}
</script>

View File

@ -0,0 +1,11 @@
export declare function generateFtlFilesCodeFactory(params: {
ftlValuesGlobalName: string;
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
}): {
generateFtlFilesCode: (params: {
pageBasename: "login.ftl" | "register.ftl";
}) => {
ftlCode: string;
};
};

View File

@ -0,0 +1,115 @@
"use strict";
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateFtlFilesCodeFactory = void 0;
var cheerio_1 = __importDefault(require("cheerio"));
var replaceImportFromStatic_1 = require("../replaceImportFromStatic");
var fs_1 = __importDefault(require("fs"));
var path_1 = require("path");
var objectKeys_1 = require("evt/tools/typeSafety/objectKeys");
function generateFtlFilesCodeFactory(params) {
var ftlValuesGlobalName = params.ftlValuesGlobalName, cssGlobalsToDefine = params.cssGlobalsToDefine, indexHtmlCode = params.indexHtmlCode;
var $ = cheerio_1.default.load(indexHtmlCode);
$("script:not([src])").each(function () {
var _a = [];
for (var _i = 0; _i < arguments.length; _i++) {
_a[_i] = arguments[_i];
}
var _b = __read(_a, 2), element = _b[1];
var fixedJsCode = replaceImportFromStatic_1.replaceImportFromStaticInJsCode({
ftlValuesGlobalName: ftlValuesGlobalName,
"jsCode": $(element).html()
}).fixedJsCode;
$(element).text(fixedJsCode);
});
[
["link", "href"],
["script", "src"],
].forEach(function (_a) {
var _b = __read(_a, 2), selector = _b[0], attrName = _b[1];
return $(selector).each(function () {
var _a = [];
for (var _i = 0; _i < arguments.length; _i++) {
_a[_i] = arguments[_i];
}
var _b = __read(_a, 2), element = _b[1];
var href = $(element).attr(attrName);
if (!(href === null || href === void 0 ? void 0 : href.startsWith("/"))) {
return;
}
$(element).attr(attrName, "${url.resourcesPath}" + href);
});
});
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
var ftlPlaceholders = {
'{ "x": "xIdLqMeOed9sdLdIdOxdK0d" }': fs_1.default.readFileSync(path_1.join(__dirname, "ftl2js.ftl"))
.toString("utf8")
.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")
};
$("head").prepend(__spreadArray(__spreadArray([], __read((Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
'',
'<style>',
replaceImportFromStatic_1.generateCssCodeToDefineGlobals({ cssGlobalsToDefine: cssGlobalsToDefine }).cssCodeToPrependInHead,
'</style>',
''
]))), [
'<script>',
' Object.assign(',
" window." + ftlValuesGlobalName + ",",
" " + objectKeys_1.objectKeys(ftlPlaceholders)[0],
' );',
'</script>',
'',
objectKeys_1.objectKeys(ftlPlaceholders)[1],
''
]).join("\n"));
var partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params) {
var pageBasename = params.pageBasename;
var $ = cheerio_1.default.load(partiallyFixedIndexHtmlCode);
$("head").prepend([
'',
'<script>',
" window." + ftlValuesGlobalName + " = { \"pageBasename\": \"" + pageBasename + "\" };",
'</script>',
''
].join("\n"));
var ftlCode = $.html();
objectKeys_1.objectKeys(ftlPlaceholders)
.forEach(function (id) { return ftlCode = ftlCode.replace(id, ftlPlaceholders[id]); });
return { ftlCode: ftlCode };
}
return { generateFtlFilesCode: generateFtlFilesCode };
}
exports.generateFtlFilesCodeFactory = generateFtlFilesCodeFactory;
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/bin/build-keycloak-theme/generateFtl/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,oDAA8B;AAC9B,sEAGoC;AACpC,0CAAoB;AACpB,6BAAwC;AACxC,8DAA6D;AAE7D,SAAgB,2BAA2B,CACvC,MAIC;IAGO,IAAA,mBAAmB,GAAwC,MAAM,oBAA9C,EAAE,kBAAkB,GAAoB,MAAM,mBAA1B,EAAE,aAAa,GAAK,MAAM,cAAX,CAAY;IAE1E,IAAM,CAAC,GAAG,iBAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IAEtC,CAAC,CAAC,mBAAmB,CAAC,CAAC,IAAI,CAAC;QAAC,YAAc;aAAd,UAAc,EAAd,qBAAc,EAAd,IAAc;YAAd,uBAAc;;QAAd,IAAA,KAAA,aAAc,EAAR,OAAO,QAAA,CAAC;QAE/B,IAAA,WAAW,GAAK,yDAA+B,CAAC;YACpD,mBAAmB,qBAAA;YACnB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAG;SAC/B,CAAC,YAHiB,CAGhB;QAEH,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAEjC,CAAC,CAAC,CAAC;IAGF;QACG,CAAC,MAAM,EAAE,MAAM,CAAC;QAChB,CAAC,QAAQ,EAAE,KAAK,CAAC;KACV,CAAC,OAAO,CAAC,UAAC,EAAoB;YAApB,KAAA,aAAoB,EAAnB,QAAQ,QAAA,EAAE,QAAQ,QAAA;QACpC,OAAA,CAAC,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC;YAAC,YAAc;iBAAd,UAAc,EAAd,qBAAc,EAAd,IAAc;gBAAd,uBAAc;;YAAd,IAAA,KAAA,aAAc,EAAR,OAAO,QAAA,CAAC;YAE5B,IAAM,IAAI,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YAEvC,IAAI,CAAC,CAAA,IAAI,aAAJ,IAAI,uBAAJ,IAAI,CAAE,UAAU,CAAC,GAAG,CAAC,CAAA,EAAE;gBACxB,OAAO;aACV;YAED,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,EAAE,sBAAsB,GAAG,IAAI,CAAC,CAAC;QAE7D,CAAC,CAAC;IAVF,CAUE,CACL,CAAC;IAEF,6FAA6F;IAC7F,IAAM,eAAe,GAAG;QACpB,oCAAoC,EAChC,YAAE,CAAC,YAAY,CAAC,WAAQ,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;aAC7C,QAAQ,CAAC,MAAM,CAAC;aAChB,KAAK,CAAC,8CAA8C,CAAE,CAAC,CAAC,CAAC;QAClE,sCAAsC,EAClC;YACI,iBAAiB;YACjB,+BAA+B;YAC/B,kEAAkE;YAClE,cAAc;YACd,QAAQ;SACX,CAAC,IAAI,CAAC,IAAI,CAAC;KACnB,CAAC;IAEF,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CACb,uCACO,CAAC,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACpD,EAAE;QACF,SAAS;QACT,wDAA8B,CAC1B,EAAE,kBAAkB,oBAAA,EAAE,CACzB,CAAC,sBAAsB;QACxB,UAAU;QACV,EAAE;KACL,CAAC;QACF,UAAU;QACV,oBAAoB;QACpB,oBAAkB,mBAAmB,MAAG;QACxC,aAAW,uBAAU,CAAC,eAAe,CAAC,CAAC,CAAC,CAAG;QAC3C,QAAQ;QACR,WAAW;QACX,EAAE;QACF,uBAAU,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;QAC9B,EAAE;OACJ,IAAI,CAAC,IAAI,CAAC,CACf,CAAC;IAGF,IAAM,2BAA2B,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAE7C,SAAS,oBAAoB,CACzB,MAEC;QAGO,IAAA,YAAY,GAAK,MAAM,aAAX,CAAY;QAEhC,IAAM,CAAC,GAAG,iBAAO,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC;QAEpD,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CACb;YACI,EAAE;YACF,UAAU;YACV,eAAa,mBAAmB,iCAAyB,YAAY,UAAM;YAC3E,WAAW;YACX,EAAE;SACL,CAAC,IAAI,CAAC,IAAI,CAAC,CACf,CAAC;QAEF,IAAI,OAAO,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QAEvB,uBAAU,CAAC,eAAe,CAAC;aACtB,OAAO,CAAC,UAAA,EAAE,IAAI,OAAA,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,eAAe,CAAC,EAAE,CAAC,CAAC,EAAlD,CAAkD,CAAC,CAAC;QAEvE,OAAO,EAAE,OAAO,SAAA,EAAE,CAAC;IAEvB,CAAC;IAED,OAAO,EAAE,oBAAoB,sBAAA,EAAE,CAAC;AAGpC,CAAC;AAnHD,kEAmHC"}

View File

@ -0,0 +1,11 @@
export declare type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
};
export declare function generateJavaStackFiles(params: {
parsedPackageJson: ParsedPackageJson;
keycloakThemeBuildingDirPath: string;
}): {
jarFilePath: string;
};

View File

@ -0,0 +1,73 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateJavaStackFiles = void 0;
var url = __importStar(require("url"));
var fs = __importStar(require("fs"));
var path_1 = require("path");
function generateJavaStackFiles(params) {
var _a = params.parsedPackageJson, name = _a.name, version = _a.version, homepage = _a.homepage, keycloakThemeBuildingDirPath = params.keycloakThemeBuildingDirPath;
{
var pomFileCode = (function generatePomFileCode() {
var groupId = (function () {
var _a, _b;
var fallbackGroupId = "there.was.no.homepage.field.in.the.package.json." + name;
return (!homepage ?
fallbackGroupId :
(_b = (_a = url.parse(homepage).host) === null || _a === void 0 ? void 0 : _a.split(".").reverse().join(".")) !== null && _b !== void 0 ? _b : fallbackGroupId) + ".keycloak";
})();
var artefactId = name + "-keycloak-theme";
var pomFileCode = [
"<?xml version=\"1.0\"?>",
"<project xmlns=\"http://maven.apache.org/POM/4.0.0\"",
"\txmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"",
"\txsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\">",
"\t<modelVersion>4.0.0</modelVersion>",
"\t<groupId>" + groupId + "</groupId>",
"\t<artifactId>" + artefactId + "</artifactId>",
"\t<version>" + version + "</version>",
"\t<name>" + artefactId + "</name>",
"\t<description />",
"</project>"
].join("\n");
return { pomFileCode: pomFileCode };
})().pomFileCode;
fs.writeFileSync(path_1.join(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
var themeManifestFilePath = path_1.join(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(path_1.dirname(themeManifestFilePath));
}
catch (_b) { }
fs.writeFileSync(themeManifestFilePath, Buffer.from(JSON.stringify({
"themes": [
{
"name": name,
"types": ["login"]
}
]
}, null, 2), "utf8"));
}
return { "jarFilePath": path_1.join(keycloakThemeBuildingDirPath, "target", name + "-" + version + ".jar") };
}
exports.generateJavaStackFiles = generateJavaStackFiles;
//# sourceMappingURL=generateJavaStackFiles.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"generateJavaStackFiles.js","sourceRoot":"","sources":["../../src/bin/build-keycloak-theme/generateJavaStackFiles.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;AACA,uCAA2B;AAC3B,qCAAyB;AACzB,6BAAgE;AAQhE,SAAgB,sBAAsB,CAClC,MAGC;IAIG,IAAA,KAEA,MAAM,kBAFwC,EAAzB,IAAI,UAAA,EAAE,OAAO,aAAA,EAAE,QAAQ,cAAA,EAC5C,4BAA4B,GAC5B,MAAM,6BADsB,CACrB;IAEX;QAEY,IAAA,WAAW,GAAK,CAAC,SAAS,mBAAmB;YAGjD,IAAM,OAAO,GAAG,CAAC;;gBAEb,IAAM,eAAe,GAAG,qDAAmD,IAAM,CAAC;gBAElF,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC;oBACf,eAAe,CAAC,CAAC;oBACjB,MAAA,MAAA,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,IAAI,0CAAE,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,mCAAI,eAAe,CAC9E,GAAG,WAAW,CAAC;YAEpB,CAAC,CAAC,EAAE,CAAC;YAEL,IAAM,UAAU,GAAM,IAAI,oBAAiB,CAAC;YAE5C,IAAM,WAAW,GAAG;gBAChB,yBAAuB;gBACvB,sDAAoD;gBACpD,2DAAwD;gBACxD,sGAAmG;gBACnG,sCAAqC;gBACrC,gBAAa,OAAO,eAAY;gBAChC,mBAAgB,UAAU,kBAAe;gBACzC,gBAAa,OAAO,eAAY;gBAChC,aAAU,UAAU,YAAS;gBAC7B,mBAAkB;gBAClB,YAAY;aACf,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAEb,OAAO,EAAE,WAAW,aAAA,EAAE,CAAC;QAE3B,CAAC,CAAC,EAAE,YAhCe,CAgCd;QAEL,EAAE,CAAC,aAAa,CACZ,WAAQ,CAAC,4BAA4B,EAAE,SAAS,CAAC,EACjD,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CACnC,CAAC;KAEL;IAED;QAEI,IAAM,qBAAqB,GAAG,WAAQ,CAClC,4BAA4B,EAAE,KAAK,EAAE,MAAM,EAC3C,WAAW,EAAE,UAAU,EAAE,sBAAsB,CAClD,CAAC;QAEF,IAAI;YAEA,EAAE,CAAC,SAAS,CAAC,cAAW,CAAC,qBAAqB,CAAC,CAAC,CAAC;SAEpD;QAAC,WAAM,GAAG;QAEX,EAAE,CAAC,aAAa,CACZ,qBAAqB,EACrB,MAAM,CAAC,IAAI,CACP,IAAI,CAAC,SAAS,CAAC;YACX,QAAQ,EAAE;gBACN;oBACI,MAAM,EAAE,IAAI;oBACZ,OAAO,EAAE,CAAC,OAAO,CAAC;iBACrB;aACJ;SACJ,EAAE,IAAI,EAAE,CAAC,CAAC,EACX,MAAM,CACT,CACJ,CAAC;KAEL;IAED,OAAO,EAAE,aAAa,EAAE,WAAQ,CAAC,4BAA4B,EAAE,QAAQ,EAAK,IAAI,SAAI,OAAO,SAAM,CAAC,EAAE,CAAC;AAEzG,CAAC;AAvFD,wDAuFC"}

View File

@ -0,0 +1,6 @@
export declare const ftlValuesGlobalName = "keycloakPagesContext";
export declare function generateKeycloakThemeResources(params: {
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
}): void;

View File

@ -0,0 +1,76 @@
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateKeycloakThemeResources = exports.ftlValuesGlobalName = void 0;
var transformCodebase_1 = require("../../tools/transformCodebase");
var fs = __importStar(require("fs"));
var path_1 = require("path");
var replaceImportFromStatic_1 = require("./replaceImportFromStatic");
var generateFtl_1 = require("./generateFtl");
exports.ftlValuesGlobalName = "keycloakPagesContext";
function generateKeycloakThemeResources(params) {
var themeName = params.themeName, reactAppBuildDirPath = params.reactAppBuildDirPath, keycloakThemeBuildingDirPath = params.keycloakThemeBuildingDirPath;
var themeDirPath = path_1.join(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
var allCssGlobalsToDefine = {};
transformCodebase_1.transformCodebase({
"destDirPath": path_1.join(themeDirPath, "resources"),
"srcDirPath": reactAppBuildDirPath,
"transformSourceCodeString": function (_a) {
var filePath = _a.filePath, sourceCode = _a.sourceCode;
if (/\.css?$/i.test(filePath)) {
var _b = replaceImportFromStatic_1.replaceImportFromStaticInCssCode({ "cssCode": sourceCode.toString("utf8") }), cssGlobalsToDefine = _b.cssGlobalsToDefine, fixedCssCode = _b.fixedCssCode;
allCssGlobalsToDefine = __assign(__assign({}, allCssGlobalsToDefine), cssGlobalsToDefine);
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
var fixedJsCode = replaceImportFromStatic_1.replaceImportFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
ftlValuesGlobalName: exports.ftlValuesGlobalName
}).fixedJsCode;
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return { "modifiedSourceCode": sourceCode };
}
});
var generateFtlFilesCode = generateFtl_1.generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine,
ftlValuesGlobalName: exports.ftlValuesGlobalName,
"indexHtmlCode": fs.readFileSync(path_1.join(reactAppBuildDirPath, "index.html")).toString("utf8")
}).generateFtlFilesCode;
["login.ftl", "register.ftl"].forEach(function (pageBasename) {
var ftlCode = generateFtlFilesCode({ pageBasename: pageBasename }).ftlCode;
fs.writeFileSync(path_1.join(themeDirPath, pageBasename), Buffer.from(ftlCode, "utf8"));
});
fs.writeFileSync(path_1.join(themeDirPath, "theme.properties"), Buffer.from("parent=base\n", "utf8"));
}
exports.generateKeycloakThemeResources = generateKeycloakThemeResources;
//# sourceMappingURL=generateKeycloakThemeResources.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"generateKeycloakThemeResources.js","sourceRoot":"","sources":["../../src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,mEAAkE;AAClE,qCAAyB;AACzB,6BAAwC;AACxC,qEAGmC;AACnC,6CAA4D;AAE/C,QAAA,mBAAmB,GAAG,sBAAsB,CAAC;AAE1D,SAAgB,8BAA8B,CAC1C,MAIC;IAGO,IAAA,SAAS,GAAyD,MAAM,UAA/D,EAAE,oBAAoB,GAAmC,MAAM,qBAAzC,EAAE,4BAA4B,GAAK,MAAM,6BAAX,CAAY;IAEjF,IAAM,YAAY,GAAG,WAAQ,CAAC,4BAA4B,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAErH,IAAI,qBAAqB,GAA2B,EAAE,CAAC;IAEvD,qCAAiB,CAAC;QACd,aAAa,EAAE,WAAQ,CAAC,YAAY,EAAE,WAAW,CAAC;QAClD,YAAY,EAAE,oBAAoB;QAClC,2BAA2B,EAAE,UAAC,EAAwB;gBAAtB,QAAQ,cAAA,EAAE,UAAU,gBAAA;YAEhD,IAAI,UAAU,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAErB,IAAA,KAAuC,0DAAgC,CACzE,EAAE,SAAS,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAC7C,EAFO,kBAAkB,wBAAA,EAAE,YAAY,kBAEvC,CAAC;gBAEF,qBAAqB,yBACd,qBAAqB,GACrB,kBAAkB,CACxB,CAAC;gBAEF,OAAO,EAAE,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,CAAC;aAEtE;YAED,IAAI,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAElB,IAAA,WAAW,GAAK,yDAA+B,CAAC;oBACpD,QAAQ,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;oBACrC,mBAAmB,6BAAA;iBACtB,CAAC,YAHiB,CAGhB;gBAEH,OAAO,EAAE,oBAAoB,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,EAAE,CAAC;aAErE;YAED,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,CAAC;QAEhD,CAAC;KACJ,CAAC,CAAC;IAEK,IAAA,oBAAoB,GAAK,yCAA2B,CAAC;QACzD,oBAAoB,EAAE,qBAAqB;QAC3C,mBAAmB,6BAAA;QACnB,eAAe,EAAE,EAAE,CAAC,YAAY,CAC5B,WAAQ,CAAC,oBAAoB,EAAE,YAAY,CAAC,CAC/C,CAAC,QAAQ,CAAC,MAAM,CAAC;KACrB,CAAC,qBAN0B,CAMzB;IAEF,CAAC,WAAW,EAAE,cAAc,CAAW,CAAC,OAAO,CAAC,UAAA,YAAY;QAEjD,IAAA,OAAO,GAAK,oBAAoB,CAAC,EAAE,YAAY,cAAA,EAAE,CAAC,QAA3C,CAA4C;QAE3D,EAAE,CAAC,aAAa,CACZ,WAAQ,CAAC,YAAY,EAAE,YAAY,CAAC,EACpC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAC/B,CAAA;IAEL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,aAAa,CACZ,WAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAC,EAC1C,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,MAAM,CAAC,CACvC,CAAC;AAEN,CAAC;AA1ED,wEA0EC"}

2
bin/build-keycloak-theme/index.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
export declare const keycloakThemeBuildingDirPath: string;

View File

@ -0,0 +1,86 @@
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.keycloakThemeBuildingDirPath = void 0;
var generateKeycloakThemeResources_1 = require("./generateKeycloakThemeResources");
var generateJavaStackFiles_1 = require("./generateJavaStackFiles");
var path_1 = require("path");
var child_process = __importStar(require("child_process"));
var generateDebugFiles_1 = require("./generateDebugFiles");
var reactProjectDirPath = process.cwd();
var parsedPackageJson = require(path_1.join(reactProjectDirPath, "package.json"));
exports.keycloakThemeBuildingDirPath = path_1.join(reactProjectDirPath, "build_keycloak");
console.log("🔏 Building the keycloak theme...⌚");
if (require.main === module) {
generateKeycloakThemeResources_1.generateKeycloakThemeResources({
keycloakThemeBuildingDirPath: exports.keycloakThemeBuildingDirPath,
"reactAppBuildDirPath": path_1.join(reactProjectDirPath, "build"),
"themeName": parsedPackageJson.name
});
var jarFilePath = generateJavaStackFiles_1.generateJavaStackFiles({
parsedPackageJson: parsedPackageJson,
keycloakThemeBuildingDirPath: exports.keycloakThemeBuildingDirPath
}).jarFilePath;
child_process.execSync("mvn package", { "cwd": exports.keycloakThemeBuildingDirPath });
generateDebugFiles_1.generateDebugFiles({
keycloakThemeBuildingDirPath: exports.keycloakThemeBuildingDirPath,
"packageJsonName": parsedPackageJson.name
});
console.log([
'',
"\u2705 Your keycloak theme has been generated and bundled into ./" + path_1.relative(reactProjectDirPath, jarFilePath) + " \uD83D\uDE80",
"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/" + path_1.basename(jarFilePath) + " https://AN.URL.FOR/" + path_1.basename(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:',
'',
"$ ./" + path_1.relative(reactProjectDirPath, path_1.join(exports.keycloakThemeBuildingDirPath, generateDebugFiles_1.containerLaunchScriptBasename)),
'',
'To enable the theme within keycloak log into the admin console ( 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"));
}
//# sourceMappingURL=index.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/bin/build-keycloak-theme/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAEA,mFAAkF;AAClF,mEAAkE;AAElE,6BAA4F;AAC5F,2DAA+C;AAC/C,2DAAyF;AAGzF,IAAM,mBAAmB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;AAE1C,IAAM,iBAAiB,GAAsB,OAAO,CAAC,WAAQ,CAAC,mBAAmB,EAAE,cAAc,CAAC,CAAC,CAAC;AAEvF,QAAA,4BAA4B,GAAG,WAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAC,CAAC;AAG5F,OAAO,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AAElD,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE;IAEzB,+DAA8B,CAAC;QAC3B,4BAA4B,sCAAA;QAC5B,sBAAsB,EAAE,WAAQ,CAAC,mBAAmB,EAAE,OAAO,CAAC;QAC9D,WAAW,EAAE,iBAAiB,CAAC,IAAI;KACtC,CAAC,CAAC;IAEK,IAAA,WAAW,GAAK,+CAAsB,CAAC;QAC3C,iBAAiB,mBAAA;QACjB,4BAA4B,sCAAA;KAC/B,CAAC,YAHiB,CAGhB;IAEH,aAAa,CAAC,QAAQ,CAClB,aAAa,EACb,EAAE,KAAK,EAAE,oCAA4B,EAAE,CAC1C,CAAC;IAEF,uCAAkB,CAAC;QACf,4BAA4B,sCAAA;QAC5B,iBAAiB,EAAE,iBAAiB,CAAC,IAAI;KAC5C,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC;QACR,EAAE;QACF,sEAA+D,eAAY,CAAC,mBAAmB,EAAE,WAAW,CAAC,kBAAK;QAClH,mJAAiJ;QACjJ,EAAE;QACF,2EAA2E;QAC3E,EAAE;QACF,cAAc;QACd,4BAA4B;QAC5B,oCAAoC;QACpC,kCAAkC;QAClC,yCAAyC;QACzC,oBAAoB;QACpB,kBAAkB;QAClB,iBAAiB;QACjB,kBAAkB;QAClB,gDAA8C,eAAY,CAAC,WAAW,CAAC,4BAAuB,eAAY,CAAC,WAAW,CAAG;QACzH,yBAAyB;QACzB,gCAAgC;QAChC,sCAAsC;QACtC,UAAU;QACV,8BAA8B;QAC9B,gCAAgC;QAChC,qEAAqE;QACrE,EAAE;QACF,EAAE;QACF,8HAA8H;QAC9H,EAAE;QACF,SAAO,eAAY,CAAC,mBAAmB,EAAE,WAAQ,CAAC,oCAA4B,EAAE,kDAA6B,CAAC,CAAG;QACjH,EAAE;QACF,oJAAoJ;QACpJ,mEAAiE,iBAAiB,CAAC,IAAI,MAAG;QAC1F,+EAA+E;QAC/E,EAAE;QACF,+FAA+F;QAC/F,EAAE;KACL,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;CAEjB"}

View File

@ -0,0 +1,17 @@
export declare function replaceImportFromStaticInJsCode(params: {
ftlValuesGlobalName: string;
jsCode: string;
}): {
fixedJsCode: string;
};
export declare function replaceImportFromStaticInCssCode(params: {
cssCode: string;
}): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
};
export declare function generateCssCodeToDefineGlobals(params: {
cssGlobalsToDefine: Record<string, string>;
}): {
cssCodeToPrependInHead: string;
};

View File

@ -0,0 +1,94 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateCssCodeToDefineGlobals = exports.replaceImportFromStaticInCssCode = exports.replaceImportFromStaticInJsCode = void 0;
var crypto = __importStar(require("crypto"));
function replaceImportFromStaticInJsCode(params) {
var jsCode = params.jsCode, ftlValuesGlobalName = params.ftlValuesGlobalName;
var fixedJsCode = jsCode.replace(/"static\//g, "window." + ftlValuesGlobalName + ".url.resourcesPath.replace(/^\\//,\"\") + \"/\" + \"static/");
return { fixedJsCode: fixedJsCode };
}
exports.replaceImportFromStaticInJsCode = replaceImportFromStaticInJsCode;
function replaceImportFromStaticInCssCode(params) {
var _a;
var cssCode = params.cssCode;
var cssGlobalsToDefine = {};
new Set((_a = cssCode.match(/(url\(\/[^)]+\))/g)) !== null && _a !== void 0 ? _a : [])
.forEach(function (match) {
return cssGlobalsToDefine["url" + crypto
.createHash("sha256")
.update(match)
.digest("hex")
.substring(0, 15)] = match;
});
var fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(function (cssVariableName) {
//NOTE: split/join pattern ~ replace all
return fixedCssCode =
fixedCssCode.split(cssGlobalsToDefine[cssVariableName])
.join("var(--" + cssVariableName + ")");
});
return { fixedCssCode: fixedCssCode, cssGlobalsToDefine: cssGlobalsToDefine };
}
exports.replaceImportFromStaticInCssCode = replaceImportFromStaticInCssCode;
function generateCssCodeToDefineGlobals(params) {
var cssGlobalsToDefine = params.cssGlobalsToDefine;
return {
"cssCodeToPrependInHead": __spreadArray(__spreadArray([
":root {"
], __read(Object.keys(cssGlobalsToDefine)
.map(function (cssVariableName) { return [
"--" + cssVariableName + ":",
[
"url(",
"${url.resourcesPath}" +
cssGlobalsToDefine[cssVariableName].match(/^url\(([^)]+)\)$/)[1],
")"
].join("")
].join(" "); })
.map(function (line) { return " " + line + ";"; }))), [
"}"
]).join("\n")
};
}
exports.generateCssCodeToDefineGlobals = generateCssCodeToDefineGlobals;
//# sourceMappingURL=replaceImportFromStatic.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"replaceImportFromStatic.js","sourceRoot":"","sources":["../../src/bin/build-keycloak-theme/replaceImportFromStatic.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,6CAAiC;AAEjC,SAAgB,+BAA+B,CAC3C,MAGC;IAGO,IAAA,MAAM,GAA0B,MAAM,OAAhC,EAAE,mBAAmB,GAAK,MAAM,oBAAX,CAAY;IAE/C,IAAM,WAAW,GAAG,MAAO,CAAC,OAAO,CAC/B,YAAY,EACZ,YAAU,mBAAmB,gEAAwD,CACxF,CAAC;IAEF,OAAO,EAAE,WAAW,aAAA,EAAE,CAAC;AAE3B,CAAC;AAhBD,0EAgBC;AAED,SAAgB,gCAAgC,CAC5C,MAEC;;IAMO,IAAA,OAAO,GAAK,MAAM,QAAX,CAAY;IAE3B,IAAM,kBAAkB,GAA2B,EAAE,CAAC;IAEtD,IAAI,GAAG,CAAC,MAAA,OAAO,CAAC,KAAK,CAAC,mBAAmB,CAAC,mCAAI,EAAE,CAAC;SAC5C,OAAO,CAAC,UAAA,KAAK;QACV,OAAA,kBAAkB,CAClB,KAAK,GAAG,MAAM;aACT,UAAU,CAAC,QAAQ,CAAC;aACpB,MAAM,CAAC,KAAK,CAAC;aACb,MAAM,CAAC,KAAK,CAAC;aACb,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,CACpB,GAAG,KAAK;IANT,CAMS,CACZ,CAAC;IAEN,IAAI,YAAY,GAAG,OAAO,CAAC;IAE3B,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC,OAAO,CACnC,UAAA,eAAe;QACX,wCAAwC;QACxC,OAAA,YAAY;YACZ,YAAY,CAAC,KAAK,CAAC,kBAAkB,CAAC,eAAe,CAAC,CAAC;iBAClD,IAAI,CAAC,WAAS,eAAe,MAAG,CAAC;IAFtC,CAEsC,CAC7C,CAAC;IAEF,OAAO,EAAE,YAAY,cAAA,EAAE,kBAAkB,oBAAA,EAAE,CAAC;AAEhD,CAAC;AApCD,4EAoCC;AAED,SAAgB,8BAA8B,CAC1C,MAEC;IAKO,IAAA,kBAAkB,GAAK,MAAM,mBAAX,CAAY;IAEtC,OAAO;QACH,wBAAwB,EAAE;YACtB,SAAS;kBACN,MAAM,CAAC,IAAI,CAAC,kBAAkB,CAAC;aAC7B,GAAG,CAAC,UAAA,eAAe,IAAI,OAAA;YACpB,OAAK,eAAe,MAAG;YACvB;gBACI,MAAM;gBACN,sBAAsB;oBACtB,kBAAkB,CAAC,eAAe,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAE,CAAC,CAAC,CAAC;gBACjE,GAAG;aACN,CAAC,IAAI,CAAC,EAAE,CAAC;SACb,CAAC,IAAI,CAAC,GAAG,CAAC,EARa,CAQb,CAAC;aACX,GAAG,CAAC,UAAA,IAAI,IAAI,OAAA,SAAO,IAAI,MAAG,EAAd,CAAc,CAAC;YAChC,GAAG;WACL,IAAI,CAAC,IAAI,CAAC;KACf,CAAC;AAEN,CAAC;AA5BD,wEA4BC"}

View File

@ -0,0 +1,2 @@
#!/usr/bin/env node
export {};

View File

@ -0,0 +1,59 @@
#!/usr/bin/env node
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from) {
for (var i = 0, il = from.length, j = to.length; i < il; i++, j++)
to[j] = from[i];
return to;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var fs = __importStar(require("fs"));
var path_1 = require("path");
var build_keycloak_theme_1 = require("./build-keycloak-theme");
var child_process_1 = __importDefault(require("child_process"));
if (!fs.existsSync(build_keycloak_theme_1.keycloakThemeBuildingDirPath)) {
console.log("Error: The keycloak theme need to be build");
process.exit(1);
}
var url = "https://github.com/garronej/keycloak-react-theming/releases/download/v0.0.1/other_keycloak_thems.zip";
__spreadArray([
"wget " + url
], __read(["unzip", "rm"].map(function (prg) { return prg + " " + path_1.basename(url); }))).forEach(function (cmd) { return child_process_1.default.execSync(cmd, { "cwd": path_1.join(build_keycloak_theme_1.keycloakThemeBuildingDirPath, "src", "main", "resources", "theme") }); });
//# sourceMappingURL=download-sample-keycloak-themes.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"download-sample-keycloak-themes.js","sourceRoot":"","sources":["../src/bin/download-sample-keycloak-themes.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,qCAAyB;AACzB,6BAAkE;AAClE,+DAAsE;AACtE,gEAA0C;AAE1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,mDAA4B,CAAC,EAAE;IAC9C,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;IAC1D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;CACnB;AAED,IAAM,GAAG,GAAG,sGAAsG,CAAC;AAEnH;IACI,UAAQ,GAAK;UACV,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,UAAA,GAAG,IAAI,OAAG,GAAG,SAAI,eAAY,CAAC,GAAG,CAAG,EAA7B,CAA6B,CAAC,GAC9D,OAAO,CAAC,UAAA,GAAG,IAAI,OAAA,uBAAa,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,WAAQ,CAAC,mDAA4B,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,CAAC,EAAE,CAAC,EAAnH,CAAmH,CAAC,CAAC"}

1
lib/index.d.ts vendored Normal file
View File

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

14
lib/index.js Normal file
View File

@ -0,0 +1,14 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./keycloakFtlValues"), exports);
//# sourceMappingURL=index.js.map

1
lib/index.js.map Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,sDAAoC"}

38
lib/keycloakFtlValues.d.ts vendored Normal file
View File

@ -0,0 +1,38 @@
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
export declare type LanguageLabel = "Deutsch" | "Norsk" | "Русский" | "Svenska" | "Português (Brasil)" | "Lietuvių" | "English" | "Italiano" | "Français" | "中文简体" | "Español" | "Čeština" | "日本語" | "Slovenčina" | "Polish" | "Català" | "Nederlands" | "tr";
export declare type LanguageTag = "de" | "no" | "ru" | "sv" | "pt-BR" | "lt" | "en" | "it" | "fr" | "zh-CN" | "es" | "cs" | "ja" | "sk" | "pl" | "ca" | "nl" | "tr";
export declare type KeycloakFtlValues = {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
url: {
loginAction: string;
resourcesPath: string;
resourcesCommonPath: string;
loginRestartFlowUrl: string;
};
realm: {
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
};
locale?: {
supported: {
url: string;
label: LanguageLabel;
languageTag: LanguageTag;
};
current: LanguageLabel;
};
auth?: {
showUsername: boolean;
showResetCredentials: boolean;
showTryAnotherWayLink: boolean;
attemptedUsername?: boolean;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
isAppInitiatedAction: boolean;
};
export declare const keycloakPagesContext: KeycloakFtlValues | undefined;

9
lib/keycloakFtlValues.js Normal file
View File

@ -0,0 +1,9 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.keycloakPagesContext = void 0;
var generateKeycloakThemeResources_1 = require("../bin/build-keycloak-theme/generateKeycloakThemeResources");
var id_1 = require("evt/tools/typeSafety/id");
exports.keycloakPagesContext = (_a = {}, _a[generateKeycloakThemeResources_1.ftlValuesGlobalName] = id_1.id(window[generateKeycloakThemeResources_1.ftlValuesGlobalName]), _a).keycloakPagesContext;
;
//# sourceMappingURL=keycloakFtlValues.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"keycloakFtlValues.js","sourceRoot":"","sources":["../src/lib/keycloakFtlValues.ts"],"names":[],"mappings":";;;;AAGA,6GAAiG;AAEjG,8CAA6C;AA+C9B,QAAA,oBAAoB,aAC7B,GAAC,oDAAmB,IAAG,OAAE,CAAiC,MAAc,CAAC,oDAAmB,CAAC,CAAC,2BAAG;AACvG,CAAC"}

View File

@ -1,94 +1,85 @@
{
"name": "keycloakify",
"version": "6.8.3",
"name": "keycloak-react-theming",
"version": "0.0.20",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
"url": "git://github.com/garronej/keycloakify.git"
},
"main": "dist/lib/index.js",
"types": "dist/lib/index.d.ts",
"scripts": {
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/lib && yarn grant-exec-perms && yarn copy-files dist/",
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
"pretest": "yarn build:test",
"test": "node dist_test/test/bin && node dist_test/test/lib",
"generate-messages": "node dist/bin/generate-i18n-messages.js",
"link_in_test_app": "node dist/bin/link_in_test_app.js",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
"format": "yarn _format --write",
"format:check": "yarn _format --list-different"
"url": "git://github.com/garronej/keycloak-react-theming.git"
},
"main": "lib/index.js",
"types": "lib/index.d.ts",
"bin": {
"keycloakify": "dist/bin/keycloakify/index.js",
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
},
"lint-staged": {
"*.{ts,tsx,json,md}": [
"prettier --write"
]
},
"husky": {
"hooks": {
"pre-commit": "lint-staged -v"
}
"build-keycloak-theme": "bin/build-keycloak-theme/index.js",
"download-sample-keycloak-themes": "bin/download-sample-keycloak-themes.js"
},
"author": "u/garronej",
"license": "MIT",
"files": [
"src/",
"dist/",
"!dist/tsconfig.tsbuildinfo"
"src/bin/build-keycloak-theme/generateDebugFiles/index.ts",
"src/bin/build-keycloak-theme/generateDebugFiles/standalone-ha.xml",
"src/bin/build-keycloak-theme/generateFtl/ftl2js.ftl",
"src/bin/build-keycloak-theme/generateFtl/index.ts",
"src/bin/build-keycloak-theme/generateJavaStackFiles.ts",
"src/bin/build-keycloak-theme/generateKeycloakThemeResources.ts",
"src/bin/build-keycloak-theme/index.ts",
"src/bin/build-keycloak-theme/replaceImportFromStatic.ts",
"src/bin/download-sample-keycloak-themes.ts",
"src/lib/index.ts",
"src/lib/keycloakFtlValues.ts",
"src/tools/crawl.ts",
"src/tools/transformCodebase.ts",
"bin/build-keycloak-theme/generateDebugFiles/index.d.ts",
"bin/build-keycloak-theme/generateDebugFiles/index.js",
"bin/build-keycloak-theme/generateDebugFiles/index.js.map",
"bin/build-keycloak-theme/generateDebugFiles/standalone-ha.xml",
"bin/build-keycloak-theme/generateFtl/ftl2js.ftl",
"bin/build-keycloak-theme/generateFtl/index.d.ts",
"bin/build-keycloak-theme/generateFtl/index.js",
"bin/build-keycloak-theme/generateFtl/index.js.map",
"bin/build-keycloak-theme/generateJavaStackFiles.d.ts",
"bin/build-keycloak-theme/generateJavaStackFiles.js",
"bin/build-keycloak-theme/generateJavaStackFiles.js.map",
"bin/build-keycloak-theme/generateKeycloakThemeResources.d.ts",
"bin/build-keycloak-theme/generateKeycloakThemeResources.js",
"bin/build-keycloak-theme/generateKeycloakThemeResources.js.map",
"bin/build-keycloak-theme/index.d.ts",
"bin/build-keycloak-theme/index.js",
"bin/build-keycloak-theme/index.js.map",
"bin/build-keycloak-theme/replaceImportFromStatic.d.ts",
"bin/build-keycloak-theme/replaceImportFromStatic.js",
"bin/build-keycloak-theme/replaceImportFromStatic.js.map",
"bin/download-sample-keycloak-themes.d.ts",
"bin/download-sample-keycloak-themes.js",
"bin/download-sample-keycloak-themes.js.map",
"lib/index.d.ts",
"lib/index.js",
"lib/index.js.map",
"lib/keycloakFtlValues.d.ts",
"lib/keycloakFtlValues.js",
"lib/keycloakFtlValues.js.map",
"tools/crawl.d.ts",
"tools/crawl.js",
"tools/crawl.js.map",
"tools/transformCodebase.d.ts",
"tools/transformCodebase.js",
"tools/transformCodebase.js.map"
],
"keywords": [
"bluehats",
"keycloak",
"react",
"theme",
"FreeMarker",
"ftl",
"login",
"register"
"theme"
],
"homepage": "https://github.com/garronej/keycloakify",
"peerDependencies": {
"@emotion/react": "^11.4.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"homepage": "https://github.com/garronej/keycloak-react-theming",
"devDependencies": {
"@babel/core": "^7.0.0",
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"@types/node": "^10.0.0",
"copyfiles": "^2.4.1",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "18.1.0",
"denoify": "^0.6.5",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"scripting-tools": "^0.19.13",
"typescript": "^4.1.5"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.6",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"powerhooks": "^0.20.23",
"react-markdown": "^5.0.3",
"rfc4648": "^1.5.2",
"scripting-tools": "^0.19.13",
"tsafe": "^1.1.3",
"tss-react": "^4.3.4",
"zod": "^3.17.10"
"evt": "^1.9.12"
}
}

View File

@ -1,27 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"baseBranches": ["main", "landingpage"],
"extends": ["config:base"],
"dependencyDashboard": false,
"bumpVersion": "patch",
"rangeStrategy": "bump",
"ignorePaths": [".github/**"],
"branchPrefix": "renovate_",
"vulnerabilityAlerts": {
"enabled": false
},
"packageRules": [
{
"packagePatterns": ["*"],
"excludePackagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
"enabled": false
},
{
"packagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "branch",
"groupName": "garronej_modules_update"
}
]
}

View File

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

View File

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

View File

@ -0,0 +1,113 @@
<script>const _=
{
"url": {
"loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}"
},
"realm": {
"displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || undefined,
"internationalizationEnabled": ${realm.internationalizationEnabled?c}
},
"locale": (function (){
<#if realm.internationalizationEnabled>
return {
"supported": (function(){
<#if realm.internationalizationEnabled>
var out= [];
<#list locale.supported as lng>
out.push({
"url": "${lng.url}",
"label": "${lng.label}",
"languageTag": "${lng.languageTag}"
});
</#list>
return out;
</#if>
return undefined;
})(),
"current": "${locale.current}"
};
</#if>
return undefined;
})(),
"auth": (function (){
<#if auth?has_content>
var out= {
"showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c}
};
<#if auth.showUsername() && !auth.showResetCredentials()>
Object.assign(
out,
{
"attemptedUsername": "${auth.attemptedUsername}"
}
);
</#if>
return out;
</#if>
return undefined;
})(),
"scripts": (function(){
var out = [];
<#if scripts??>
<#list scripts as script>
out.push("${script}");
</#list>
</#if>
return out;
})(),
"message": (function (){
<#if message?has_content>
return { 
"type": "${message.type}",
"summary": "${kcSanitize(message.summary)?no_esc}"
};
</#if>
return undefined;
})(),
"isAppInitiatedAction": (function (){
<#if isAppInitiatedAction??>
return true;
</#if>
return false;
})()
}
</script>

View File

@ -0,0 +1,127 @@
import cheerio from "cheerio";
import {
replaceImportFromStaticInJsCode,
generateCssCodeToDefineGlobals
} from "../replaceImportFromStatic";
import fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
export function generateFtlFilesCodeFactory(
params: {
ftlValuesGlobalName: string;
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
}
) {
const { ftlValuesGlobalName, cssGlobalsToDefine, indexHtmlCode } = params;
const $ = cheerio.load(indexHtmlCode);
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
ftlValuesGlobalName,
"jsCode": $(element).html()!
});
$(element).text(fixedJsCode);
});
([
["link", "href"],
["script", "src"],
] as const).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
const href = $(element).attr(attrName);
if (!href?.startsWith("/")) {
return;
}
$(element).attr(attrName, "${url.resourcesPath}" + href);
})
);
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const ftlPlaceholders = {
'{ "x": "xIdLqMeOed9sdLdIdOxdK0d" }':
fs.readFileSync(pathJoin(__dirname, "ftl2js.ftl"))
.toString("utf8")
.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")
};
$("head").prepend(
[
...(Object.keys(cssGlobalsToDefine).length === 0 ? [] : [
'',
'<style>',
generateCssCodeToDefineGlobals(
{ cssGlobalsToDefine }
).cssCodeToPrependInHead,
'</style>',
''
]),
'<script>',
' Object.assign(',
` window.${ftlValuesGlobalName},`,
` ${objectKeys(ftlPlaceholders)[0]}`,
' );',
'</script>',
'',
objectKeys(ftlPlaceholders)[1],
''
].join("\n"),
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(
params: {
pageBasename: "login.ftl" | "register.ftl"
}
): { ftlCode: string; } {
const { pageBasename } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
$("head").prepend(
[
'',
'<script>',
` window.${ftlValuesGlobalName} = { "pageBasename": "${pageBasename}" };`,
'</script>',
''
].join("\n")
);
let ftlCode = $.html();
objectKeys(ftlPlaceholders)
.forEach(id => ftlCode = ftlCode.replace(id, ftlPlaceholders[id]));
return { ftlCode };
}
return { generateFtlFilesCode };
}

View File

@ -0,0 +1,100 @@
import * as url from "url";
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
export type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
};
export function generateJavaStackFiles(
params: {
parsedPackageJson: ParsedPackageJson;
keycloakThemeBuildingDirPath: string;
}
): { jarFilePath: string; } {
const {
parsedPackageJson: { name, version, homepage },
keycloakThemeBuildingDirPath
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): { pomFileCode: string; } {
const groupId = (() => {
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${name}`;
return (!homepage ?
fallbackGroupId :
url.parse(homepage).host?.split(".").reverse().join(".") ?? fallbackGroupId
) + ".keycloak";
})();
const artefactId = `${name}-keycloak-theme`;
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artefactId}</artifactId>`,
` <version>${version}</version>`,
` <name>${artefactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, "pom.xml"),
Buffer.from(pomFileCode, "utf8")
);
}
{
const themeManifestFilePath = pathJoin(
keycloakThemeBuildingDirPath, "src", "main",
"resources", "META-INF", "keycloak-themes.json"
);
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch { }
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify({
"themes": [
{
"name": name,
"types": ["login"]
}
]
}, null, 2),
"utf8"
)
);
}
return { "jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${name}-${version}.jar`) };
}

View File

@ -0,0 +1,88 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin } from "path";
import {
replaceImportFromStaticInCssCode,
replaceImportFromStaticInJsCode
} from "./replaceImportFromStatic";
import { generateFtlFilesCodeFactory } from "./generateFtl";
export const ftlValuesGlobalName = "keycloakPagesContext";
export function generateKeycloakThemeResources(
params: {
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
}
) {
const { themeName, reactAppBuildDirPath, keycloakThemeBuildingDirPath } = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({
"destDirPath": pathJoin(themeDirPath, "resources"),
"srcDirPath": reactAppBuildDirPath,
"transformSourceCodeString": ({ filePath, sourceCode }) => {
if (/\.css?$/i.test(filePath)) {
const { cssGlobalsToDefine, fixedCssCode } = replaceImportFromStaticInCssCode(
{ "cssCode": sourceCode.toString("utf8") }
);
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
const { fixedJsCode } = replaceImportFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
ftlValuesGlobalName
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return { "modifiedSourceCode": sourceCode };
}
});
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine,
ftlValuesGlobalName,
"indexHtmlCode": fs.readFileSync(
pathJoin(reactAppBuildDirPath, "index.html")
).toString("utf8")
});
(["login.ftl", "register.ftl"] as const).forEach(pageBasename => {
const { ftlCode } = generateFtlFilesCode({ pageBasename });
fs.writeFileSync(
pathJoin(themeDirPath, pageBasename),
Buffer.from(ftlCode, "utf8")
)
});
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from("parent=base\n", "utf8")
);
}

View File

@ -0,0 +1,81 @@
#!/usr/bin/env node
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";
const reactProjectDirPath = process.cwd();
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
console.log("🔏 Building the keycloak theme...⌚");
if (require.main === module) {
generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
"themeName": parsedPackageJson.name
});
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 ( 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"));
}

View File

@ -0,0 +1,91 @@
import * as crypto from "crypto";
export function replaceImportFromStaticInJsCode(
params: {
ftlValuesGlobalName: string;
jsCode: string;
}
): { fixedJsCode: string; } {
const { jsCode, ftlValuesGlobalName } = params;
const fixedJsCode = jsCode!.replace(
/"static\//g,
`window.${ftlValuesGlobalName}.url.resourcesPath.replace(/^\\//,"") + "/" + "static/`
);
return { fixedJsCode };
}
export function replaceImportFromStaticInCssCode(
params: {
cssCode: string;
}
): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/(url\(\/[^)]+\))/g) ?? [])
.forEach(match =>
cssGlobalsToDefine[
"url" + crypto
.createHash("sha256")
.update(match)
.digest("hex")
.substring(0, 15)
] = match
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
fixedCssCode =
fixedCssCode.split(cssGlobalsToDefine[cssVariableName])
.join(`var(--${cssVariableName})`)
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(
params: {
cssGlobalsToDefine: Record<string, string>;
}
): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine } = params;
return {
"cssCodeToPrependInHead": [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName => [
`--${cssVariableName}:`,
[
"url(",
"${url.resourcesPath}" +
cssGlobalsToDefine[cssVariableName].match(/^url\(([^)]+)\)$/)![1],
")"
].join("")
].join(" "))
.map(line => ` ${line};`),
"}"
].join("\n")
};
}

View File

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

View File

@ -1,40 +0,0 @@
#!/usr/bin/env node
import { keycloakThemeBuildingDirPath } from "./keycloakify";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath, isSilent } = params;
for (const ext of ["", "-community"]) {
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
isSilent
});
}
}
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
isSilent
});
})();
}

View File

@ -0,0 +1,18 @@
#!/usr/bin/env node
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
import child_process from "child_process";
if (!fs.existsSync(keycloakThemeBuildingDirPath)) {
console.log("Error: The keycloak theme need to be build");
process.exit(1);
}
const url = "https://github.com/garronej/keycloak-react-theming/releases/download/v0.0.1/other_keycloak_thems.zip";
[
`wget ${url}`,
...["unzip", "rm"].map(prg => `${prg} ${pathBasename(url)}`),
].forEach(cmd => child_process.execSync(cmd, { "cwd": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme") }));

View File

@ -1,87 +0,0 @@
import "minimal-polyfills/Object.fromEntries";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
rm_rf(tmpDirPath);
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
{
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
crawl(baseThemeDirPath).forEach(filePath => {
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/);
if (match === null) {
return;
}
const [, typeOfPage, language] = match;
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
});
}
rm_r(tmpDirPath);
Object.keys(record).forEach(pageType => {
const recordForPageType = record[pageType];
Object.keys(recordForPageType).forEach(language => {
const filePath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_messages", keycloakVersion, pageType, `${language}.ts`);
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
fs.writeFileSync(
filePath,
Buffer.from(
[
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
"//PLEASE DO NOT EDIT MANUALLY",
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
"export default messages;",
"/* spell-checker: enable */"
].join("\n"),
"utf8"
)
);
logger.log(`${filePath} wrote`);
});
});
}

View File

@ -1,180 +0,0 @@
import { z } from "zod";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
//NOTE: Only for the pom.xml file, questionable utility...
groupId: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
isSilent: boolean;
}): BuildOptions {
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
const url = (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (CNAME !== undefined) {
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
}
if (url === undefined) {
return undefined;
}
return {
"origin": url.origin,
"pathname": (() => {
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
})();
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraPages, extraThemeProperties } = keycloakify ?? {};
const themeName = name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"version": version,
extraPages,
extraThemeProperties,
isSilent
};
})();
if (isExternalAssetsCliParamProvided) {
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
...common,
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
url !== undefined,
[
"Can't compile in external assets mode if we don't know where",
"the app will be hosted.",
"You should provide a homepage field in the package.json (or create a",
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
}
}
return id<BuildOptions.Standalone>({
...common,
"isStandalone": true,
"urlPathname": url?.pathname
});
}

View File

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

View File

@ -1,350 +0,0 @@
<script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out =
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= {
<#assign fieldNames = [
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
]>
<#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute>
<#if fieldNames?seq_contains(attribute.name)>
<#continue>
</#if>
<#assign fieldNames += [attribute.name]>
</#list>
</#if>
<#recover>
</#attempt>
"printIfExists": function (fieldName, x) {
<#if !messagesPerField?? >
return undefined;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"existsError": function (fieldName) {
<#if !messagesPerField?? >
return false;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"get": function (fieldName) {
<#if !messagesPerField?? >
return '';
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
<#if messagesPerField.existsError('${fieldName}')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
</#if>
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
},
"exists": function (fieldName) {
<#if !messagesPerField?? >
return false;
<#else>
<#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){
<#attempt>
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
<#recover>
</#attempt>
}
</#list>
throw new Error("There is no " + fieldName + " field");
</#if>
}
};
out["pageId"] = "${pageId}";
return out;
})()
<#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = "">
<#attempt>
<#local isHash = object?is_hash || object?is_hash_ex>
<#recover>
<#return "ABORT: Can't evaluate if " + path?join(".") + " is hash">
</#attempt>
<#if isHash>
<#if path?size gt 10>
<#return "ABORT: Too many recursive calls">
</#if>
<#local keys = "">
<#attempt>
<#local keys = object?keys>
<#recover>
<#return "ABORT: We can't list keys on this object">
</#attempt>
<#local out_seq = []>
<#list keys as key>
<#if ["class","declaredConstructors","superclass","declaringClass" ]?seq_contains(key) >
<#continue>
</#if>
<#if
(
["loginUpdatePasswordUrl", "loginUpdateProfileUrl", "loginUsernameReminderUrl", "loginUpdateTotpUrl"]?seq_contains(key) &&
are_same_path(path, ["url"])
) || (
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
<#-- https://github.com/InseeFrLab/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
<#-- https://github.com/InseeFrLab/keycloakify/issues/109#issuecomment-1134610163 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink())
) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"]) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
key == "identityProviderBrokerCtx" &&
are_same_path(path, []) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(pageId)
) || (
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
are_same_path(path, ["realm"])
)
>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
<#continue>
</#if>
<#if key == "attemptedUsername" && are_same_path(path, ["auth"])>
<#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#continue>
</#if>
<#recover>
</#attempt>
</#if>
<#attempt>
<#if !object[key]??>
<#continue>
</#if>
<#recover>
<#local out_seq += ["/*Couldn't test if '" + key + "' is available on this object*/"]>
<#continue>
</#attempt>
<#local propertyValue = "">
<#attempt>
<#local propertyValue = object[key]>
<#recover>
<#local out_seq += ["/*Couldn't dereference '" + key + "' on this object*/"]>
<#continue>
</#attempt>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(propertyValue, path + [ key ])>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + key + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += ['"' + key + '": ' + rec_out + ","]>
</#list>
<#return (["{"] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "}"])?join("\n")>
</#if>
<#local isMethod = "">
<#attempt>
<#local isMethod = object?is_method>
<#recover>
<#return "ABORT: Can't test if it'sa method.">
</#attempt>
<#if isMethod>
<#if are_same_path(path, ["auth", "showUsername"])>
<#attempt>
<#return auth.showUsername()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showUsername()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showResetCredentials"])>
<#attempt>
<#return auth.showResetCredentials()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showResetCredentials()">
</#attempt>
</#if>
<#if are_same_path(path, ["auth", "showTryAnotherWayLink"])>
<#attempt>
<#return auth.showTryAnotherWayLink()?c>
<#recover>
<#return "ABORT: Couldn't evaluate auth.showTryAnotherWayLink()">
</#attempt>
</#if>
<#return "ABORT: It's a method">
</#if>
<#local isBoolean = "">
<#attempt>
<#local isBoolean = object?is_boolean>
<#recover>
<#return "ABORT: Can't test if it's a boolean">
</#attempt>
<#if isBoolean>
<#return object?c>
</#if>
<#local isEnumerable = "">
<#attempt>
<#local isEnumerable = object?is_enumerable>
<#recover>
<#return "ABORT: Can't test if it's an enumerable">
</#attempt>
<#if isEnumerable>
<#local out_seq = []>
<#local i = 0>
<#list object as array_item>
<#if !array_item??>
<#local out_seq += ["null,"]>
<#continue>
</#if>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>
<#if rec_out?starts_with("ABORT:")>
<#local errorMessage = rec_out?remove_beginning("ABORT:")>
<#if errorMessage != " It's a method" >
<#local out_seq += ["/*" + i?string + ": " + errorMessage + "*/"]>
</#if>
<#continue>
</#if>
<#local out_seq += [rec_out + ","]>
</#list>
<#return (["["] + out_seq?map(str -> ""?right_pad(4 * (path?size + 1)) + str) + [ ""?right_pad(4 * path?size) + "]"])?join("\n")>
</#if>
<#attempt>
<#return '"' + object?js_string + '"'>;
<#recover>
</#attempt>
<#return "ABORT: Couldn't convert into string non hash, non method, non boolean, non enumerable object">
</#function>
<#function are_same_path path searchedPath>
<#if path?size != searchedPath?size>
<#return false>
</#if>
<#local i=0>
<#list path as property>
<#local searchedProperty=searchedPath[i]>
<#if searchedProperty?is_string && searchedProperty == "*">
<#continue>
</#if>
<#if searchedProperty?is_string && !property?is_string>
<#return false>
</#if>
<#if searchedProperty?is_number && !property?is_number>
<#return false>
</#if>
<#if searchedProperty?string != property?string>
<#return false>
</#if>
<#local i+= 1>
</#list>
<#return true>
</#function>
</script>

View File

@ -1,190 +0,0 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [
"login.ftl",
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl"
] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export type PageId = typeof pageIds[number];
export function generateFtlFilesCodeFactory(params: {
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!,
buildOptions
});
$(element).text(fixedJsCode);
});
$("style").each((...[, element]) => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!,
buildOptions
});
$(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,
buildOptions.isStandalone
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
);
})
);
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildOptions
}).cssCodeToPrependInHead,
"</style>",
""
].join("\n")
);
}
}
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>',
" </#list>",
"</#if>"
].join("\n")
};
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]
].join("\n")
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params: { pageId: string }): {
ftlCode: string;
} {
const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html();
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
return { ftlCode };
}
return { generateFtlFilesCode };
}

View File

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

View File

@ -1,89 +0,0 @@
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
groupId: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateJavaStackFiles(params: {
version: string;
keycloakThemeBuildingDirPath: string;
doBundlesEmailTemplate: boolean;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const {
version,
buildOptions: { groupId, themeName },
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const artefactId = `${themeName}-keycloak-theme`;
const pomFileCode = [
`<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`,
` xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"`,
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
` <modelVersion>4.0.0</modelVersion>`,
` <groupId>${groupId}</groupId>`,
` <artifactId>${artefactId}</artifactId>`,
` <version>${version}</version>`,
` <name>${artefactId}</name>`,
` <description />`,
`</project>`
].join("\n");
return { pomFileCode };
})();
fs.writeFileSync(pathJoin(keycloakThemeBuildingDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
}
{
const themeManifestFilePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "META-INF", "keycloak-themes.json");
try {
fs.mkdirSync(pathDirname(themeManifestFilePath));
} catch {}
fs.writeFileSync(
themeManifestFilePath,
Buffer.from(
JSON.stringify(
{
"themes": [
{
"name": themeName,
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])]
}
]
},
null,
2
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`)
};
}

View File

@ -1,203 +0,0 @@
import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import * as child_process from "child_process";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateKeycloakThemeResources(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
keycloakThemeEmailDirPath: string;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath
})
) {
return undefined;
}
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8")
});
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine
};
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
buildOptions
});
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
let doBundlesEmailTemplate: boolean;
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
].join("\n")
);
doBundlesEmailTemplate = false;
break email;
}
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": keycloakThemeEmailDirPath,
"destDirPath": pathJoin(themeDirPath, "..", "email")
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
"buildOptions": buildOptions
});
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
child_process.execSync(`rm -r ${tmpDirPath}`);
}
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
return { doBundlesEmailTemplate };
}

View File

@ -1,65 +0,0 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: {
keycloakVersion: string;
keycloakThemeBuildingDirPath: string;
buildOptions: BuildOptionsLike;
}) {
const {
keycloakThemeBuildingDirPath,
keycloakVersion,
buildOptions: { themeName }
} = params;
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from(
[
"#!/bin/bash",
"",
`docker rm ${containerName} || true`,
"",
`cd ${keycloakThemeBuildingDirPath}`,
"",
"docker run \\",
" -p 8080:8080 \\",
` --name ${containerName} \\`,
" -e KEYCLOAK_ADMIN=admin \\",
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
` -v ${pathJoin(
keycloakThemeBuildingDirPath,
"src",
"main",
"resources",
"theme",
themeName
)}:/opt/keycloak/themes/${themeName}:rw \\`,
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
""
].join("\n"),
"utf8"
),
{ "mode": 0o755 }
);
}

View File

@ -1,8 +0,0 @@
#!/usr/bin/env node
export * from "./keycloakify";
import { main } from "./keycloakify";
if (require.main === module) {
main();
}

View File

@ -1,119 +0,0 @@
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
const reactProjectDirPath = process.cwd();
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
export function main() {
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const buildOptions = readBuildOptions({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
"CNAME": (() => {
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
buildOptions,
//We have to leave it at that otherwise we break our default theme.
//Problem is that we can't guarantee that the the old resources
//will still be available on the newer keycloak version.
"keycloakVersion": "11.0.3"
});
const { jarFilePath } = generateJavaStackFiles({
"version": buildOptions.version,
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate,
buildOptions
});
child_process.execSync("mvn package", {
"cwd": keycloakThemeBuildingDirPath
});
//We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "19.0.1";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath,
"keycloakVersion": containerKeycloakVersion,
buildOptions
});
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
"",
//TODO: Restore when we find a good Helm chart for Keycloak.
//"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/keycloak/providers",
" extraEnv: |",
" - name: KEYCLOAK_USER",
" value: admin",
" - name: KEYCLOAK_PASSWORD",
" value: xxxxxxxxx",
" - name: JAVA_OPTS",
" value: -Dkeycloak.profile=preview",
"",
"",
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
"",
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename))} 👈`,
"",
"Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags",
"",
"Once your container is up and running: ",
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
'- Create a realm named "myrealm"',
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
"",
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
""
].join("\n")
);
}

View File

@ -1,86 +0,0 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
};
export type ExternalAssets = {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { 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, buildOptions } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
(...[, n, u, e]) => `
${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
? `
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){}
});
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
}
}
return "${u}";
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };
}

View File

@ -1,64 +0,0 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = {
urlPathname: string | undefined;
};
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine, buildOptions } = params;
return {
"cssCodeToPrependInHead": [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName =>
[
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/"
)
].join(" ")
)
.map(line => ` ${line};`),
"}"
].join("\n")
};
}

View File

@ -1,47 +0,0 @@
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
urlPathname: string | undefined;
};
export type Standalone = Common & {
isStandalone: true;
};
export type ExternalAssets = Common & {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
fixedCssCode: string;
} {
const { cssCode, buildOptions } = params;
const fixedCssCode = cssCode.replace(
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(${
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
})`
);
return { fixedCssCode };
}

View File

@ -1,126 +0,0 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { exclude } from "tsafe/exclude";
import * as fs from "fs";
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
fs.writeFileSync(
pathJoin(keycloakifyDirPath, "dist", "package.json"),
Buffer.from(
JSON.stringify(
(() => {
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"));
return {
...packageJsonParsed,
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
"types": packageJsonParsed["types"].replace(/^dist\//, "")
};
})(),
null,
2
),
"utf8"
)
);
const commonThirdPartyDeps = (() => {
const namespaceModuleNames = ["@emotion"];
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"];
return [
...namespaceModuleNames
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...standaloneModuleNames
];
})();
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
execSync(["rm -rf", "mkdir"].map(cmd => `${cmd} ${yarnHomeDirPath}`).join(" && "));
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params;
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : [])].join(" ");
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`);
execSync(cmd, {
cwd,
"env": {
...process.env,
"HOME": yarnHomeDirPath
}
});
};
const testAppPaths = (() => {
const arg = process.argv[2];
const testAppNames = arg !== undefined ? [arg] : ["keycloakify-starter", "keycloakify-advanced-starter"];
return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(keycloakifyDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter(exclude(undefined));
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ===");
const total = commonThirdPartyDeps.length;
let current = 0;
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
current++;
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
);
execYarnLink({ "cwd": localInstallPath });
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": "keycloakify"
})
);

View File

@ -1,5 +0,0 @@
import { pathJoin } from "./tools/pathJoin";
export const mockTestingSubDirOfPublicDirBasename = "keycloak_static";
export const mockTestingResourcesPath = pathJoin(mockTestingSubDirOfPublicDirBasename, "resources");
export const mockTestingResourcesCommonPath = pathJoin(mockTestingResourcesPath, "resources_common");

View File

@ -1,47 +0,0 @@
import { getLatestsSemVersionedTagFactory } from "./tools/octokit-addons/getLatestsSemVersionedTag";
import { Octokit } from "@octokit/rest";
import cliSelect from "cli-select";
export async function promptKeycloakVersion() {
const { getLatestsSemVersionedTag } = (() => {
const { octokit } = (() => {
const githubToken = process.env.GITHUB_TOKEN;
const octokit = new Octokit(githubToken === undefined ? undefined : { "auth": githubToken });
return { octokit };
})();
const { getLatestsSemVersionedTag } = getLatestsSemVersionedTagFactory({ octokit });
return { getLatestsSemVersionedTag };
})();
console.log("Initialize the directory with email template from which keycloak version?");
const tags = [
...(await getLatestsSemVersionedTag({
"count": 10,
"doIgnoreBeta": true,
"owner": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
"11.0.3"
];
if (process.env["GITHUB_ACTIONS"] === "true") {
return { "keycloakVersion": tags[0] };
}
const { value: keycloakVersion } = await cliSelect<string>({
"values": tags
}).catch(() => {
console.log("Aborting");
process.exit(-1);
});
console.log(keycloakVersion);
return { keycloakVersion };
}

View File

@ -1,73 +0,0 @@
export type NpmModuleVersion = {
major: number;
minor: number;
patch: number;
betaPreRelease?: number;
};
export namespace NpmModuleVersion {
export function parse(versionStr: string): NpmModuleVersion {
const match = versionStr.match(/^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-beta.([0-9]+))?/);
if (!match) {
throw new Error(`${versionStr} is not a valid NPM version`);
}
return {
"major": parseInt(match[1]),
"minor": parseInt(match[2]),
"patch": parseInt(match[3]),
...(() => {
const str = match[4];
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
})()
};
}
export function stringify(v: NpmModuleVersion) {
return `${v.major}.${v.minor}.${v.patch}${v.betaPreRelease === undefined ? "" : `-beta.${v.betaPreRelease}`}`;
}
/**
*
* v1 < v2 => -1
* v1 === v2 => 0
* v1 > v2 => 1
*
*/
export function compare(v1: NpmModuleVersion, v2: NpmModuleVersion): -1 | 0 | 1 {
const sign = (diff: number): -1 | 0 | 1 => (diff === 0 ? 0 : diff < 0 ? -1 : 1);
const noUndefined = (n: number | undefined) => n ?? Infinity;
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (noUndefined(v1[level]) !== noUndefined(v2[level])) {
return sign(noUndefined(v1[level]) - noUndefined(v2[level]));
}
}
return 0;
}
/*
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("3.0.0-beta.4")) === -1 )
console.log(compare(parse("3.0.0-beta.3"), parse("4.0.0")) === -1 )
*/
export function bumpType(params: { versionBehindStr: string; versionAheadStr: string }): "major" | "minor" | "patch" | "betaPreRelease" | "same" {
const versionAhead = parse(params.versionAheadStr);
const versionBehind = parse(params.versionBehindStr);
if (compare(versionBehind, versionAhead) === 1) {
throw new Error(`Version regression ${versionBehind} -> ${versionAhead}`);
}
for (const level of ["major", "minor", "patch", "betaPreRelease"] as const) {
if (versionBehind[level] !== versionAhead[level]) {
return level;
}
}
return "same";
}
}

View File

@ -1,15 +0,0 @@
import parseArgv from "minimist";
export type CliOptions = {
isSilent: boolean;
hasExternalAssets: boolean;
};
export const getCliOptions = (processArgv: string[]): CliOptions => {
const argv = parseArgv(processArgv);
return {
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
};

View File

@ -1,81 +0,0 @@
import { basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process";
import * as fs from "fs";
import { transformCodebase } from "./transformCodebase";
import { rm, rm_rf } from "./rm";
import * as crypto from "crypto";
/** assert url ends with .zip */
export function downloadAndUnzip(params: {
isSilent: boolean;
url: string;
destDirPath: string;
pathOfDirToExtractInArchive?: string;
cacheDirPath: string;
}) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const extractDirPath = pathJoin(
cacheDirPath,
`_${crypto.createHash("sha256").update(JSON.stringify({ url, pathOfDirToExtractInArchive })).digest("hex").substring(0, 15)}`
);
fs.mkdirSync(cacheDirPath, { "recursive": true });
const { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath } = (() => {
const filePath = pathJoin(cacheDirPath, "isSuccessByExtractDirPath.json");
type IsSuccessByExtractDirPath = Record<string, boolean | undefined>;
function readIsSuccessByExtractDirPath(): IsSuccessByExtractDirPath {
if (!fs.existsSync(filePath)) {
return {};
}
return JSON.parse(fs.readFileSync(filePath).toString("utf8"));
}
function writeIsSuccessByExtractDirPath(isSuccessByExtractDirPath: IsSuccessByExtractDirPath): void {
fs.writeFileSync(filePath, Buffer.from(JSON.stringify(isSuccessByExtractDirPath, null, 2), "utf8"));
}
return { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath };
})();
downloadAndUnzip: {
const isSuccessByExtractDirPath = readIsSuccessByExtractDirPath();
if (isSuccessByExtractDirPath[extractDirPath]) {
break downloadAndUnzip;
}
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: false
});
rm_rf(extractDirPath);
fs.mkdirSync(extractDirPath);
const zipFileBasename = pathBasename(url);
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath
});
rm(zipFileBasename, { "cwd": extractDirPath });
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: true
});
}
transformCodebase({
"srcDirPath": pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive),
destDirPath
});
}

View File

@ -1,19 +0,0 @@
import * as fs from "fs";
import * as path from "path";
function getProjectRootRec(dirPath: string): string {
if (fs.existsSync(path.join(dirPath, "package.json"))) {
return dirPath;
}
return getProjectRootRec(path.join(dirPath, ".."));
}
let result: string | undefined = undefined;
export function getProjectRoot(): string {
if (result !== undefined) {
return result;
}
return (result = getProjectRootRec(__dirname));
}

View File

@ -1,10 +0,0 @@
import { getProjectRoot } from "./getProjectRoot";
import { join as pathJoin } from "path";
import * as child_process from "child_process";
import * as fs from "fs";
Object.entries<string>(JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["bin"]).forEach(([, scriptPath]) =>
child_process.execSync(`chmod +x ${scriptPath}`, {
"cwd": getProjectRoot()
})
);

View File

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

View File

@ -1,27 +0,0 @@
type LoggerOpts = {
force?: boolean;
};
type Logger = {
log: (message: string, opts?: LoggerOpts) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
return {
log: (message, { force } = {}) => {
if (isSilent && !force) {
return;
}
console.log(message);
},
warn: message => {
console.warn(message);
},
error: message => {
console.error(message);
}
};
};

View File

@ -1,40 +0,0 @@
import { listTagsFactory } from "./listTags";
import type { Octokit } from "@octokit/rest";
import { NpmModuleVersion } from "../NpmModuleVersion";
export function getLatestsSemVersionedTagFactory(params: { octokit: Octokit }) {
const { octokit } = params;
async function getLatestsSemVersionedTag(params: { owner: string; repo: string; doIgnoreBeta: boolean; count: number }): Promise<
{
tag: string;
version: NpmModuleVersion;
}[]
> {
const { owner, repo, doIgnoreBeta, count } = params;
const semVersionedTags: { tag: string; version: NpmModuleVersion }[] = [];
const { listTags } = listTagsFactory({ octokit });
for await (const tag of listTags({ owner, repo })) {
let version: NpmModuleVersion;
try {
version = NpmModuleVersion.parse(tag.replace(/^[vV]?/, ""));
} catch {
continue;
}
if (doIgnoreBeta && version.betaPreRelease !== undefined) {
continue;
}
semVersionedTags.push({ tag, version });
}
return semVersionedTags.sort(({ version: vX }, { version: vY }) => NpmModuleVersion.compare(vY, vX)).slice(0, count);
}
return { getLatestsSemVersionedTag };
}

View File

@ -1,49 +0,0 @@
import type { Octokit } from "@octokit/rest";
const per_page = 99;
export function listTagsFactory(params: { octokit: Octokit }) {
const { octokit } = params;
const octokit_repo_listTags = async (params: { owner: string; repo: string; per_page: number; page: number }) => {
return octokit.repos.listTags(params);
};
async function* listTags(params: { owner: string; repo: string }): AsyncGenerator<string> {
const { owner, repo } = params;
let page = 1;
while (true) {
const resp = await octokit_repo_listTags({
owner,
repo,
per_page,
"page": page++
});
for (const branch of resp.data.map(({ name }) => name)) {
yield branch;
}
if (resp.data.length < 99) {
break;
}
}
}
/** Returns the same "latest" tag as deno.land/x, not actually the latest though */
async function getLatestTag(params: { owner: string; repo: string }): Promise<string | undefined> {
const { owner, repo } = params;
const itRes = await listTags({ owner, repo }).next();
if (itRes.done) {
return undefined;
}
return itRes.value;
}
return { listTags, getLatestTag };
}

View File

@ -1,6 +0,0 @@
export function pathJoin(...path: string[]): string {
return path
.map((part, i) => (i === 0 ? part : part.replace(/^\/+/, "")))
.map((part, i) => (i === path.length - 1 ? part : part.replace(/\/+$/, "")))
.join("/");
}

View File

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

View File

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

View File

@ -1,11 +0,0 @@
{
"extends": "../../tsproject.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES5",
"esModuleInterop": true,
"lib": ["es2015", "DOM", "ES2019.Object"],
"outDir": "../../dist/bin",
"rootDir": "."
}
}

View File

@ -1,43 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type ErrorProps = KcProps & {
kcContext: KcContextBase.Error;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Error = memo((props: ErrorProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { message, client } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
<div id="kc-error-message">
<p className="instruction">{message.summary}</p>
{client !== undefined && client.baseUrl !== undefined && (
<p>
<a id="backToApplication" href={client.baseUrl}>
{msg("backToApplication")}
</a>
</p>
)}
</div>
}
/>
);
});
export default Error;

View File

@ -1,60 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
export type IdpReviewUserProfileProps = KcProps & {
kcContext: KcContextBase.IdpReviewUserProfile;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const IdpReviewUserProfile = memo((props: IdpReviewUserProfileProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("loginIdpReviewProfileTitle")}
formNode={
<form id="kc-idp-review-profile-form" className={cx(kcProps.kcFormClass)} action={url.loginAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
<div className={cx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(kcProps.kcFormOptionsClass)}>
<div className={cx(kcProps.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormButtonsClass)}>
<input
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
});
export default IdpReviewUserProfile;

View File

@ -1,60 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type InfoProps = KcProps & {
kcContext: KcContextBase.Info;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Info = memo((props: InfoProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined);
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
formNode={
<div id="kc-info-message">
<p className="instruction">
{message.summary}
{requiredActions !== undefined && (
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
)}
</p>
{!skipLink && pageRedirectUri !== undefined ? (
<p>
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
</p>
) : actionUri !== undefined ? (
<p>
<a href={actionUri}>{msg("proceedWithAction")}</a>
</p>
) : (
client.baseUrl !== undefined && (
<p>
<a href={client.baseUrl}>{msg("backToApplication")}</a>
</p>
)
)}
</div>
}
/>
);
});
export default Info;

View File

@ -1,109 +0,0 @@
import React, { lazy, memo, Suspense } from "react";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps";
import { __unsafe_useI18n as useI18n } from "../i18n";
import type { I18n } from "../i18n";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
const Login = lazy(() => import("./Login"));
const Register = lazy(() => import("./Register"));
const RegisterUserProfile = lazy(() => import("./RegisterUserProfile"));
const Info = lazy(() => import("./Info"));
const Error = lazy(() => import("./Error"));
const LoginResetPassword = lazy(() => import("./LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail"));
const Terms = lazy(() => import("./Terms"));
const LoginOtp = lazy(() => import("./LoginOtp"));
const LoginPassword = lazy(() => import("./LoginPassword"));
const LoginUsername = lazy(() => import("./LoginUsername"));
const WebauthnAuthenticate = lazy(() => import("./WebauthnAuthenticate"));
const LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
export type KcAppProps = KcProps & {
kcContext: KcContextBase;
i18n?: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const KcApp = memo((props_: KcAppProps) => {
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
"extraMessages": {},
"doSkip": userProvidedI18n !== undefined
});
return userProvidedI18n ?? i18n;
})();
if (i18n === null) {
return null;
}
const commonProps = { i18n, Template, ...kcProps };
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login {...{ kcContext, ...commonProps }} />;
case "register.ftl":
return <Register {...{ kcContext, ...commonProps }} />;
case "register-user-profile.ftl":
return <RegisterUserProfile {...{ kcContext, ...commonProps }} />;
case "info.ftl":
return <Info {...{ kcContext, ...commonProps }} />;
case "error.ftl":
return <Error {...{ kcContext, ...commonProps }} />;
case "login-reset-password.ftl":
return <LoginResetPassword {...{ kcContext, ...commonProps }} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, ...commonProps }} />;
case "terms.ftl":
return <Terms {...{ kcContext, ...commonProps }} />;
case "login-otp.ftl":
return <LoginOtp {...{ kcContext, ...commonProps }} />;
case "login-username.ftl":
return <LoginUsername {...{ kcContext, ...commonProps }} />;
case "login-password.ftl":
return <LoginPassword {...{ kcContext, ...commonProps }} />;
case "webauthn-authenticate.ftl":
return <WebauthnAuthenticate {...{ kcContext, ...commonProps }} />;
case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, ...commonProps }} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, ...commonProps }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...commonProps }} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, ...commonProps }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...commonProps }} />;
case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, ...commonProps }} />;
case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, ...commonProps }} />;
case "update-user-profile.ftl":
return <UpdateUserProfile {...{ kcContext, ...commonProps }} />;
case "idp-review-user-profile.ftl":
return <IdpReviewUserProfile {...{ kcContext, ...commonProps }} />;
}
})()}
</Suspense>
);
});
export default KcApp;

View File

@ -1,213 +0,0 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { assert } from "tsafe/assert";
/** 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 KcTemplateClassKey =
| "stylesCommon"
| "styles"
| "scripts"
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
| "kcHeaderWrapperClass"
| "kcFormCardClass"
| "kcFormCardAccountClass"
| "kcFormHeaderClass"
| "kcLocaleWrapperClass"
| "kcContentWrapperClass"
| "kcLabelWrapperClass"
| "kcFormGroupClass"
| "kcResetFlowIcon"
| "kcFeedbackSuccessIcon"
| "kcFeedbackWarningIcon"
| "kcFeedbackErrorIcon"
| "kcFeedbackInfoIcon"
| "kcFormSocialAccountContentClass"
| "kcFormSocialAccountClass"
| "kcSignUpClass"
| "kcInfoAreaWrapperClass";
export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>;
export const defaultKcTemplateProps = {
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"scripts": [],
"kcHtmlClass": ["login-pf"],
"kcLoginClass": ["login-pf-page"],
"kcContentWrapperClass": ["row"],
"kcHeaderClass": ["login-pf-page-header"],
"kcHeaderWrapperClass": [],
"kcFormCardClass": ["card-pf"],
"kcFormCardAccountClass": ["login-pf-accounts"],
"kcFormSocialAccountClass": ["login-pf-social-section"],
"kcFormSocialAccountContentClass": ["col-xs-12", "col-sm-6"],
"kcFormHeaderClass": ["login-pf-header"],
"kcLocaleWrapperClass": [],
"kcFeedbackErrorIcon": ["pficon", "pficon-error-circle-o"],
"kcFeedbackWarningIcon": ["pficon", "pficon-warning-triangle-o"],
"kcFeedbackSuccessIcon": ["pficon", "pficon-ok"],
"kcFeedbackInfoIcon": ["pficon", "pficon-info"],
"kcResetFlowIcon": ["pficon", "pficon-arrow fa-2x"],
"kcFormGroupClass": ["form-group"],
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcSignUpClass": ["login-pf-signup"],
"kcInfoAreaWrapperClass": []
} as const;
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
/** Tu use if you don't want any default */
export const allClearKcTemplateProps = allPropertiesValuesToUndefined(defaultKcTemplateProps);
assert<typeof allClearKcTemplateProps extends KcTemplateProps ? true : false>();
export type KcProps = KcPropsGeneric<
| KcTemplateClassKey
| "kcLogoLink"
| "kcLogoClass"
| "kcContainerClass"
| "kcContentClass"
| "kcFeedbackAreaClass"
| "kcLocaleClass"
| "kcAlertIconClasserror"
| "kcFormAreaClass"
| "kcFormSocialAccountListClass"
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass"
| "kcInputWrapperClass"
| "kcFormOptionsClass"
| "kcFormButtonsClass"
| "kcFormSettingClass"
| "kcTextareaClass"
| "kcInfoAreaClass"
| "kcFormGroupHeader"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonDefaultClass"
| "kcButtonLargeClass"
| "kcButtonBlockClass"
| "kcInputLargeClass"
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass"
| "kcSelectOTPListClass"
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass"
>;
export const defaultKcProps = {
...defaultKcTemplateProps,
"kcLogoLink": "http://www.keycloak.org",
"kcLogoClass": "login-pf-brand",
"kcContainerClass": "container-fluid",
"kcContentClass": ["col-sm-8", "col-sm-offset-2", "col-md-6", "col-md-offset-3", "col-lg-6", "col-lg-offset-3"],
"kcFeedbackAreaClass": ["col-md-12"],
"kcLocaleClass": ["col-xs-12", "col-sm-1"],
"kcAlertIconClasserror": ["pficon", "pficon-error-circle-o"],
"kcFormAreaClass": ["col-sm-10", "col-sm-offset-1", "col-md-8", "col-md-offset-2", "col-lg-8", "col-lg-offset-2"],
"kcFormSocialAccountListClass": ["login-pf-social", "list-unstyled", "login-pf-social-all"],
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
"kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"],
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
"kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcFormSettingClass": ["login-pf-settings"],
"kcTextareaClass": ["form-control"],
"kcInfoAreaClass": ["col-xs-12", "col-sm-4", "col-md-4", "col-lg-5", "details"],
// user-profile grouping
"kcFormGroupHeader": ["pf-c-form__group"],
// css classes for form buttons main class used for all buttons
"kcButtonClass": ["btn"],
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
"kcButtonPrimaryClass": ["btn-primary"],
"kcButtonDefaultClass": ["btn-default"],
// classes defining size of the button
"kcButtonLargeClass": ["btn-lg"],
"kcButtonBlockClass": ["btn-block"],
// css classes for input
"kcInputLargeClass": ["input-lg"],
// css classes for form accessability
"kcSrOnlyClass": ["sr-only"],
// css classes for select-authenticator form
"kcSelectAuthListClass": ["list-group", "list-view-pf"],
"kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"],
"kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
"kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
"kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
"kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
"kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
"kcSelectAuthListItemDescriptionClass": ["list-view-pf-description"],
"kcSelectAuthListItemHeadingClass": ["list-group-item-heading"],
"kcSelectAuthListItemHelpTextClass": ["list-group-item-text"],
// css classes for the authenticators
"kcAuthenticatorDefaultClass": ["fa", "list-view-pf-icon-lg"],
"kcAuthenticatorPasswordClass": ["fa", "fa-unlock list-view-pf-icon-lg"],
"kcAuthenticatorOTPClass": ["fa", "fa-mobile", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
"kcAuthenticatorWebAuthnPasswordlessClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
//css classes for the OTP Login Form
"kcSelectOTPListClass": ["card-pf", "card-pf-view", "card-pf-view-select", "card-pf-view-single-select"],
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
"kcFormOptionsWrapperClass": []
} as const;
assert<typeof defaultKcProps extends KcProps ? true : false>();
/** Tu use if you don't want any default */
export const allClearKcProps = allPropertiesValuesToUndefined(defaultKcProps);
assert<typeof allClearKcProps extends KcProps ? true : false>();

View File

@ -1,206 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
export type LoginProps = KcProps & {
kcContext: KcContextBase.Login;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const Login = memo((props: LoginProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
//NOTE: Even if we login with email Keycloak expect username and password in
//the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
formElement.submit();
});
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
<div
id="kc-form-wrapper"
className={cx(
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
{(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return (
<>
<label htmlFor={autoCompleteHelper} className={cx(kcProps.kcLabelClass)}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={cx(kcProps.kcInputClass)}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off"
})}
/>
</>
);
})()}
</div>
<div className={cx(kcProps.kcFormGroupClass)}>
<label htmlFor="password" className={cx(kcProps.kcLabelClass)}>
{msg("password")}
</label>
<input
tabIndex={2}
id="password"
className={cx(kcProps.kcInputClass)}
name="password"
type="password"
autoComplete="off"
/>
</div>
<div className={cx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options">
{realm.rememberMe && !usernameEditDisabled && (
<div className="checkbox">
<label>
<input
tabIndex={3}
id="rememberMe"
name="rememberMe"
type="checkbox"
{...(login.rememberMe
? {
"checked": true
}
: {})}
/>
{msg("rememberMe")}
</label>
</div>
)}
</div>
<div className={cx(kcProps.kcFormOptionsWrapperClass)}>
{realm.resetPasswordAllowed && (
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
{msg("doForgotPassword")}
</a>
</span>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
type="hidden"
id="id-hidden-input"
name="credentialId"
{...(auth?.selectedCredential !== undefined
? {
"value": auth.selectedCredential
}
: {})}
/>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
)}
</div>
{realm.password && social.providers !== undefined && (
<div id="kc-social-providers" className={cx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
<ul
className={cx(
kcProps.kcFormSocialAccountListClass,
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
)}
>
{social.providers.map(p => (
<li key={p.providerId} className={cx(kcProps.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
<span>{p.displayName}</span>
</a>
</li>
))}
</ul>
</div>
)}
</div>
}
infoNode={
realm.password &&
realm.registrationAllowed &&
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
}
/>
);
});
export default Login;

View File

@ -1,195 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type LoginConfigTotpProps = KcProps & {
kcContext: KcContextBase.LoginConfigTotp;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginConfigTotp = memo((props: LoginConfigTotpProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
};
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("loginTotpTitle")}
formNode={
<>
<ol id="kc-totp-settings">
<li>
<p>{msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.policy.supportedApplications.map(app => (
<li>{app}</li>
))}
</ul>
</li>
{mode && mode == "manual" ? (
<>
<li>
<p>{msg("loginTotpManualStep2")}</p>
<p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
</p>
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("loginTotpScanBarcode")}
</a>
</p>
</li>
<li>
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (
<li>
<p>{msg("loginTotpStep2")}</p>
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" />
<br />
<p>
<a href={totp.manualUrl} id="mode-manual">
{msg("loginTotpUnableToScan")}
</a>
</p>
</li>
)}
<li>
<p>{msg("loginTotpStep3")}</p>
<p>{msg("loginTotpStep3DeviceName")}</p>
</li>
</ol>
<form action={url.loginAction} className={cx(kcProps.kcFormClass)} id="kc-totp-settings-form" method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<div className={cx(kcProps.kcInputWrapperClass)}>
<label htmlFor="totp" className={cx(kcProps.kcLabelClass)}>
{msg("authenticatorCode")}
</label>{" "}
<span className="required">*</span>
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="totp"
name="totp"
autoComplete="off"
className={cx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("totp")}
/>
{messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={cx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("totp")}
</span>
)}
</div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div>
<div className={cx(kcProps.kcFormGroupClass)}>
<div className={cx(kcProps.kcInputWrapperClass)}>
<label htmlFor="userLabel" className={cx(kcProps.kcLabelClass)}>
{msg("loginTotpDeviceName")}
</label>{" "}
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={cx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={cx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
)}
</div>
</div>
{isAppInitiatedAction ? (
<>
<input
type="submit"
className={cx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonLargeClass,
kcProps.kcButtonLargeClass
)}
id="cancelTOTPBtn"
name="cancel-aia"
value="true"
>
${msg("doCancel")}
</button>
</>
) : (
<input
type="submit"
className={cx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
)}
</form>
</>
}
/>
);
});
export default LoginConfigTotp;

View File

@ -1,67 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type LoginIdpLinkConfirmProps = KcProps & {
kcContext: KcContextBase.LoginIdpLinkConfirm;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginIdpLinkConfirm = memo((props: LoginIdpLinkConfirmProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, idpAlias } = kcContext;
const { msg } = i18n;
const { cx } = useCssAndCx();
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("confirmLinkIdpTitle")}
formNode={
<form id="kc-register-form" action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction"
id="updateProfile"
value="updateProfile"
>
{msg("confirmLinkIdpReviewProfile")}
</button>
<button
type="submit"
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction"
id="linkAccount"
value="linkAccount"
>
{msg("confirmLinkIdpContinue", idpAlias)}
</button>
</div>
</form>
}
/>
);
});
export default LoginIdpLinkConfirm;

View File

@ -1,43 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LoginIdpLinkEmailProps = KcProps & {
kcContext: KcContextBase.LoginIdpLinkEmail;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginIdpLinkEmail = memo((props: LoginIdpLinkEmailProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={
<>
<p id="instruction1" className="instruction">
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
</p>
<p id="instruction2" className="instruction">
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
</p>
<p id="instruction3" className="instruction">
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
</p>
</>
}
/>
);
});
export default LoginIdpLinkEmail;

View File

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

View File

@ -1,47 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LoginPageExpired = KcProps & {
kcContext: KcContextBase.LoginPageExpired;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginPageExpired = memo((props: LoginPageExpired) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("pageExpiredTitle")}
formNode={
<>
<p id="instruction1" className="instruction">
{msg("pageExpiredMsg1")}
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
{msg("doClickHere")}
</a>{" "}
.<br />
{msg("pageExpiredMsg2")}{" "}
<a id="loginContinueLink" href={url.loginAction}>
{msg("doClickHere")}
</a>{" "}
.
</p>
</>
}
/>
);
});
export default LoginPageExpired;

View File

@ -1,99 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
export type LoginPasswordProps = KcProps & {
kcContext: KcContextBase.LoginPassword;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginPassword = memo((props: LoginPasswordProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { realm, url, login } = kcContext;
const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
formElement.submit();
});
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form">
<div id="kc-form-wrapper">
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
<hr />
<label htmlFor="password" className={cx(kcProps.kcLabelClass)}>
{msg("password")}
</label>
<input
tabIndex={2}
id="password"
className={cx(kcProps.kcInputClass)}
name="password"
type="password"
autoFocus={true}
autoComplete="on"
defaultValue={login.password ?? ""}
/>
</div>
<div className={cx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options" />
<div className={cx(kcProps.kcFormOptionsWrapperClass)}>
{realm.resetPasswordAllowed && (
<span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
{msg("doForgotPassword")}
</a>
</span>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
</div>
</div>
}
/>
);
});
export default LoginPassword;

View File

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

View File

@ -1,128 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type LoginUpdatePasswordProps = KcProps & {
kcContext: KcContextBase.LoginUpdatePassword;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginUpdatePassword = memo((props: LoginUpdatePasswordProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("updatePasswordTitle")}
formNode={
<form id="kc-passwd-update-form" className={cx(kcProps.kcFormClass)} action={url.loginAction} method="post">
<input
type="text"
id="username"
name="username"
value={username}
readOnly={true}
autoComplete="username"
style={{ display: "none" }}
/>
<input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
<div className={cx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}>
<div className={cx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password-new" className={cx(kcProps.kcLabelClass)}>
{msg("passwordNew")}
</label>
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="password"
id="password-new"
name="password-new"
autoFocus
autoComplete="new-password"
className={cx(kcProps.kcInputClass)}
/>
</div>
</div>
<div className={cx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass))}>
<div className={cx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(kcProps.kcLabelClass)}>
{msg("passwordConfirm")}
</label>
</div>
<div className={cx(kcProps.kcInputWrapperClass)}>
<input
type="password"
id="password-confirm"
name="password-confirm"
autoComplete="new-password"
className={cx(kcProps.kcInputClass)}
/>
</div>
</div>
<div className={cx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(kcProps.kcFormOptionsClass)}>
<div className={cx(kcProps.kcFormOptionsWrapperClass)}>
{isAppInitiatedAction && (
<div className="checkbox">
<label>
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
{msgStr("logoutOtherSessions")}
</label>
</div>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormButtonsClass)}>
{isAppInitiatedAction ? (
<>
<input
className={cx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
<button
className={cx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
)}
</div>
</div>
</form>
}
/>
);
});
export default LoginUpdatePassword;

View File

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

View File

@ -1,171 +0,0 @@
import React, { useState, memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
export type LoginUsernameProps = KcProps & {
kcContext: KcContextBase.LoginUsername;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginUsername = memo((props: LoginUsernameProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault();
setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement;
//NOTE: Even if we login with email Keycloak expect username and password in
//the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
formElement.submit();
});
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
formNode={
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
<div
id="kc-form-wrapper"
className={cx(
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
<div className={cx(kcProps.kcFormGroupClass)}>
{!usernameHidden &&
(() => {
const label = !realm.loginWithEmailAllowed
? "username"
: realm.registrationEmailAsUsername
? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return (
<>
<label htmlFor={autoCompleteHelper} className={cx(kcProps.kcLabelClass)}>
{msg(label)}
</label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={cx(kcProps.kcInputClass)}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
autoFocus={true}
autoComplete="off"
/>
</>
);
})()}
</div>
<div className={cx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options">
{realm.rememberMe && !usernameHidden && (
<div className="checkbox">
<label>
<input
tabIndex={3}
id="rememberMe"
name="rememberMe"
type="checkbox"
{...(login.rememberMe
? {
"checked": true
}
: {})}
/>
{msg("rememberMe")}
</label>
</div>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
)}
</div>
{realm.password && social.providers !== undefined && (
<div id="kc-social-providers" className={cx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
<ul
className={cx(
kcProps.kcFormSocialAccountListClass,
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
)}
>
{social.providers.map(p => (
<li key={p.providerId} className={cx(kcProps.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
<span>{p.displayName}</span>
</a>
</li>
))}
</ul>
</div>
)}
</div>
}
infoNode={
realm.password &&
realm.registrationAllowed &&
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
}
/>
);
});
export default LoginUsername;

View File

@ -1,43 +0,0 @@
import React, { memo } from "react";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LoginVerifyEmailProps = KcProps & {
kcContext: KcContextBase.LoginVerifyEmail;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LoginVerifyEmail = memo((props: LoginVerifyEmailProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { msg } = i18n;
const { url, user } = kcContext;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
<>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<br />
<a href={url.loginAction}>{msg("doClickHere")}</a>
&nbsp;
{msg("emailVerifyInstruction3")}
</p>
</>
}
/>
);
});
export default LoginVerifyEmail;

View File

@ -1,71 +0,0 @@
import React, { memo } from "react";
import { useCssAndCx } from "../tools/useCssAndCx";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
export type LogoutConfirmProps = KcProps & {
kcContext: KcContextBase.LogoutConfirm;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const LogoutConfirm = memo((props: LogoutConfirmProps) => {
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
const { url, client, logoutConfirm } = kcContext;
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={false}
headerNode={msg("logoutConfirmTitle")}
formNode={
<>
<div id="kc-logout-confirm" className="content-area">
<p className="instruction">{msg("logoutConfirmHeader")}</p>
<form className="form-actions" action={url.logoutConfirmAction} method="POST">
<input type="hidden" name="session_code" value={logoutConfirm.code} />
<div className={cx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options">
<div className={cx(kcProps.kcFormOptionsWrapperClass)}></div>
</div>
<div id="kc-form-buttons" className={cx(kcProps.kcFormGroupClass)}>
<input
tabIndex={4}
className={cx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="confirmLogout"
id="kc-logout"
type="submit"
value={msgStr("doLogout")}
/>
</div>
</div>
</form>
<div id="kc-info-message">
{!logoutConfirm.skipLink && client.baseUrl && (
<p>
<a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
</p>
)}
</div>
</div>
</>
}
/>
);
});
export default LogoutConfirm;

Some files were not shown because too many files have changed in this diff Show More