Migrate to keycloakify 10
This commit is contained in:
parent
081c7d4150
commit
030836d534
|
@ -1,30 +0,0 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
/Dockerfile
|
||||
/node_modules
|
||||
/.github
|
||||
/.vscode
|
||||
/docs
|
||||
/build
|
49
.github/workflows/ci.yaml
vendored
49
.github/workflows/ci.yaml
vendored
|
@ -28,7 +28,7 @@ jobs:
|
|||
to_version: ${{ steps.step1.outputs.to_version }}
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
- uses: garronej/ts-ci@v2.1.2
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
|
@ -60,50 +60,3 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- check_if_version_upgraded
|
||||
- create_github_release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: docker/setup-qemu-action@v1
|
||||
- uses: docker/setup-buildx-action@v1
|
||||
- uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Computing Docker image tags
|
||||
id: step1
|
||||
env:
|
||||
IS_UPGRADED_VERSION: ${{ needs.check_if_version_upgraded.outputs.is_upgraded_version }}
|
||||
TO_VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||
run: |
|
||||
OUT=$GITHUB_REPOSITORY:$TO_VERSION,$GITHUB_REPOSITORY:latest
|
||||
OUT=$(echo "$OUT" | awk '{print tolower($0)}')
|
||||
echo ::set-output name=docker_tags::$OUT
|
||||
- uses: docker/build-push-action@v2
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
tags: ${{ steps.step1.outputs.docker_tags }}
|
||||
|
||||
github_pages:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- create_github_release
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build
|
||||
# We tell GitHub pages that our package.json["homepage"] field is our domain name.
|
||||
# If you wish to use the default GitHub pages domain name, like https://<username>.github.io/<repo>,
|
||||
# you'll have to use base: "/repo/" in your vite.config.ts.
|
||||
- run: echo $(node -e 'console.log(require("url").parse(require("./package.json").homepage).host)') > dist/CNAME
|
||||
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/setup-node@v3.6.0
|
||||
- run: npx -y -p gh-pages@3.0.0 gh-pages -u "github-actions-bot <actions@github.com>" -d dist
|
||||
|
||||
|
|
13
Dockerfile
13
Dockerfile
|
@ -1,13 +0,0 @@
|
|||
# build environment
|
||||
FROM node:18-alpine as build
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN yarn build
|
||||
|
||||
# production environment
|
||||
FROM nginx:stable-alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
CMD nginx -g 'daemon off;'
|
65
index.html
65
index.html
|
@ -4,70 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<!--
|
||||
Notice the use of %BASE_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%BASE_URL%favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
-->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%BASE_URL%favicon-32x32.png">
|
||||
|
||||
<title>Keycloakify starter</title>
|
||||
|
||||
<!-- NOTE: Here we import the WorkSans font as an example of how to import self hosted custom fonts. Don't keep it in your actual theme!
|
||||
SEE: https://docs.keycloakify.dev/limitations#self-hosted-fonts
|
||||
Don't forget to import your custom fonts in Storybook as well: https://github.com/keycloakify/keycloakify-starter/blob/bb019e66fb09166cb9af1e24e230994f59daa420/src/keycloak-theme/login/createPageStory.tsx#L21
|
||||
-->
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<style>
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
/*400*/
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
/*700*/
|
||||
font-display: swap;
|
||||
src: url("%BASE_URL%fonts/WorkSans/worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta name="keycloakify-ignore-start">
|
||||
<script>console.log("This is logged Only in the main app, stripped out in the theme")</script>
|
||||
<meta name="keycloakify-ignore-end">
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
35
nginx.conf
35
nginx.conf
|
@ -1,35 +0,0 @@
|
|||
server {
|
||||
listen 80;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_proxied expired no-cache no-store private auth;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
# Any route containing a file extension (e.g. /devicesfile.js)
|
||||
location ~ ^.+\..+$ {
|
||||
try_files $uri =404;
|
||||
|
||||
location ~* \.(?:html|json|txt)$ {
|
||||
expires -1;
|
||||
}
|
||||
|
||||
# Vite generates filenames with hashes so we can
|
||||
# tell the browser to keep in cache the resources.
|
||||
location ~* \.(?:css|js|md|woff2?|eot|ttf|xml)$ {
|
||||
expires 1y;
|
||||
access_log off;
|
||||
add_header Cache-Control "public";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
16
package.json
16
package.json
|
@ -1,8 +1,7 @@
|
|||
{
|
||||
"name": "keycloakify-starter",
|
||||
"homepage": "https://starter.keycloakify.dev",
|
||||
"version": "6.1.10",
|
||||
"description": "A starter/demo project for keycloakify",
|
||||
"description": "Starter for Keycloakify 10",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||
|
@ -15,18 +14,12 @@
|
|||
"storybook": "storybook dev -p 6006",
|
||||
"build-storybook": "storybook build"
|
||||
},
|
||||
"author": "u/garronej",
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"evt": "^2.5.7",
|
||||
"keycloakify": "^9.6.6",
|
||||
"oidc-spa": "^4.6.2",
|
||||
"powerhooks": "^1.0.8",
|
||||
"keycloakify": "10.0.0-rc.31",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tsafe": "^1.6.6",
|
||||
"zod": "^3.22.4"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-essentials": "^8.0.2",
|
||||
|
@ -48,8 +41,7 @@
|
|||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"storybook": "^8.0.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.8",
|
||||
"vite-plugin-commonjs": "^0.10.1"
|
||||
"vite": "^5.0.8"
|
||||
},
|
||||
"_comment": "See https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092",
|
||||
"resolutions": {
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
|
||||
/*
|
||||
This file is only meant to be used by Storybook
|
||||
*/
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: normal; /*400*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("./worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("./worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Work Sans";
|
||||
font-style: normal;
|
||||
font-weight: bold; /*700*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
|
@ -1,7 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
<script>
|
||||
parent.postMessage(location.href, location.origin);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,58 +0,0 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
background-color: #242424;
|
||||
}
|
||||
|
||||
|
||||
.App {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.App-payload {
|
||||
text-align: center;
|
||||
margin-bottom: 4rem;
|
||||
color: white;
|
||||
/* link color */
|
||||
a {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.App-logo-wrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 15vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo.rotate {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import "./App.css";
|
||||
import reactSvgUrl from "./assets/react.svg";
|
||||
import viteSvgUrl from "./assets/vite.svg";
|
||||
import { OidcProvider, useOidc, getKeycloakAccountUrl } from "./oidc";
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
// To integrate Keycloak to your React App you have many options such as:
|
||||
// - https://www.npmjs.com/package/keycloak-js
|
||||
// - https://github.com/authts/oidc-client-ts
|
||||
// - https://github.com/authts/react-oidc-context
|
||||
// In this starter we use oidc-spa instead
|
||||
// It's a new library made by us, the Keycloakify team.
|
||||
// Check it out: https://github.com/keycloakify/oidc-spa
|
||||
<OidcProvider>
|
||||
<ContextualizedApp />
|
||||
</OidcProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextualizedApp() {
|
||||
|
||||
const { isUserLoggedIn, login, logout, oidcTokens } = useOidc();
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div>
|
||||
<div className="App-payload">
|
||||
{isUserLoggedIn ?
|
||||
(
|
||||
<>
|
||||
|
||||
<h1>Hello {oidcTokens.decodedIdToken.name} !</h1>
|
||||
<a
|
||||
href={getKeycloakAccountUrl({ locale: "en" })}
|
||||
>
|
||||
Link to your Keycloak account
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => logout({ redirectTo: "home" })}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<Jwt />
|
||||
</>
|
||||
)
|
||||
:
|
||||
(
|
||||
<button
|
||||
onClick={() => login({
|
||||
doesCurrentHrefRequiresAuth: false,
|
||||
//extraQueryParams: { kc_idp_hint: "google" }
|
||||
})}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className="App-logo-wrapper">
|
||||
<img src={reactSvgUrl} className="App-logo rotate" alt="logo" />
|
||||
|
||||
<img src={viteSvgUrl} className="App-logo" alt="logo" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
function Jwt() {
|
||||
|
||||
const { oidcTokens } = useOidc({
|
||||
assertUserLoggedIn: true
|
||||
});
|
||||
|
||||
// NOTE: Use `Bearer ${oidcTokens.accessToken}` as the Authorization header to call your backend
|
||||
// Here we just display the decoded id token
|
||||
|
||||
return (
|
||||
<pre style={{ textAlign: "left" }}>
|
||||
{JSON.stringify(oidcTokens.decodedIdToken, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4 KiB |
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,4 +0,0 @@
|
|||
import App from "./App";
|
||||
export * from "./App";
|
||||
|
||||
export default App;
|
|
@ -1,60 +0,0 @@
|
|||
// See documentation of oidc-spa for more details:
|
||||
// https://docs.oidc-spa.dev
|
||||
|
||||
import { createReactOidc } from "oidc-spa/react";
|
||||
import { z } from "zod";
|
||||
|
||||
//On older Keycloak version you need the /auth (e.g: http://localhost:8080/auth)
|
||||
//On newer version you must remove it (e.g: http://localhost:8080 )
|
||||
const keycloakUrl = "https://cloud-iam.keycloakify.dev/";
|
||||
const keycloakRealm = "keycloakify";
|
||||
const keycloakClientId= "starter";
|
||||
|
||||
export const { OidcProvider, useOidc } = createReactOidc({
|
||||
issuerUri: `${keycloakUrl}/realms/${keycloakRealm}`,
|
||||
clientId: keycloakClientId,
|
||||
// NOTE: You can also pass queries params when calling login()
|
||||
extraQueryParams: () => ({
|
||||
// This adding ui_locales to the url will ensure the consistency of the language between the app and the login pages
|
||||
// If your app implements a i18n system (like i18nifty.dev for example) you should use this and replace "en" by the
|
||||
// current language of the app.
|
||||
// On the other side you will find kcContext.locale.currentLanguageTag to be whatever you set here.
|
||||
"ui_locales": "en",
|
||||
"my_custom_param": "value of foo transferred to login page"
|
||||
}),
|
||||
publicUrl: import.meta.env.BASE_URL,
|
||||
decodedIdTokenSchema: z.object({
|
||||
// Use https://jwt.io/ to tell what's in your idToken
|
||||
// It will depend of your Keycloak configuration.
|
||||
// Here I declare only two field on the type but actually there are
|
||||
// Many more things available.
|
||||
sub: z.string(),
|
||||
name: z.string(),
|
||||
preferred_username: z.string(),
|
||||
// This is a custom attribute set up in our Keycloak configuration
|
||||
// it's not present by default.
|
||||
// See https://docs.keycloakify.dev/realtime-input-validation#getting-your-custom-user-attribute-to-be-included-in-the-jwt
|
||||
favorite_pet: z.union([z.literal("cat"), z.literal("dog"), z.literal("bird")])
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export function getKeycloakAccountUrl(
|
||||
params: {
|
||||
locale: string;
|
||||
}
|
||||
){
|
||||
const { locale } = params;
|
||||
|
||||
const accountUrl = new URL(`${keycloakUrl}/realms/${keycloakRealm}/account`);
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
|
||||
searchParams.append("kc_locale", locale);
|
||||
searchParams.append("referrer", keycloakClientId);
|
||||
searchParams.append("referrer_uri", window.location.href);
|
||||
|
||||
accountUrl.search = searchParams.toString();
|
||||
|
||||
return accountUrl.toString();
|
||||
}
|
34
src/account/KcApp.tsx
Normal file
34
src/account/KcApp.tsx
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { Suspense, lazy } from "react";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
|
||||
const Fallback = lazy(() => import("keycloakify/account/Fallback"));
|
||||
const Template = lazy(() => import("./Template"));
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
default:
|
||||
return <Fallback
|
||||
{...{
|
||||
kcContext,
|
||||
i18n,
|
||||
Template,
|
||||
}}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
8
src/account/KcContext.ts
Normal file
8
src/account/KcContext.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/account";
|
||||
|
||||
export type KcContextExtraProperties = {};
|
||||
|
||||
export type KcContextExtraPropertiesPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtraProperties, KcContextExtraPropertiesPerPage>;
|
40
src/account/PageStory.tsx
Normal file
40
src/account/PageStory.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { createGetKcContextMock } from "keycloakify/account";
|
||||
import type {
|
||||
KcContextExtraProperties,
|
||||
KcContextExtraPropertiesPerPage
|
||||
} from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
|
||||
const kcContextExtraProperties: KcContextExtraProperties = {};
|
||||
const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {};
|
||||
|
||||
export const { getKcContextMock } = createGetKcContextMock({
|
||||
kcContextExtraProperties,
|
||||
kcContextExtraPropertiesPerPage,
|
||||
overrides: {},
|
||||
overridesPerPage: {}
|
||||
});
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<KcApp kcContext={kcContextMock} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
}
|
||||
|
|
@ -1,36 +1,62 @@
|
|||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
|
||||
// Copy pasted from: https://github.com/keycloakify/keycloakify/blob/main/src/account/Template.tsx
|
||||
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import type { TemplateProps } from "keycloakify/account/TemplateProps";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||
|
||||
const { isReady } = usePrepareTemplate({
|
||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||
"styles": [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("accountManagementTitle")
|
||||
useEffect(() => {
|
||||
document.title = msgStr("accountManagementTitle");
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "html",
|
||||
className: getClassName("kcHtmlClass")
|
||||
});
|
||||
|
||||
if (!isReady) {
|
||||
useSetClassName({
|
||||
qualifiedName: "body",
|
||||
className: clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { currentLanguageTag } = locale ?? {};
|
||||
|
||||
if (currentLanguageTag === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = currentLanguageTag;
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesPath}/css/account.css`
|
||||
]
|
||||
});
|
||||
|
||||
if (!areAllStyleSheetsLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -55,9 +81,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
<a href={getChangeLocalUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
5
src/account/i18n.ts
Normal file
5
src/account/i18n.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createUseI18n } from "keycloakify/account";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
|
@ -1,29 +0,0 @@
|
|||
Your theme source files should be located in a keycloak-theme directory somewhere in your src directory OR at the root of your directory.
|
||||
Acceptable directory strucuture:
|
||||
|
||||
```txt
|
||||
src/
|
||||
keycloak-theme/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
|
||||
===OR===
|
||||
|
||||
src/
|
||||
foo/
|
||||
bar/
|
||||
keycloak-theme/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
|
||||
===OR===
|
||||
|
||||
src/
|
||||
login/
|
||||
account/
|
||||
email/
|
||||
```
|
||||
|
||||
You don't need to have all three variant of the theme. If you only need the login theme for example you can have only the login directory.
|
|
@ -1,9 +0,0 @@
|
|||
/*
|
||||
If you use global CSS like we do here(not recommended)
|
||||
Be mindful that the CSS of the login theme may clash with the CSS of the account theme in Storybook (and only in storybook).
|
||||
This is why I made sure to use .my-root-account-class instead of .my-root-class that is already used in the login theme.
|
||||
*/
|
||||
|
||||
.my-root-account-class {
|
||||
background: url(./assets/background.svg) no-repeat center center fixed;
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { PageProps } from "keycloakify/account";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import Template from "./Template";
|
||||
|
||||
const Password = lazy(() => import("./pages/Password"));
|
||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||
const Fallback = lazy(()=> import("keycloakify/account"));
|
||||
|
||||
const classes = {
|
||||
"kcBodyClass": "my-root-account-class"
|
||||
} satisfies PageProps["classes"];
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext; }) {
|
||||
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "password.ftl": return <Password {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
default: return <Fallback {...{ kcContext, i18n, classes }} Template={Template} doUseDefaultCss={true} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<svg width="1521" height="961" viewBox="0 0 1521 961" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.4">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path d="M289.342 250.792L427.47 389.611C471.444 433.805 542.707 433.805 586.621 389.611L724.749 250.792L507.046 32L289.342 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M586.267 389.258L586.267 389.258C542.548 433.256 471.603 433.256 427.824 389.258L290.047 250.792L507.046 32.7089L724.044 250.792L586.267 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dd)">
|
||||
<path d="M32 509.755L170.128 648.573C214.103 692.767 285.365 692.767 329.28 648.573L467.408 509.755L249.704 290.962L32 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M328.925 648.221L328.925 648.221C285.206 692.218 214.262 692.219 170.483 648.221L32.7054 509.755L249.704 291.671L466.702 509.755L328.925 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dd)">
|
||||
<path d="M289.281 767.036L427.409 905.854C471.384 950.048 542.646 950.048 586.561 905.854L724.689 767.036L506.985 548.243L289.281 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M586.206 905.502L586.206 905.502C542.487 949.499 471.543 949.5 427.764 905.502L289.986 767.036L506.985 548.952L723.983 767.036L586.206 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dd)">
|
||||
<path d="M546.562 509.755L684.69 648.573C728.665 692.767 799.927 692.767 843.842 648.573L981.97 509.755L764.266 290.962L546.562 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M843.487 648.221L843.487 648.221C799.768 692.218 728.824 692.219 685.044 648.221L547.267 509.755L764.266 291.671L981.264 509.755L843.487 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dd)">
|
||||
<path d="M803.843 250.792L941.971 389.611C985.945 433.805 1057.21 433.805 1101.12 389.611L1239.25 250.792L1021.55 32L803.843 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M1100.77 389.258L1100.77 389.258C1057.05 433.256 986.105 433.256 942.325 389.258L804.548 250.792L1021.55 32.7089L1238.55 250.792L1100.77 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_dd)">
|
||||
<path d="M1062.81 509.755L1200.93 648.573C1244.91 692.767 1316.17 692.767 1360.08 648.573L1498.21 509.755L1280.51 290.962L1062.81 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M1359.73 648.221L1359.73 648.221C1316.01 692.218 1245.07 692.219 1201.29 648.221L1063.51 509.755L1280.51 291.671L1497.51 509.755L1359.73 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_dd)">
|
||||
<path d="M805.524 767.036L943.653 905.854C987.627 950.048 1058.89 950.048 1102.8 905.854L1240.93 767.036L1023.23 548.243L805.524 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M1102.45 905.502L1102.45 905.502C1058.73 949.499 987.786 949.5 944.007 905.502L806.23 767.036L1023.23 548.952L1240.23 767.036L1102.45 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="257.342" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd" x="0" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd" x="257.281" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_dd" x="514.562" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter4_dd" x="771.843" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter5_dd" x="1030.81" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter6_dd" x="773.524" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 9.3 KiB |
|
@ -1,30 +0,0 @@
|
|||
import { getKcContext, type KcContext } from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>; }) {
|
||||
|
||||
const { kcContext } = getKcContext({
|
||||
mockPageId: pageId,
|
||||
storyPartialKcContext: params.kcContext
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
|
||||
<link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
|
||||
<KcApp kcContext={kcContext} />
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import { createUseI18n } from "keycloakify/account";
|
||||
|
||||
//NOTE: See src/login/i18n.ts for instructions on customization of i18n messages.
|
||||
export const { useI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = NonNullable<ReturnType<typeof useI18n>>;
|
|
@ -1,23 +0,0 @@
|
|||
import { createGetKcContext } from "keycloakify/account";
|
||||
|
||||
export type KcContextExtension =
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
|
||||
|
||||
export const { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||
mockData: [
|
||||
{
|
||||
pageId: "my-extra-page-2.ftl",
|
||||
someCustomValue: "foo bar"
|
||||
}
|
||||
],
|
||||
mockProperties: {
|
||||
MY_ENV_VARIABLE: "Mocked value"
|
||||
}
|
||||
});
|
||||
|
||||
export const { kcContext } = getKcContext({
|
||||
//mockPageId: "password.ftl",
|
||||
});
|
||||
|
||||
export type KcContext = NonNullable<ReturnType<typeof getKcContext>["kcContext"]>;
|
|
@ -1,15 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="my-extra-page-1" >
|
||||
<h1>Hello world 1</h1>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
// someCustomValue is declared by you in ../kcContext.ts
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="my-extra-page-2" >
|
||||
<h1>Hello world 2</h1>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "password.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "account/Password",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory
|
||||
kcContext={{
|
||||
message: { type: "success", summary: "This is a test message" }
|
||||
}}
|
||||
/>
|
||||
};
|
|
@ -1,105 +0,0 @@
|
|||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
"classes": {
|
||||
...classes,
|
||||
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||
}
|
||||
});
|
||||
|
||||
const { url, password, account, stateChecker } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
|
||||
<div className="row">
|
||||
<div className="col-md-10">
|
||||
<h2>{msg("changePasswordHtmlTitle")}</h2>
|
||||
</div>
|
||||
<div className="col-md-2 subtitle">
|
||||
<span className="subtitle">{msg("allFieldsRequired")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action={url.passwordUrl} className="form-horizontal" method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={account.username ?? ""}
|
||||
autoComplete="username"
|
||||
readOnly
|
||||
style={{ "display": "none" }}
|
||||
/>
|
||||
|
||||
{password.passwordSet && (
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password" className="control-label">
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-new" className="control-label">
|
||||
{msg("passwordNew")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div className="col-sm-2 col-md-2">
|
||||
<label htmlFor="password-confirm" className="control-label two-lines">
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="col-sm-10 col-md-10">
|
||||
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="submitAction"
|
||||
value="Save"
|
||||
>
|
||||
{msg("doSave")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
|
||||
.my-color {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.my-font {
|
||||
font-family: 'Work Sans';
|
||||
}
|
||||
|
||||
.my-root-class {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.my-root-class body {
|
||||
background: url(./assets/background.svg) no-repeat center center fixed;
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
import "./KcApp.css";
|
||||
import { lazy, Suspense } from "react";
|
||||
import Fallback, { type PageProps } from "keycloakify/login";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import Template from "./Template";
|
||||
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
|
||||
const Register = lazy(() => import("./pages/Register"));
|
||||
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
|
||||
const Terms = lazy(() => import("./pages/Terms"));
|
||||
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
|
||||
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
|
||||
const Info = lazy(() => import("keycloakify/login/pages/Info"));
|
||||
|
||||
// This is like adding classes to theme.properties
|
||||
// https://github.com/keycloak/keycloak/blob/11.0.3/themes/src/main/resources/theme/keycloak/login/theme.properties
|
||||
const classes = {
|
||||
// NOTE: The classes are defined in ./KcApp.css
|
||||
"kcHtmlClass": "my-root-class",
|
||||
"kcHeaderWrapperClass": "my-color my-font"
|
||||
} satisfies PageProps["classes"];
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext; }) {
|
||||
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
if (i18n === null) {
|
||||
//NOTE: Text resources for the current language are still being downloaded, we can't display anything yet.
|
||||
//We could display a loading progress but it's usually a matter of milliseconds.
|
||||
return null;
|
||||
}
|
||||
|
||||
/*
|
||||
* Examples assuming i18n.currentLanguageTag === "en":
|
||||
* i18n.msg("access-denied") === <span>Access denied</span>
|
||||
* i18n.msg("foo") === <span>foo in English</span>
|
||||
*/
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl": return <Login {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "register.ftl": return <Register {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />
|
||||
case "terms.ftl": return <Terms {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
// Removes those pages in you project. They are included to show you how to implement keycloak pages
|
||||
// that are not yes implemented by Keycloakify.
|
||||
// See: https://docs.keycloakify.dev/limitations#some-pages-still-have-the-default-theme.-why
|
||||
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, i18n, Template, classes }} doUseDefaultCss={true} />;
|
||||
// We choose to use the default Template for the Info page and to download the theme resources.
|
||||
// This is just an example to show you what is possible. You likely don't want to keep this as is.
|
||||
case "info.ftl": return (
|
||||
<Info
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={lazy(() => import("keycloakify/login/Template"))}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
default: return <Fallback {...{ kcContext, i18n, classes }} Template={Template} doUseDefaultCss={true} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,212 +0,0 @@
|
|||
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||
import { type TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import keycloakifyLogoPngUrl from "./assets/keycloakify-logo.png";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
displayWide = false,
|
||||
showAnotherWayIfPresent = true,
|
||||
headerNode,
|
||||
showUsernameNode = null,
|
||||
infoNode = null,
|
||||
kcContext,
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
classes,
|
||||
children
|
||||
} = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const { isReady } = usePrepareTemplate({
|
||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||
"styles": [
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesCommonPath}/lib/zocial/zocial.css`,
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
],
|
||||
"htmlClassName": getClassName("kcHtmlClass"),
|
||||
"bodyClassName": getClassName("kcBodyClass"),
|
||||
"htmlLangProperty": locale?.currentLanguageTag,
|
||||
"documentTitle": i18n.msgStr("loginTitle", kcContext.realm.displayName)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log(`Value of MY_ENV_VARIABLE on the Keycloak server: "${kcContext.properties.MY_ENV_VARIABLE}"`);
|
||||
}, []);
|
||||
|
||||
if (!isReady) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLoginClass")}>
|
||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
||||
<div
|
||||
id="kc-header-wrapper"
|
||||
className={getClassName("kcHeaderWrapperClass")}
|
||||
style={{ "fontFamily": '"Work Sans"' }}
|
||||
>
|
||||
{/*
|
||||
Here we are referencing the `keycloakify-logo.png` in the `public` directory.
|
||||
When possible don't use this approach, instead ...
|
||||
*/}
|
||||
<img src={`${import.meta.env.BASE_URL}keycloakify-logo.png`} alt="Keycloakify logo" width={50} />
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}!!!
|
||||
{/* ...rely on the bundler to import your assets, it's more efficient */}
|
||||
<img src={keycloakifyLogoPngUrl} alt="Keycloakify logo" width={50} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
|
||||
<header className={getClassName("kcFormHeaderClass")}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<div id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
<ul>
|
||||
{locale.supported.map(({ languageTag }) => (
|
||||
<li key={languageTag} className="kc-dropdown-item">
|
||||
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
|
||||
{message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
|
||||
{message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
|
||||
{message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
"__html": message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && showAnotherWayIfPresent && (
|
||||
<form
|
||||
id="kc-select-try-another-way-form"
|
||||
action={url.loginAction}
|
||||
method="post"
|
||||
className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<a
|
||||
href="#"
|
||||
id="try-another-way"
|
||||
onClick={() => {
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{msg("doTryAnotherWay")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={getClassName("kcSignUpClass")}>
|
||||
<div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
<svg width="1521" height="961" viewBox="0 0 1521 961" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g opacity="0.4">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path d="M289.342 250.792L427.47 389.611C471.444 433.805 542.707 433.805 586.621 389.611L724.749 250.792L507.046 32L289.342 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M586.267 389.258L586.267 389.258C542.548 433.256 471.603 433.256 427.824 389.258L290.047 250.792L507.046 32.7089L724.044 250.792L586.267 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_dd)">
|
||||
<path d="M32 509.755L170.128 648.573C214.103 692.767 285.365 692.767 329.28 648.573L467.408 509.755L249.704 290.962L32 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M328.925 648.221L328.925 648.221C285.206 692.218 214.262 692.219 170.483 648.221L32.7054 509.755L249.704 291.671L466.702 509.755L328.925 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_dd)">
|
||||
<path d="M289.281 767.036L427.409 905.854C471.384 950.048 542.646 950.048 586.561 905.854L724.689 767.036L506.985 548.243L289.281 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M586.206 905.502L586.206 905.502C542.487 949.499 471.543 949.5 427.764 905.502L289.986 767.036L506.985 548.952L723.983 767.036L586.206 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_dd)">
|
||||
<path d="M546.562 509.755L684.69 648.573C728.665 692.767 799.927 692.767 843.842 648.573L981.97 509.755L764.266 290.962L546.562 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M843.487 648.221L843.487 648.221C799.768 692.218 728.824 692.219 685.044 648.221L547.267 509.755L764.266 291.671L981.264 509.755L843.487 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_dd)">
|
||||
<path d="M803.843 250.792L941.971 389.611C985.945 433.805 1057.21 433.805 1101.12 389.611L1239.25 250.792L1021.55 32L803.843 250.792Z" fill="#EFEEEE"/>
|
||||
<path d="M1100.77 389.258L1100.77 389.258C1057.05 433.256 986.105 433.256 942.325 389.258L804.548 250.792L1021.55 32.7089L1238.55 250.792L1100.77 389.258Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter5_dd)">
|
||||
<path d="M1062.81 509.755L1200.93 648.573C1244.91 692.767 1316.17 692.767 1360.08 648.573L1498.21 509.755L1280.51 290.962L1062.81 509.755Z" fill="#EFEEEE"/>
|
||||
<path d="M1359.73 648.221L1359.73 648.221C1316.01 692.218 1245.07 692.219 1201.29 648.221L1063.51 509.755L1280.51 291.671L1497.51 509.755L1359.73 648.221Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
<g filter="url(#filter6_dd)">
|
||||
<path d="M805.524 767.036L943.653 905.854C987.627 950.048 1058.89 950.048 1102.8 905.854L1240.93 767.036L1023.23 548.243L805.524 767.036Z" fill="#EFEEEE"/>
|
||||
<path d="M1102.45 905.502L1102.45 905.502C1058.73 949.499 987.786 949.5 944.007 905.502L806.23 767.036L1023.23 548.952L1240.23 767.036L1102.45 905.502Z" stroke="white" stroke-opacity="0.01"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="257.342" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_dd" x="0" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter2_dd" x="257.281" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter3_dd" x="514.562" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter4_dd" x="771.843" y="0" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter5_dd" x="1030.81" y="258.962" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter6_dd" x="773.524" y="516.243" width="489.408" height="444.757" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="6" dy="6"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.75 0 0 0 0 0.71011 0 0 0 0 0.653125 0 0 0 0.51 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-6" dy="-6"/>
|
||||
<feGaussianBlur stdDeviation="13"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.83 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 102 KiB |
|
@ -1,30 +0,0 @@
|
|||
import { getKcContext, type KcContext } from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: {
|
||||
pageId: PageId;
|
||||
}) {
|
||||
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>>; }) {
|
||||
|
||||
const { kcContext } = getKcContext({
|
||||
mockPageId: pageId,
|
||||
storyPartialKcContext: params.kcContext
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If you import custom fonts in your index.html you have to import them in storybook as well*/}
|
||||
<link rel="stylesheet" type="text/css" href={`${import.meta.env.BASE_URL}fonts/WorkSans/font.css`} />
|
||||
<KcApp kcContext={kcContext} />
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import { createUseI18n } from "keycloakify/login";
|
||||
|
||||
export const { useI18n } = createUseI18n({
|
||||
// NOTE: Here you can override the default i18n messages
|
||||
// or define new ones that, for example, you would have
|
||||
// defined in the Keycloak admin UI for UserProfile
|
||||
// https://user-images.githubusercontent.com/6702424/182050652-522b6fe6-8ee5-49df-aca3-dba2d33f24a5.png
|
||||
en: {
|
||||
alphanumericalCharsOnly: "Only alphanumerical characters",
|
||||
gender: "Gender",
|
||||
// Here we overwrite the default english value for the message "doForgotPassword"
|
||||
// that is "Forgot Password?" see: https://github.com/InseeFrLab/keycloakify/blob/f0ae5ea908e0aa42391af323b6d5e2fd371af851/src/lib/i18n/generated_messages/18.0.1/login/en.ts#L17
|
||||
doForgotPassword: "I forgot my password",
|
||||
invalidUserMessage: "Invalid username or password. (this message was overwrite in the theme)"
|
||||
},
|
||||
fr: {
|
||||
/* spell-checker: disable */
|
||||
alphanumericalCharsOnly: "Caractère alphanumérique uniquement",
|
||||
gender: "Genre",
|
||||
doForgotPassword: "J'ai oublié mon mot de passe",
|
||||
invalidUserMessage: "Nom d'utilisateur ou mot de passe invalide. (ce message a été écrasé dans le thème)"
|
||||
/* spell-checker: enable */
|
||||
}
|
||||
});
|
||||
|
||||
export type I18n = NonNullable<ReturnType<typeof useI18n>>;
|
|
@ -1,104 +0,0 @@
|
|||
import { createGetKcContext } from "keycloakify/login";
|
||||
|
||||
export type KcContextExtension =
|
||||
| { pageId: "login.ftl"; }
|
||||
| { pageId: "my-extra-page-1.ftl"; }
|
||||
| { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
|
||||
// NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
|
||||
// but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
|
||||
// keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
|
||||
| { pageId: "register.ftl"; authorizedMailDomains: string[]; };
|
||||
|
||||
//NOTE: In most of the cases you do not need to overload the KcContext, you can
|
||||
// just call createGetKcContext(...) without type arguments.
|
||||
// You want to overload the KcContext only if:
|
||||
// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
|
||||
// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
|
||||
export const { getKcContext } = createGetKcContext<KcContextExtension>({
|
||||
mockData: [
|
||||
{
|
||||
pageId: "login.ftl",
|
||||
locale: {
|
||||
//When we test the login page we do it in french
|
||||
currentLanguageTag: "fr",
|
||||
},
|
||||
//Uncomment the following line for hiding the Alert message
|
||||
//"message": undefined
|
||||
//Uncomment the following line for showing an Error message
|
||||
//message: { type: "error", summary: "This is an error" }
|
||||
},
|
||||
{
|
||||
pageId: "my-extra-page-2.ftl",
|
||||
someCustomValue: "foo bar baz",
|
||||
},
|
||||
{
|
||||
//NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
|
||||
pageId: "register-user-profile.ftl",
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
profile: {
|
||||
attributes: [
|
||||
{
|
||||
validators: {
|
||||
pattern: {
|
||||
pattern: "^[a-zA-Z0-9]+$",
|
||||
"ignore.empty.value": true,
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
"error-message": "${alphanumericalCharsOnly}",
|
||||
},
|
||||
},
|
||||
//NOTE: To override the default mock value
|
||||
value: undefined,
|
||||
name: "username"
|
||||
},
|
||||
{
|
||||
validators: {
|
||||
options: {
|
||||
options: ["male", "female", "non_binary", "prefer_not_to_say"]
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
displayName: "${gender}",
|
||||
annotations: {},
|
||||
required: true,
|
||||
groupAnnotations: {},
|
||||
readOnly: false,
|
||||
name: "gender"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
pageId: "register.ftl",
|
||||
authorizedMailDomains: [
|
||||
"example.com",
|
||||
"another-example.com",
|
||||
"*.yet-another-example.com",
|
||||
"*.example.com",
|
||||
"hello-world.com"
|
||||
],
|
||||
// Simulate we got an error with the email field. Return text if message for given field exists.
|
||||
messagesPerField: {
|
||||
printIfExists: <T>(fieldName: string, text: T) => { console.log({ fieldName }); return fieldName === "email" ? text : undefined; },
|
||||
existsError: (fieldName: string) => fieldName === "email",
|
||||
get: (fieldName: string) => `Fake error for ${fieldName}`,
|
||||
exists: (fieldName: string) => fieldName === "email"
|
||||
},
|
||||
|
||||
}
|
||||
],
|
||||
// Defined in vite.config.ts
|
||||
// See: https://docs.keycloakify.dev/environnement-variables
|
||||
mockProperties: {
|
||||
MY_ENV_VARIABLE: "Mocked value"
|
||||
}
|
||||
});
|
||||
|
||||
export const { kcContext } = getKcContext({
|
||||
// Uncomment to test the login page for development.
|
||||
//mockPageId: "login.ftl",
|
||||
});
|
||||
|
||||
|
||||
export type KcContext = NonNullable<ReturnType<typeof getKcContext>["kcContext"]>;
|
|
@ -1,83 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "login.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/Login",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />,
|
||||
};
|
||||
|
||||
export const WithoutPasswordField: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { password: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutRegistration: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { registrationAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutRememberMe: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { rememberMe: false } }} />,
|
||||
};
|
||||
|
||||
export const WithoutPasswordReset: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { resetPasswordAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => <PageStory kcContext={{ realm: { loginWithEmailAllowed: false } }} />,
|
||||
};
|
||||
|
||||
export const WithPresetUsername: Story = {
|
||||
render: () => <PageStory kcContext={{ login: { username: "max.mustermann@mail.com" } }} />,
|
||||
};
|
||||
|
||||
export const WithImmutablePresetUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
attemptedUsername: "max.mustermann@mail.com",
|
||||
showUsername: true,
|
||||
},
|
||||
usernameHidden: true,
|
||||
message: { type: "info", summary: "Please re-authenticate to continue" },
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const WithSocialProviders: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{ loginUrl: 'google', alias: 'google', providerId: 'google', displayName: 'Google' },
|
||||
{ loginUrl: 'microsoft', alias: 'microsoft', providerId: 'microsoft', displayName: 'Microsoft' },
|
||||
{ loginUrl: 'facebook', alias: 'facebook', providerId: 'facebook', displayName: 'Facebook' },
|
||||
{ loginUrl: 'instagram', alias: 'instagram', providerId: 'instagram', displayName: 'Instagram' },
|
||||
{ loginUrl: 'twitter', alias: 'twitter', providerId: 'twitter', displayName: 'Twitter' },
|
||||
{ loginUrl: 'linkedin', alias: 'linkedin', providerId: 'linkedin', displayName: 'LinkedIn' },
|
||||
{ loginUrl: 'stackoverflow', alias: 'stackoverflow', providerId: 'stackoverflow', displayName: 'Stackoverflow' },
|
||||
{ loginUrl: 'github', alias: 'github', providerId: 'github', displayName: 'Github' },
|
||||
{ loginUrl: 'gitlab', alias: 'gitlab', providerId: 'gitlab', displayName: 'Gitlab' },
|
||||
{ loginUrl: 'bitbucket', alias: 'bitbucket', providerId: 'bitbucket', displayName: 'Bitbucket' },
|
||||
{ loginUrl: 'paypal', alias: 'paypal', providerId: 'paypal', displayName: 'PayPal' },
|
||||
{ loginUrl: 'openshift', alias: 'openshift', providerId: 'openshift', displayName: 'OpenShift' },
|
||||
],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
};
|
|
@ -1,204 +0,0 @@
|
|||
import { useState, type FormEventHandler } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useConstCallback } from "keycloakify/tools/useConstCallback";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const my_custom_param = new URL(window.location.href).searchParams.get("my_custom_param");
|
||||
|
||||
if (my_custom_param !== null) {
|
||||
console.log("my_custom_param:", my_custom_param);
|
||||
}
|
||||
|
||||
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { social, realm, url, usernameHidden, login, auth, registrationDisabled } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
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, doUseDefaultCss, classes }}
|
||||
displayInfo={
|
||||
realm.password &&
|
||||
realm.registrationAllowed &&
|
||||
!registrationDisabled
|
||||
}
|
||||
displayWide={realm.password && social.providers !== undefined}
|
||||
headerNode={msg("doLogIn")}
|
||||
infoNode={
|
||||
<div id="kc-registration">
|
||||
<span>
|
||||
{msg("noAccount")}
|
||||
<a tabIndex={6} href={url.registrationUrl}>
|
||||
{msg("doRegister")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && getClassName("kcContentWrapperClass"))}>
|
||||
<div
|
||||
id="kc-form-wrapper"
|
||||
className={clsx(
|
||||
realm.password &&
|
||||
social.providers && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
|
||||
)}
|
||||
>
|
||||
{realm.password && (
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
{!usernameHidden &&
|
||||
(() => {
|
||||
const label = !realm.loginWithEmailAllowed
|
||||
? "username"
|
||||
: realm.registrationEmailAsUsername
|
||||
? "email"
|
||||
: "usernameOrEmail";
|
||||
|
||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={autoCompleteHelper} className={getClassName("kcLabelClass")}>
|
||||
{msg(label)}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id={autoCompleteHelper}
|
||||
className={getClassName("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={getClassName("kcFormGroupClass")}>
|
||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={2}
|
||||
id="password"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(getClassName("kcFormGroupClass"), getClassName("kcFormSettingClass"))}>
|
||||
<div id="kc-form-options">
|
||||
{realm.rememberMe && !usernameHidden && (
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input
|
||||
tabIndex={3}
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
{...(login.rememberMe === "on"
|
||||
? {
|
||||
"checked": true
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
{msg("rememberMe")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
{realm.resetPasswordAllowed && (
|
||||
<span>
|
||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
||||
{msg("doForgotPassword")}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormGroupClass")}>
|
||||
<input
|
||||
type="hidden"
|
||||
id="id-hidden-input"
|
||||
name="credentialId"
|
||||
{...(auth?.selectedCredential !== undefined
|
||||
? {
|
||||
"value": auth.selectedCredential
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("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={clsx(getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass"))}
|
||||
>
|
||||
<ul
|
||||
className={clsx(
|
||||
getClassName("kcFormSocialAccountListClass"),
|
||||
social.providers.length > 4 && getClassName("kcFormSocialAccountDoubleListClass")
|
||||
)}
|
||||
>
|
||||
{social.providers.map(p => (
|
||||
<li key={p.providerId} className={getClassName("kcFormSocialAccountListLinkClass")}>
|
||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
|
||||
<span>{p.displayName}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
//This is to show that you can create stories for pages that you haven't overloaded.
|
||||
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "login-reset-password.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/LoginResetPassword",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
|
@ -1,21 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
infoNode={<span>footer</span>}
|
||||
>
|
||||
<form>
|
||||
{/*...*/}
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "my-extra-page-2.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/MyExtraPage2",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WitAbc: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
someCustomValue: "abc"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
|
@ -1,26 +0,0 @@
|
|||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
|
||||
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
// someCustomValue is declared by you in ../kcContext.ts
|
||||
console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
headerNode={<>Header <i>text</i></>}
|
||||
infoNode={<span>footer</span>}
|
||||
>
|
||||
|
||||
<form>
|
||||
{kcContext.someCustomValue}
|
||||
{/*...*/}
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
|
||||
}
|
|
@ -1,183 +0,0 @@
|
|||
// ejected using 'npx eject-keycloak-page'
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}>
|
||||
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="firstName"
|
||||
defaultValue={register.formData.firstName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
|
||||
{msg("lastName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="lastName"
|
||||
defaultValue={register.formData.lastName ?? ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="email" className={getClassName("kcLabelClass")}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="email"
|
||||
defaultValue={register.formData.email ?? ""}
|
||||
autoComplete="email"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="username" className={getClassName("kcLabelClass")}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="username"
|
||||
defaultValue={register.formData.username ?? ""}
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{passwordRequired && (
|
||||
<>
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password" className={getClassName("kcLabelClass")}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className={getClassName("kcInputClass")}
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
|
||||
)}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,71 +0,0 @@
|
|||
// ejected using 'npx eject-keycloak-page'
|
||||
import { useState } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileFormFields";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doUseDefaultCss, classes }}
|
||||
displayMessage={messagesPerField.exists("global")}
|
||||
displayRequiredFields={true}
|
||||
headerNode={msg("registerTitle")}
|
||||
>
|
||||
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
|
||||
<UserProfileFormFields
|
||||
kcContext={kcContext}
|
||||
onIsFormSubmittableValueChange={setIsFormSubmittable}
|
||||
i18n={i18n}
|
||||
getClassName={getClassName}
|
||||
/>
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={getClassName("kcFormGroupClass")} style={{ "marginBottom": 30 }}>
|
||||
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||
<div className={getClassName("kcFormOptionsWrapperClass")}>
|
||||
<span>
|
||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonBlockClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
disabled={!isFormSubmittable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { createPageStory } from "../createPageStory";
|
||||
|
||||
const { PageStory } = createPageStory({
|
||||
pageId: "terms.ftl"
|
||||
});
|
||||
|
||||
const meta = {
|
||||
title: "login/Terms",
|
||||
component: PageStory,
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Primary: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
|
@ -1,81 +0,0 @@
|
|||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import { Markdown } from "keycloakify/tools/Markdown";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms";
|
||||
import type { KcContext } from "../kcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { useDownloadTerms } from "keycloakify/login";
|
||||
|
||||
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { getClassName } = useGetClassName({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
// NOTE: If you aren't going to customize the layout of the page you can move this hook to
|
||||
// KcApp.tsx, see: https://docs.keycloakify.dev/terms-and-conditions
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
"downloadTermMarkdown": async ({currentLanguageTag}) => {
|
||||
|
||||
const tos_url = (() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return `${import.meta.env.BASE_URL}terms/fr.md`;
|
||||
default: return `${import.meta.env.BASE_URL}terms/en.md`;
|
||||
}
|
||||
})();
|
||||
|
||||
const markdownString = await fetch(tos_url).then(response => response.text());
|
||||
|
||||
return markdownString;
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const termMarkdown = evtTermMarkdown.state;
|
||||
|
||||
if (termMarkdown === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("termsTitle")}>
|
||||
<div id="kc-terms-text">
|
||||
<Markdown>{termMarkdown}</Markdown>
|
||||
</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={clsx(
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonClass"),
|
||||
getClassName("kcButtonPrimaryClass"),
|
||||
getClassName("kcButtonLargeClass")
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-accept"
|
||||
type="submit"
|
||||
value={msgStr("doAccept")}
|
||||
/>
|
||||
<input
|
||||
className={clsx(getClassName("kcButtonClass"), getClassName("kcButtonDefaultClass"), getClassName("kcButtonLargeClass"))}
|
||||
name="cancel"
|
||||
id="kc-decline"
|
||||
type="submit"
|
||||
value={msgStr("doDecline")}
|
||||
/>
|
||||
</form>
|
||||
<div className="clearfix" />
|
||||
</Template>
|
||||
);
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
import { useEffect, Fragment } from "react";
|
||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useFormValidation } from "keycloakify/login/lib/useFormValidation";
|
||||
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
|
||||
import type { I18n } from "../../i18n";
|
||||
|
||||
export type UserProfileFormFieldsProps = {
|
||||
kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
|
||||
i18n: I18n;
|
||||
getClassName: (classKey: ClassKey) => string;
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
BeforeField?: (props: { attribute: Attribute }) => JSX.Element | null;
|
||||
AfterField?: (props: { attribute: Attribute }) => JSX.Element | null;
|
||||
};
|
||||
|
||||
export function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
||||
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
|
||||
|
||||
const { advancedMsg, msg } = i18n;
|
||||
|
||||
const {
|
||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||
formValidationDispatch,
|
||||
attributesWithPassword
|
||||
} = useFormValidation({
|
||||
kcContext,
|
||||
i18n
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||
}, [isFormSubmittable]);
|
||||
|
||||
let currentGroup = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{attributesWithPassword.map((attribute, i) => {
|
||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||
|
||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||
|
||||
const formGroupClassName = clsx(
|
||||
getClassName("kcFormGroupClass"),
|
||||
displayableErrors.length !== 0 && getClassName("kcFormGroupErrorClass")
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<label id={`header-${group}`} className={getClassName("kcFormGroupHeader")}>
|
||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label id={`description-${group}`} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{BeforeField && <BeforeField attribute={attribute} />}
|
||||
|
||||
<div className={formGroupClassName}>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor={attribute.name} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
{(() => {
|
||||
const { options } = attribute.validators;
|
||||
|
||||
if (options !== undefined) {
|
||||
return (
|
||||
<select
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
"action": "update value",
|
||||
"name": attribute.name,
|
||||
"newValue": event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
"action": "focus lost",
|
||||
"name": attribute.name
|
||||
})
|
||||
}
|
||||
value={value}
|
||||
>
|
||||
<>
|
||||
<option value="" selected disabled hidden>
|
||||
{msg("selectAnOption")}
|
||||
</option>
|
||||
{options.options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</>
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={(() => {
|
||||
switch (attribute.name) {
|
||||
case "password-confirm":
|
||||
case "password":
|
||||
return "password";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
})()}
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={value}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
"action": "update value",
|
||||
"name": attribute.name,
|
||||
"newValue": event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
"action": "focus lost",
|
||||
"name": attribute.name
|
||||
})
|
||||
}
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{displayableErrors.length !== 0 &&
|
||||
(() => {
|
||||
const divId = `input-error-${attribute.name}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`#${divId} > span: { display: block; }`}</style>
|
||||
<span
|
||||
id={divId}
|
||||
className={getClassName("kcInputErrorMessageClass")}
|
||||
style={{
|
||||
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
||||
}}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
{AfterField && <AfterField attribute={attribute} />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
57
src/login/KcApp.tsx
Normal file
57
src/login/KcApp.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import { Suspense, lazy } from "react";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import { useDownloadTerms } from "keycloakify/login";
|
||||
|
||||
const Fallback = lazy(() => import("keycloakify/login/Fallback"));
|
||||
const Template = lazy(() => import("./Template"));
|
||||
const UserProfileFormFields = lazy(() => import("./UserProfileFormFields"));
|
||||
|
||||
export default function KcApp(props: { kcContext: KcContext }) {
|
||||
const { kcContext } = props;
|
||||
|
||||
const i18n = useI18n({ kcContext });
|
||||
|
||||
useDownloadTerms({
|
||||
kcContext,
|
||||
downloadTermMarkdown: async ({ currentLanguageTag }) => {
|
||||
|
||||
const termsFileName = (() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "fr": return "fr.md";
|
||||
case "es": return "es.md";
|
||||
default: return "en.md";
|
||||
}
|
||||
})();
|
||||
|
||||
// The files are in the public directory.
|
||||
const response = await fetch(`${import.meta.env}terms/${termsFileName}`);
|
||||
|
||||
return response.text();
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
default:
|
||||
return <Fallback
|
||||
{...{
|
||||
kcContext,
|
||||
i18n,
|
||||
Template,
|
||||
UserProfileFormFields
|
||||
}}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
8
src/login/KcContext.ts
Normal file
8
src/login/KcContext.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import type { ExtendKcContext } from "keycloakify/login";
|
||||
|
||||
export type KcContextExtraProperties = {};
|
||||
|
||||
export type KcContextExtraPropertiesPerPage = {};
|
||||
|
||||
export type KcContext = ExtendKcContext<KcContextExtraProperties, KcContextExtraPropertiesPerPage>;
|
40
src/login/PageStory.tsx
Normal file
40
src/login/PageStory.tsx
Normal file
|
@ -0,0 +1,40 @@
|
|||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { KcContext } from "./kcContext";
|
||||
import KcApp from "./KcApp";
|
||||
import { createGetKcContextMock } from "keycloakify/login";
|
||||
import type {
|
||||
KcContextExtraProperties,
|
||||
KcContextExtraPropertiesPerPage
|
||||
} from "./kcContext";
|
||||
|
||||
const kcContextExtraProperties: KcContextExtraProperties = {};
|
||||
const kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage = {};
|
||||
|
||||
export const { getKcContextMock } = createGetKcContextMock({
|
||||
kcContextExtraProperties,
|
||||
kcContextExtraPropertiesPerPage,
|
||||
overrides: {},
|
||||
overridesPerPage: {}
|
||||
});
|
||||
|
||||
export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
|
||||
const { pageId } = params;
|
||||
|
||||
function PageStory(props: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
|
||||
const { kcContext: overrides } = props;
|
||||
|
||||
const kcContextMock = getKcContextMock({
|
||||
pageId,
|
||||
overrides
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<KcApp kcContext={kcContextMock} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return { PageStory };
|
||||
}
|
||||
|
278
src/login/Template.tsx
Normal file
278
src/login/Template.tsx
Normal file
|
@ -0,0 +1,278 @@
|
|||
// Copy pasted from: https://github.com/keycloakify/keycloakify/blob/main/src/login/Template.tsx
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
headerNode,
|
||||
showUsernameNode = null,
|
||||
socialProvidersNode = null,
|
||||
infoNode = null,
|
||||
documentTitle,
|
||||
bodyClassName,
|
||||
kcContext,
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
classes,
|
||||
children
|
||||
} = props;
|
||||
|
||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||
|
||||
const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext;
|
||||
|
||||
useEffect(() => {
|
||||
document.title = documentTitle ?? msgStr("loginTitle", kcContext.realm.displayName);
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "html",
|
||||
className: getClassName("kcHtmlClass")
|
||||
});
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "body",
|
||||
className: bodyClassName ?? getClassName("kcBodyClass")
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { currentLanguageTag } = locale ?? {};
|
||||
|
||||
if (currentLanguageTag === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html = document.querySelector("html");
|
||||
assert(html !== null);
|
||||
html.lang = currentLanguageTag;
|
||||
}, []);
|
||||
|
||||
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
|
||||
componentOrHookName: "Template",
|
||||
hrefs: !doUseDefaultCss
|
||||
? []
|
||||
: [
|
||||
`${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
||||
`${url.resourcesCommonPath}/lib/pficon/pficon.css`,
|
||||
`${url.resourcesPath}/css/login.css`
|
||||
]
|
||||
});
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "Template",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "module",
|
||||
src: `${url.resourcesPath}/js/menu-button-links.js`
|
||||
},
|
||||
...(authenticationSession === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
type: "module",
|
||||
textContent: [
|
||||
`import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`,
|
||||
``,
|
||||
`checkCookiesAndSetTimer(`,
|
||||
` "${authenticationSession.authSessionId}",`,
|
||||
` "${authenticationSession.tabId}",`,
|
||||
` "${url.ssoLoginInOtherTabsUrl}"`,
|
||||
`);`
|
||||
].join("\n")
|
||||
} as const
|
||||
]),
|
||||
...scripts.map(
|
||||
script =>
|
||||
({
|
||||
type: "text/javascript",
|
||||
src: script
|
||||
}) as const
|
||||
)
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (areAllStyleSheetsLoaded) {
|
||||
insertScriptTags();
|
||||
}
|
||||
}, [areAllStyleSheetsLoaded]);
|
||||
|
||||
if (!areAllStyleSheetsLoaded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLoginClass")}>
|
||||
<div id="kc-header" className={getClassName("kcHeaderClass")}>
|
||||
<div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={getClassName("kcFormCardClass")}>
|
||||
<header className={getClassName("kcFormHeaderClass")}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
|
||||
<div className={getClassName("kcLocaleMainClass")} id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
|
||||
<div id="kc-locale-dropdown" className={clsx("menu-button-links", getClassName("kcLocaleDropDownClass"))}>
|
||||
<button
|
||||
tabIndex={1}
|
||||
id="kc-current-locale-link"
|
||||
aria-label={msgStr("languages")}
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-controls="language-switch1"
|
||||
>
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</button>
|
||||
<ul
|
||||
role="menu"
|
||||
tabIndex={-1}
|
||||
aria-labelledby="kc-current-locale-link"
|
||||
aria-activedescendant=""
|
||||
id="language-switch1"
|
||||
className={getClassName("kcLocaleListClass")}
|
||||
>
|
||||
{locale.supported.map(({ languageTag }, i) => (
|
||||
<li key={languageTag} className={getClassName("kcLocaleListItemClass")} role="none">
|
||||
<a
|
||||
role="menuitem"
|
||||
id={`language-${i + 1}`}
|
||||
className={getClassName("kcLocaleItemClass")}
|
||||
href={getChangeLocalUrl(languageTag)}
|
||||
>
|
||||
{labelBySupportedLanguageTag[languageTag]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div id="kc-username" className={getClassName("kcFormGroupClass")}>
|
||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
<div id="kc-username" className={getClassName("kcFormGroupClass")}>
|
||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={getClassName("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div
|
||||
className={clsx(
|
||||
`alert-${message.type}`,
|
||||
getClassName("kcAlertClass"),
|
||||
`pf-m-${message?.type === "error" ? "danger" : message.type}`
|
||||
)}
|
||||
>
|
||||
<div className="pf-c-alert__icon">
|
||||
{message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
|
||||
{message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
|
||||
{message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
|
||||
{message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
|
||||
</div>
|
||||
<span
|
||||
className={getClassName("kcAlertTitleClass")}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<div className={getClassName("kcFormGroupClass")}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<a
|
||||
href="#"
|
||||
id="try-another-way"
|
||||
onClick={() => {
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{msg("doTryAnotherWay")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{socialProvidersNode}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={getClassName("kcSignUpClass")}>
|
||||
<div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
699
src/login/UserProfileFormFields.tsx
Normal file
699
src/login/UserProfileFormFields.tsx
Normal file
|
@ -0,0 +1,699 @@
|
|||
// Copy pasted from: https://github.com/keycloakify/keycloakify/blob/main/src/login/UserProfileFormFields.tsx
|
||||
|
||||
import { useEffect, useReducer, Fragment } from "react";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { ClassKey } from "keycloakify/login/TemplateProps";
|
||||
import {
|
||||
useUserProfileForm,
|
||||
getButtonToDisplayForMultivaluedAttributeField,
|
||||
type KcContextLike,
|
||||
type FormAction,
|
||||
type FormFieldError
|
||||
} from "keycloakify/login/lib/useUserProfileForm";
|
||||
import type { Attribute } from "keycloakify/login/KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
|
||||
export type UserProfileFormFieldsProps = {
|
||||
kcContext: KcContextLike;
|
||||
i18n: I18n;
|
||||
getClassName: (classKey: ClassKey) => string;
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
BeforeField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||
AfterField?: (props: BeforeAfterFieldProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
type BeforeAfterFieldProps = {
|
||||
attribute: Attribute;
|
||||
dispatchFormAction: React.Dispatch<FormAction>;
|
||||
displayableErrors: FormFieldError[];
|
||||
i18n: I18n;
|
||||
valueOrValues: string | string[];
|
||||
};
|
||||
|
||||
// NOTE: Enabled by default but it's a UX best practice to set it to false.
|
||||
const doMakeUserConfirmPassword = true;
|
||||
|
||||
export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
|
||||
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
const {
|
||||
formState: { formFieldStates, isFormSubmittable },
|
||||
dispatchFormAction
|
||||
} = useUserProfileForm({
|
||||
kcContext,
|
||||
i18n,
|
||||
doMakeUserConfirmPassword
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||
}, [isFormSubmittable]);
|
||||
|
||||
const groupNameRef = { current: "" };
|
||||
|
||||
return (
|
||||
<>
|
||||
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
||||
return (
|
||||
<Fragment key={attribute.name}>
|
||||
<GroupLabel attribute={attribute} getClassName={getClassName} i18n={i18n} groupNameRef={groupNameRef} />
|
||||
{BeforeField !== undefined && (
|
||||
<BeforeField
|
||||
attribute={attribute}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
displayableErrors={displayableErrors}
|
||||
i18n={i18n}
|
||||
valueOrValues={valueOrValues}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className={getClassName("kcFormGroupClass")}
|
||||
style={{
|
||||
display: attribute.name === "password-confirm" && !doMakeUserConfirmPassword ? "none" : undefined
|
||||
}}
|
||||
>
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label htmlFor={attribute.name} className={getClassName("kcLabelClass")}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={getClassName("kcInputWrapperClass")}>
|
||||
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
||||
<div
|
||||
className={getClassName("kcInputHelperTextBeforeClass")}
|
||||
id={`form-help-text-before-${attribute.name}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{advancedMsg(attribute.annotations.inputHelperTextBefore)}
|
||||
</div>
|
||||
)}
|
||||
<InputFiledByType
|
||||
attribute={attribute}
|
||||
valueOrValues={valueOrValues}
|
||||
displayableErrors={displayableErrors}
|
||||
formValidationDispatch={dispatchFormAction}
|
||||
getClassName={getClassName}
|
||||
i18n={i18n}
|
||||
/>
|
||||
<FieldErrors
|
||||
attribute={attribute}
|
||||
getClassName={getClassName}
|
||||
displayableErrors={displayableErrors}
|
||||
fieldIndex={undefined}
|
||||
/>
|
||||
{attribute.annotations.inputHelperTextAfter !== undefined && (
|
||||
<div
|
||||
className={getClassName("kcInputHelperTextAfterClass")}
|
||||
id={`form-help-text-after-${attribute.name}`}
|
||||
aria-live="polite"
|
||||
>
|
||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{AfterField !== undefined && (
|
||||
<AfterField
|
||||
attribute={attribute}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
displayableErrors={displayableErrors}
|
||||
i18n={i18n}
|
||||
valueOrValues={valueOrValues}
|
||||
/>
|
||||
)}
|
||||
{/* NOTE: Downloading of html5DataAnnotations scripts is done in the useUserProfileForm hook */}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupLabel(props: {
|
||||
attribute: Attribute;
|
||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
||||
i18n: I18n;
|
||||
groupNameRef: {
|
||||
current: string;
|
||||
};
|
||||
}) {
|
||||
const { attribute, getClassName, i18n, groupNameRef } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
if (attribute.group?.name !== groupNameRef.current) {
|
||||
groupNameRef.current = attribute.group?.name ?? "";
|
||||
|
||||
if (groupNameRef.current !== "") {
|
||||
assert(attribute.group !== undefined);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={getClassName("kcFormGroupClass")}
|
||||
{...Object.fromEntries(Object.entries(attribute.group.html5DataAnnotations).map(([key, value]) => [`data-${key}`, value]))}
|
||||
>
|
||||
{(() => {
|
||||
const groupDisplayHeader = attribute.group.displayHeader ?? "";
|
||||
const groupHeaderText = groupDisplayHeader !== "" ? advancedMsg(groupDisplayHeader) : attribute.group.name;
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcContentWrapperClass")}>
|
||||
<label id={`header-${attribute.group.name}`} className={getClassName("kcFormGroupHeader")}>
|
||||
{groupHeaderText}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
{(() => {
|
||||
const groupDisplayDescription = attribute.group.displayDescription ?? "";
|
||||
|
||||
if (groupDisplayDescription !== "") {
|
||||
const groupDescriptionText = advancedMsg(groupDisplayDescription);
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcLabelWrapperClass")}>
|
||||
<label id={`description-${attribute.group.name}`} className={getClassName("kcLabelClass")}>
|
||||
{groupDescriptionText}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function FieldErrors(props: {
|
||||
attribute: Attribute;
|
||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
||||
displayableErrors: FormFieldError[];
|
||||
fieldIndex: number | undefined;
|
||||
}) {
|
||||
const { attribute, getClassName, fieldIndex } = props;
|
||||
|
||||
const displayableErrors = props.displayableErrors.filter(error => error.fieldIndex === fieldIndex);
|
||||
|
||||
if (displayableErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`}
|
||||
className={getClassName("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors
|
||||
.filter(error => error.fieldIndex === fieldIndex)
|
||||
.map(({ errorMessage }, i, arr) => (
|
||||
<Fragment key={i}>
|
||||
<span key={i}>{errorMessage}</span>
|
||||
{arr.length - 1 !== i && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type InputFiledByTypeProps = {
|
||||
attribute: Attribute;
|
||||
valueOrValues: string | string[];
|
||||
displayableErrors: FormFieldError[];
|
||||
formValidationDispatch: React.Dispatch<FormAction>;
|
||||
getClassName: UserProfileFormFieldsProps["getClassName"];
|
||||
i18n: I18n;
|
||||
};
|
||||
|
||||
function InputFiledByType(props: InputFiledByTypeProps) {
|
||||
const { attribute, valueOrValues } = props;
|
||||
|
||||
switch (attribute.annotations.inputType) {
|
||||
case "textarea":
|
||||
return <TextareaTag {...props} />;
|
||||
case "select":
|
||||
case "multiselect":
|
||||
return <SelectTag {...props} />;
|
||||
case "select-radiobuttons":
|
||||
case "multiselect-checkboxes":
|
||||
return <InputTagSelects {...props} />;
|
||||
default: {
|
||||
if (valueOrValues instanceof Array) {
|
||||
return (
|
||||
<>
|
||||
{valueOrValues.map((...[, i]) => (
|
||||
<InputTag key={i} {...props} fieldIndex={i} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const inputNode = <InputTag {...props} fieldIndex={undefined} />;
|
||||
|
||||
if (attribute.name === "password" || attribute.name === "password-confirm") {
|
||||
return (
|
||||
<PasswordWrapper getClassName={props.getClassName} i18n={props.i18n} passwordInputId={attribute.name}>
|
||||
{inputNode}
|
||||
</PasswordWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return inputNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PasswordWrapper(props: { getClassName: (classKey: ClassKey) => string; i18n: I18n; passwordInputId: string; children: JSX.Element }) {
|
||||
const { getClassName, i18n, passwordInputId, children } = props;
|
||||
|
||||
const { msgStr } = i18n;
|
||||
|
||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
||||
|
||||
useEffect(() => {
|
||||
const passwordInputElement = document.getElementById(passwordInputId);
|
||||
|
||||
assert(passwordInputElement instanceof HTMLInputElement);
|
||||
|
||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
||||
}, [isPasswordRevealed]);
|
||||
|
||||
return (
|
||||
<div className={getClassName("kcInputGroup")}>
|
||||
{children}
|
||||
<button
|
||||
type="button"
|
||||
className={getClassName("kcFormPasswordVisibilityButtonClass")}
|
||||
aria-label={msgStr(isPasswordRevealed ? "hidePassword" : "showPassword")}
|
||||
aria-controls={passwordInputId}
|
||||
onClick={toggleIsPasswordRevealed}
|
||||
>
|
||||
<i
|
||||
className={getClassName(isPasswordRevealed ? "kcFormPasswordVisibilityIconHide" : "kcFormPasswordVisibilityIconShow")}
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefined }) {
|
||||
const { attribute, fieldIndex, getClassName, formValidationDispatch, valueOrValues, i18n, displayableErrors } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type={(() => {
|
||||
const { inputType } = attribute.annotations;
|
||||
|
||||
if (inputType?.startsWith("html5-")) {
|
||||
return inputType.slice(6);
|
||||
}
|
||||
|
||||
return inputType ?? "text";
|
||||
})()}
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={(() => {
|
||||
if (fieldIndex !== undefined) {
|
||||
assert(valueOrValues instanceof Array);
|
||||
return valueOrValues[fieldIndex];
|
||||
}
|
||||
|
||||
assert(typeof valueOrValues === "string");
|
||||
|
||||
return valueOrValues;
|
||||
})()}
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={displayableErrors.find(error => error.fieldIndex === fieldIndex) !== undefined}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
placeholder={attribute.annotations.inputTypePlaceholder}
|
||||
pattern={attribute.annotations.inputTypePattern}
|
||||
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)}
|
||||
maxLength={
|
||||
attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`)
|
||||
}
|
||||
minLength={
|
||||
attribute.annotations.inputTypeMinlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMinlength}`)
|
||||
}
|
||||
max={attribute.annotations.inputTypeMax}
|
||||
min={attribute.annotations.inputTypeMin}
|
||||
step={attribute.annotations.inputTypeStep}
|
||||
{...Object.fromEntries(Object.entries(attribute.html5DataAnnotations ?? {}).map(([key, value]) => [`data-${key}`, value]))}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: (() => {
|
||||
if (fieldIndex !== undefined) {
|
||||
assert(valueOrValues instanceof Array);
|
||||
|
||||
return valueOrValues.map((value, i) => {
|
||||
if (i === fieldIndex) {
|
||||
return event.target.value;
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
return event.target.value;
|
||||
})()
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
props.formValidationDispatch({
|
||||
action: "focus lost",
|
||||
name: attribute.name,
|
||||
fieldIndex: fieldIndex
|
||||
})
|
||||
}
|
||||
/>
|
||||
{(() => {
|
||||
if (fieldIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assert(valueOrValues instanceof Array);
|
||||
|
||||
const values = valueOrValues;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldErrors
|
||||
attribute={attribute}
|
||||
getClassName={getClassName}
|
||||
displayableErrors={displayableErrors}
|
||||
fieldIndex={fieldIndex}
|
||||
/>
|
||||
<AddRemoveButtonsMultiValuedAttribute
|
||||
attribute={attribute}
|
||||
values={values}
|
||||
fieldIndex={fieldIndex}
|
||||
dispatchFormAction={formValidationDispatch}
|
||||
i18n={i18n}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AddRemoveButtonsMultiValuedAttribute(props: {
|
||||
attribute: Attribute;
|
||||
values: string[];
|
||||
fieldIndex: number;
|
||||
dispatchFormAction: React.Dispatch<Extract<FormAction, { action: "update" }>>;
|
||||
i18n: I18n;
|
||||
}) {
|
||||
const { attribute, values, fieldIndex, dispatchFormAction, i18n } = props;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
const { hasAdd, hasRemove } = getButtonToDisplayForMultivaluedAttributeField({ attribute, values, fieldIndex });
|
||||
|
||||
const idPostfix = `-${attribute.name}-${fieldIndex + 1}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
{hasRemove && (
|
||||
<>
|
||||
<button
|
||||
id={`kc-remove${idPostfix}`}
|
||||
type="button"
|
||||
className="pf-c-button pf-m-inline pf-m-link"
|
||||
onClick={() =>
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: values.filter((_, i) => i !== fieldIndex)
|
||||
})
|
||||
}
|
||||
>
|
||||
{msg("remove")}
|
||||
</button>
|
||||
{hasAdd ? <> | </> : null}
|
||||
</>
|
||||
)}
|
||||
{hasAdd && (
|
||||
<button
|
||||
id={`kc-add${idPostfix}`}
|
||||
type="button"
|
||||
className="pf-c-button pf-m-inline pf-m-link"
|
||||
onClick={() =>
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: [...values, ""]
|
||||
})
|
||||
}
|
||||
>
|
||||
{msg("addValue")}
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function InputTagSelects(props: InputFiledByTypeProps) {
|
||||
const { attribute, formValidationDispatch, getClassName, valueOrValues } = props;
|
||||
|
||||
const { advancedMsg } = props.i18n;
|
||||
|
||||
const { classDiv, classInput, classLabel, inputType } = (() => {
|
||||
const { inputType } = attribute.annotations;
|
||||
|
||||
assert(inputType === "select-radiobuttons" || inputType === "multiselect-checkboxes");
|
||||
|
||||
switch (inputType) {
|
||||
case "select-radiobuttons":
|
||||
return {
|
||||
inputType: "radio",
|
||||
classDiv: getClassName("kcInputClassRadio"),
|
||||
classInput: getClassName("kcInputClassRadioInput"),
|
||||
classLabel: getClassName("kcInputClassRadioLabel")
|
||||
};
|
||||
case "multiselect-checkboxes":
|
||||
return {
|
||||
inputType: "checkbox",
|
||||
classDiv: getClassName("kcInputClassCheckbox"),
|
||||
classInput: getClassName("kcInputClassCheckboxInput"),
|
||||
classLabel: getClassName("kcInputClassCheckboxLabel")
|
||||
};
|
||||
}
|
||||
})();
|
||||
|
||||
const options = (() => {
|
||||
walk: {
|
||||
const { inputOptionsFromValidation } = attribute.annotations;
|
||||
|
||||
if (inputOptionsFromValidation === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
const validator = (attribute.validators as Record<string, { options?: string[] }>)[inputOptionsFromValidation];
|
||||
|
||||
if (validator === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
if (validator.options === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
return validator.options;
|
||||
}
|
||||
|
||||
return attribute.validators.options?.options ?? [];
|
||||
})();
|
||||
|
||||
return (
|
||||
<>
|
||||
{options.map(option => (
|
||||
<div key={option} className={classDiv}>
|
||||
<input
|
||||
type={inputType}
|
||||
id={`${attribute.name}-${option}`}
|
||||
name={attribute.name}
|
||||
value={option}
|
||||
className={classInput}
|
||||
aria-invalid={props.displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
checked={valueOrValues instanceof Array ? valueOrValues.includes(option) : valueOrValues === option}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: (() => {
|
||||
const isChecked = event.target.checked;
|
||||
|
||||
if (valueOrValues instanceof Array) {
|
||||
const newValues = [...valueOrValues];
|
||||
|
||||
if (isChecked) {
|
||||
newValues.push(option);
|
||||
} else {
|
||||
newValues.splice(newValues.indexOf(option), 1);
|
||||
}
|
||||
|
||||
return newValues;
|
||||
}
|
||||
|
||||
return event.target.checked ? option : "";
|
||||
})()
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
action: "focus lost",
|
||||
name: attribute.name,
|
||||
fieldIndex: undefined
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${attribute.name}-${option}`}
|
||||
className={`${classLabel}${attribute.readOnly ? ` ${getClassName("kcInputClassRadioCheckboxLabelDisabled")}` : ""}`}
|
||||
>
|
||||
{advancedMsg(option)}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TextareaTag(props: InputFiledByTypeProps) {
|
||||
const { attribute, formValidationDispatch, getClassName, displayableErrors, valueOrValues } = props;
|
||||
|
||||
assert(typeof valueOrValues === "string");
|
||||
|
||||
const value = valueOrValues;
|
||||
|
||||
return (
|
||||
<textarea
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
cols={attribute.annotations.inputTypeCols === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeCols}`)}
|
||||
rows={attribute.annotations.inputTypeRows === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeRows}`)}
|
||||
maxLength={attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`)}
|
||||
value={value}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
action: "focus lost",
|
||||
name: attribute.name,
|
||||
fieldIndex: undefined
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTag(props: InputFiledByTypeProps) {
|
||||
const { attribute, formValidationDispatch, getClassName, displayableErrors, i18n, valueOrValues } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
const isMultiple = attribute.annotations.inputType === "multiselect";
|
||||
|
||||
return (
|
||||
<select
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
className={getClassName("kcInputClass")}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
multiple={isMultiple}
|
||||
size={attribute.annotations.inputTypeSize === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeSize}`)}
|
||||
value={valueOrValues}
|
||||
onChange={event =>
|
||||
formValidationDispatch({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: (() => {
|
||||
if (isMultiple) {
|
||||
return Array.from(event.target.selectedOptions).map(option => option.value);
|
||||
}
|
||||
|
||||
return event.target.value;
|
||||
})()
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
formValidationDispatch({
|
||||
action: "focus lost",
|
||||
name: attribute.name,
|
||||
fieldIndex: undefined
|
||||
})
|
||||
}
|
||||
>
|
||||
{!isMultiple && <option value=""></option>}
|
||||
{(() => {
|
||||
const options = (() => {
|
||||
walk: {
|
||||
const { inputOptionsFromValidation } = attribute.annotations;
|
||||
|
||||
if (inputOptionsFromValidation === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
assert(typeof inputOptionsFromValidation === "string");
|
||||
|
||||
const validator = (attribute.validators as Record<string, { options?: string[] }>)[inputOptionsFromValidation];
|
||||
|
||||
if (validator === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
if (validator.options === undefined) {
|
||||
break walk;
|
||||
}
|
||||
|
||||
return validator.options;
|
||||
}
|
||||
|
||||
return attribute.validators.options?.options ?? [];
|
||||
})();
|
||||
|
||||
return options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{(() => {
|
||||
if (attribute.annotations.inputOptionLabels !== undefined) {
|
||||
const { inputOptionLabels } = attribute.annotations;
|
||||
|
||||
return advancedMsg(inputOptionLabels[option] ?? option);
|
||||
}
|
||||
|
||||
if (attribute.annotations.inputOptionLabelsI18nPrefix !== undefined) {
|
||||
return advancedMsg(`${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}`);
|
||||
}
|
||||
|
||||
return option;
|
||||
})()}
|
||||
</option>
|
||||
));
|
||||
})()}
|
||||
</select>
|
||||
);
|
||||
}
|
5
src/login/i18n.ts
Normal file
5
src/login/i18n.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createUseI18n } from "keycloakify/login";
|
||||
|
||||
export const { useI18n, ofTypeI18n } = createUseI18n({});
|
||||
|
||||
export type I18n = typeof ofTypeI18n;
|
183
src/login/pages/Login.stories.tsx
Normal file
183
src/login/pages/Login.stories.tsx
Normal file
|
@ -0,0 +1,183 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createPageStory } from "../PageStory";
|
||||
|
||||
const pageId = "login.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WithoutRegistration: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { registrationAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutRememberMe: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { rememberMe: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordReset: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { resetPasswordAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { loginWithEmailAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPresetUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
login: { username: "max.mustermann@mail.com" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithImmutablePresetUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
auth: {
|
||||
attemptedUsername: "max.mustermann@mail.com",
|
||||
showUsername: true
|
||||
},
|
||||
usernameHidden: true,
|
||||
message: {
|
||||
type: "info",
|
||||
summary: "Please re-authenticate to continue"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithSocialProviders: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft"
|
||||
},
|
||||
{
|
||||
loginUrl: "facebook",
|
||||
alias: "facebook",
|
||||
providerId: "facebook",
|
||||
displayName: "Facebook"
|
||||
},
|
||||
{
|
||||
loginUrl: "instagram",
|
||||
alias: "instagram",
|
||||
providerId: "instagram",
|
||||
displayName: "Instagram"
|
||||
},
|
||||
{
|
||||
loginUrl: "twitter",
|
||||
alias: "twitter",
|
||||
providerId: "twitter",
|
||||
displayName: "Twitter"
|
||||
},
|
||||
{
|
||||
loginUrl: "linkedin",
|
||||
alias: "linkedin",
|
||||
providerId: "linkedin",
|
||||
displayName: "LinkedIn"
|
||||
},
|
||||
{
|
||||
loginUrl: "stackoverflow",
|
||||
alias: "stackoverflow",
|
||||
providerId: "stackoverflow",
|
||||
displayName: "Stackoverflow"
|
||||
},
|
||||
{
|
||||
loginUrl: "github",
|
||||
alias: "github",
|
||||
providerId: "github",
|
||||
displayName: "Github"
|
||||
},
|
||||
{
|
||||
loginUrl: "gitlab",
|
||||
alias: "gitlab",
|
||||
providerId: "gitlab",
|
||||
displayName: "Gitlab"
|
||||
},
|
||||
{
|
||||
loginUrl: "bitbucket",
|
||||
alias: "bitbucket",
|
||||
providerId: "bitbucket",
|
||||
displayName: "Bitbucket"
|
||||
},
|
||||
{
|
||||
loginUrl: "paypal",
|
||||
alias: "paypal",
|
||||
providerId: "paypal",
|
||||
displayName: "PayPal"
|
||||
},
|
||||
{
|
||||
loginUrl: "openshift",
|
||||
alias: "openshift",
|
||||
providerId: "openshift",
|
||||
displayName: "OpenShift"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordField: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: { password: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
116
src/login/pages/Register.stories.tsx
Normal file
116
src/login/pages/Register.stories.tsx
Normal file
|
@ -0,0 +1,116 @@
|
|||
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createPageStory } from "../PageStory";
|
||||
|
||||
const pageId = "register.ftl";
|
||||
|
||||
const { PageStory } = createPageStory({ pageId });
|
||||
|
||||
const meta = {
|
||||
title: `login/${pageId}`,
|
||||
component: PageStory
|
||||
} satisfies Meta<typeof PageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PageStory />
|
||||
};
|
||||
|
||||
export const WithFieldError: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
email: {
|
||||
value: "max.mustermann@gmail.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (fieldName: string) => fieldName === "email",
|
||||
exists: (fieldName: string) => fieldName === "email",
|
||||
get: (fieldName: string) => (fieldName === "email" ? "I don't like your email address" : undefined),
|
||||
printIfExists: <T,>(fieldName: string, x: T) => (fieldName === "email" ? x : undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
registrationEmailAsUsername: true
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPassword: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
passwordRequired: false
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRecaptcha: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
scripts: ["https://www.google.com/recaptcha/api.js?hl=en"],
|
||||
recaptchaRequired: true,
|
||||
recaptchaSiteKey: "6LfQHvApAAAAAE73SYTd5vS0lB1Xr7zdiQ-6iBVa"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRecaptchaFrench: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
scripts: ["https://www.google.com/recaptcha/api.js?hl=fr"],
|
||||
recaptchaRequired: true,
|
||||
recaptchaSiteKey: "6LfQHvApAAAAAE73SYTd5vS0lB1Xr7zdiQ-6iBVa"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPresets: Story = {
|
||||
render: () => (
|
||||
<PageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
firstName: {
|
||||
value: "Max"
|
||||
},
|
||||
lastName: {
|
||||
value: "Mustermann"
|
||||
},
|
||||
email: {
|
||||
value: "max.mustermann@gmail.com"
|
||||
},
|
||||
username: {
|
||||
value: "max.mustermann"
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
33
src/main.tsx
33
src/main.tsx
|
@ -1,33 +1,22 @@
|
|||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { StrictMode, lazy, Suspense } from "react";
|
||||
import { kcContext as kcLoginThemeContext } from "./keycloak-theme/login/kcContext";
|
||||
import { kcContext as kcAccountThemeContext } from "./keycloak-theme/account/kcContext";
|
||||
//import { getKcContextMock } from "./login/PageStory";
|
||||
//const kcContext = getKcContextMock({ pageId: "register.ftl", overrides: {} });
|
||||
const { kcContext } = window;
|
||||
|
||||
const KcLoginThemeApp = lazy(() => import("./keycloak-theme/login/KcApp"));
|
||||
const KcAccountThemeApp = lazy(() => import("./keycloak-theme/account/KcApp"));
|
||||
// Important note:
|
||||
// In this starter example we show how you can have your react app and your Keycloak theme in the same repo.
|
||||
// Most Keycloakify user only want to great a Keycloak theme.
|
||||
// If this is your case run the few commands that will remover everything that is not strictly related to the
|
||||
//Keycloak theme:
|
||||
// https://github.com/keycloakify/keycloakify-starter?tab=readme-ov-file#i-only-want-a-keycloak-theme
|
||||
const App = lazy(() => import("./App"));
|
||||
const KcLoginThemeApp = lazy(() => import("./login/KcApp"));
|
||||
const KcAccountThemeApp = lazy(() => import("./account/KcApp"));
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<Suspense>
|
||||
{(()=>{
|
||||
|
||||
if( kcLoginThemeContext !== undefined ){
|
||||
return <KcLoginThemeApp kcContext={kcLoginThemeContext} />;
|
||||
{(() => {
|
||||
switch (kcContext?.themeType) {
|
||||
case "login": return <KcLoginThemeApp kcContext={kcContext} />;
|
||||
case "account": return <KcAccountThemeApp kcContext={kcContext} />;
|
||||
case undefined: return <h1>No Keycloak Context</h1>;
|
||||
}
|
||||
|
||||
if( kcAccountThemeContext !== undefined ){
|
||||
return <KcAccountThemeApp kcContext={kcAccountThemeContext} />;
|
||||
}
|
||||
|
||||
return <App />;
|
||||
|
||||
})()}
|
||||
</Suspense>
|
||||
</StrictMode>
|
||||
|
|
11
src/vite-env.d.ts
vendored
11
src/vite-env.d.ts
vendored
|
@ -1,6 +1,11 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.md" {
|
||||
const src: string;
|
||||
export default src;
|
||||
import type { KcContext as KcContextLogin } from "./login/kcContext";
|
||||
import type { KcContext as KcContextAccount } from "./account/kcContext";
|
||||
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
kcContext?: KcContextLogin | KcContextAccount;
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
// NOTE: This is just for the Keycloakify core contributors to be able to dynamically link
|
||||
// to a local version of the keycloakify package. This is not needed for normal usage.
|
||||
import commonjs from "vite-plugin-commonjs";
|
||||
import { keycloakify } from "keycloakify/vite-plugin";
|
||||
|
||||
|
||||
|
@ -10,47 +7,8 @@ import { keycloakify } from "keycloakify/vite-plugin";
|
|||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
commonjs(),
|
||||
keycloakify({
|
||||
// See: https://docs.keycloakify.dev/build-options#themename
|
||||
themeName: "keycloakify-starter",
|
||||
// See: https://docs.keycloakify.dev/environnement-variables
|
||||
extraThemeProperties: [
|
||||
"MY_ENV_VARIABLE=${env.MY_ENV_VARIABLE:}"
|
||||
],
|
||||
// This is a hook that will be called after the build is done
|
||||
// but before the jar is created.
|
||||
// You can use it to add/remove/edit your theme files.
|
||||
postBuild: async keycloakifyBuildOptions => {
|
||||
|
||||
const fs = await import("fs/promises");
|
||||
const path = await import("path");
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(keycloakifyBuildOptions.keycloakifyBuildDirPath, "foo.txt"),
|
||||
Buffer.from(
|
||||
[
|
||||
"This file was created by the postBuild hook of the keycloakify vite plugin",
|
||||
"",
|
||||
"Resolved keycloakifyBuildOptions:",
|
||||
"",
|
||||
JSON.stringify(keycloakifyBuildOptions, null, 2),
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
}
|
||||
})
|
||||
keycloakify()
|
||||
],
|
||||
/*
|
||||
* Uncomment this if you want to use the default domain provided by GitHub Pages
|
||||
* replace "keycloakify-starter" with your repository name.
|
||||
* This is only relevent if you are building an Wep App + A Keycloak theme.
|
||||
* If you are only building a Keycloak theme, you can ignore this.
|
||||
*/
|
||||
//base: "/keycloakify-starter/"
|
||||
build: {
|
||||
sourcemap: true
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue