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 }}
|
to_version: ${{ steps.step1.outputs.to_version }}
|
||||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||||
steps:
|
steps:
|
||||||
- uses: garronej/ts-ci@v2.1.0
|
- uses: garronej/ts-ci@v2.1.2
|
||||||
id: step1
|
id: step1
|
||||||
with:
|
with:
|
||||||
action_name: is_package_json_version_upgraded
|
action_name: is_package_json_version_upgraded
|
||||||
@ -60,50 +60,3 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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 charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
|
||||||
<!--
|
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||||
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">
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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",
|
"name": "keycloakify-starter",
|
||||||
"homepage": "https://starter.keycloakify.dev",
|
|
||||||
"version": "6.1.10",
|
"version": "6.1.10",
|
||||||
"description": "A starter/demo project for keycloakify",
|
"description": "Starter for Keycloakify 10",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||||
@ -15,18 +14,12 @@
|
|||||||
"storybook": "storybook dev -p 6006",
|
"storybook": "storybook dev -p 6006",
|
||||||
"build-storybook": "storybook build"
|
"build-storybook": "storybook build"
|
||||||
},
|
},
|
||||||
"author": "u/garronej",
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"evt": "^2.5.7",
|
"keycloakify": "10.0.0-rc.31",
|
||||||
"keycloakify": "^9.6.6",
|
|
||||||
"oidc-spa": "^4.6.2",
|
|
||||||
"powerhooks": "^1.0.8",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0"
|
||||||
"tsafe": "^1.6.6",
|
|
||||||
"zod": "^3.22.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-essentials": "^8.0.2",
|
"@storybook/addon-essentials": "^8.0.2",
|
||||||
@ -48,8 +41,7 @@
|
|||||||
"eslint-plugin-storybook": "^0.8.0",
|
"eslint-plugin-storybook": "^0.8.0",
|
||||||
"storybook": "^8.0.2",
|
"storybook": "^8.0.2",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.2.2",
|
||||||
"vite": "^5.0.8",
|
"vite": "^5.0.8"
|
||||||
"vite-plugin-commonjs": "^0.10.1"
|
|
||||||
},
|
},
|
||||||
"_comment": "See https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092",
|
"_comment": "See https://github.com/storybookjs/storybook/issues/22431#issuecomment-1630086092",
|
||||||
"resolutions": {
|
"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.0 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 { useEffect } from "react";
|
||||||
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 { assert } from "keycloakify/tools/assert";
|
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>) {
|
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||||
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||||
|
|
||||||
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
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 { locale, url, features, realm, message, referrer } = kcContext;
|
||||||
|
|
||||||
const { isReady } = usePrepareTemplate({
|
useEffect(() => {
|
||||||
"doFetchDefaultThemeResources": doUseDefaultCss,
|
document.title = msgStr("accountManagementTitle");
|
||||||
"styles": [
|
}, []);
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`,
|
|
||||||
`${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`,
|
useSetClassName({
|
||||||
`${url.resourcesPath}/css/account.css`
|
qualifiedName: "html",
|
||||||
],
|
className: getClassName("kcHtmlClass")
|
||||||
"htmlClassName": getClassName("kcHtmlClass"),
|
|
||||||
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass")),
|
|
||||||
"htmlLangProperty": locale?.currentLanguageTag,
|
|
||||||
"documentTitle": i18n.msgStr("accountManagementTitle")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,9 +81,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
<ul>
|
<ul>
|
||||||
{locale.supported.map(({ languageTag }) => (
|
{locale.supported.map(({ languageTag }) => (
|
||||||
<li key={languageTag} className="kc-dropdown-item">
|
<li key={languageTag} className="kc-dropdown-item">
|
||||||
<a href="#" onClick={() => changeLocale(languageTag)}>
|
<a href={getChangeLocalUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
|
||||||
{labelBySupportedLanguageTag[languageTag]}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</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 { createRoot } from "react-dom/client";
|
||||||
import { StrictMode, lazy, Suspense } from "react";
|
import { StrictMode, lazy, Suspense } from "react";
|
||||||
import { kcContext as kcLoginThemeContext } from "./keycloak-theme/login/kcContext";
|
//import { getKcContextMock } from "./login/PageStory";
|
||||||
import { kcContext as kcAccountThemeContext } from "./keycloak-theme/account/kcContext";
|
//const kcContext = getKcContextMock({ pageId: "register.ftl", overrides: {} });
|
||||||
|
const { kcContext } = window;
|
||||||
|
|
||||||
const KcLoginThemeApp = lazy(() => import("./keycloak-theme/login/KcApp"));
|
const KcLoginThemeApp = lazy(() => import("./login/KcApp"));
|
||||||
const KcAccountThemeApp = lazy(() => import("./keycloak-theme/account/KcApp"));
|
const KcAccountThemeApp = lazy(() => import("./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"));
|
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{(()=>{
|
{(() => {
|
||||||
|
switch (kcContext?.themeType) {
|
||||||
if( kcLoginThemeContext !== undefined ){
|
case "login": return <KcLoginThemeApp kcContext={kcContext} />;
|
||||||
return <KcLoginThemeApp kcContext={kcLoginThemeContext} />;
|
case "account": return <KcAccountThemeApp kcContext={kcContext} />;
|
||||||
|
case undefined: return <h1>No Keycloak Context</h1>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if( kcAccountThemeContext !== undefined ){
|
|
||||||
return <KcAccountThemeApp kcContext={kcAccountThemeContext} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <App />;
|
|
||||||
|
|
||||||
})()}
|
})()}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
|
11
src/vite-env.d.ts
vendored
11
src/vite-env.d.ts
vendored
@ -1,6 +1,11 @@
|
|||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module "*.md" {
|
import type { KcContext as KcContextLogin } from "./login/kcContext";
|
||||||
const src: string;
|
import type { KcContext as KcContextAccount } from "./account/kcContext";
|
||||||
export default src;
|
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
kcContext?: KcContextLogin | KcContextAccount;
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,8 +1,5 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
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";
|
import { keycloakify } from "keycloakify/vite-plugin";
|
||||||
|
|
||||||
|
|
||||||
@ -10,47 +7,8 @@ import { keycloakify } from "keycloakify/vite-plugin";
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
commonjs(),
|
keycloakify()
|
||||||
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"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
],
|
],
|
||||||
/*
|
|
||||||
* 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: {
|
build: {
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user