Compare commits
No commits in common. "430849a00b4e04b6999d01b8f37141ad958759b6" and "0882589f3e20ebf96d0a7c69e886112da18e7962" have entirely different histories.
430849a00b
...
0882589f3e
@ -3,11 +3,8 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="https://cdn.tombutcher.work/favicon/favicon-auth.ico" />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href="https://cdn.tombutcher.work/favicon/favicon-auth192.png"
|
||||
/>
|
||||
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
7853
package-lock.json
generated
23
package.json
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "auth-tombutcher-work",
|
||||
"name": "keycloakify-starter",
|
||||
"version": "0.0.0",
|
||||
"description": "auth.tombutcher.work Theme",
|
||||
"description": "Starter for Keycloakify 11",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
|
||||
@ -17,21 +17,15 @@
|
||||
"license": "MIT",
|
||||
"keywords": [],
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^5.6.1",
|
||||
"@tsparticles/engine": "^3.8.1",
|
||||
"@tsparticles/react": "^3.0.0",
|
||||
"@tsparticles/slim": "^3.8.1",
|
||||
"antd": "^5.24.3",
|
||||
"keycloakify": "^11.8.17",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-responsive": "^10.0.1"
|
||||
"keycloakify": "^11.8.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.15.0",
|
||||
"@storybook/react": "^8.6.6",
|
||||
"@storybook/react-vite": "^8.6.6",
|
||||
"storybook": "^8.1.10",
|
||||
"@storybook/react": "^8.1.10",
|
||||
"@storybook/react-vite": "^8.1.10",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
@ -45,7 +39,6 @@
|
||||
"eslint-plugin-storybook": "^0.11.1",
|
||||
"globals": "^15.12.0",
|
||||
"prettier": "3.3.1",
|
||||
"storybook": "^8.6.6",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript-eslint": "^8.15.0",
|
||||
"vite": "^5.0.8"
|
||||
|
||||
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 23.569 23.2312">
|
||||
<g>
|
||||
<rect height="23.2312" opacity="0" width="23.569" x="0" y="0"/>
|
||||
<path d="M1.14421 7.83303C1.89608 7.83303 2.29592 7.41186 2.29592 6.66749L2.29592 4.01999C2.29592 2.88139 2.88772 2.31936 3.97733 2.31936L6.6846 2.31936C7.44069 2.31936 7.85014 1.9078 7.85014 1.16554C7.85014 0.423278 7.44069 0.0234375 6.6846 0.0234375L3.84421 0.0234375C1.31484 0.0234375 0 1.31695 0 3.8196L0 6.66749C0 7.41397 0.41156 7.83303 1.14421 7.83303ZM22.0657 7.83303C22.8176 7.83303 23.2174 7.41397 23.2174 6.66749L23.2174 3.8196C23.2174 1.31695 21.9047 0.0234375 19.3732 0.0234375L16.5232 0.0234375C15.7788 0.0234375 15.3673 0.423278 15.3673 1.16554C15.3673 1.9078 15.7809 2.31936 16.5232 2.31936L19.2305 2.31936C20.3105 2.31936 20.9215 2.88139 20.9215 4.01999L20.9215 6.66749C20.9215 7.41397 21.333 7.83303 22.0657 7.83303ZM3.84421 23.2312L6.6846 23.2312C7.44069 23.2312 7.85014 22.8197 7.85014 22.087C7.85014 21.3448 7.43647 20.9353 6.6846 20.9353L3.97733 20.9353C2.88772 20.9353 2.29592 20.3733 2.29592 19.2326L2.29592 16.5851C2.29592 15.8311 1.88647 15.4195 1.14421 15.4195C0.399841 15.4195 0 15.8311 0 16.5851L0 19.4255C0 21.9356 1.31484 23.2312 3.84421 23.2312ZM16.5232 23.2312L19.3732 23.2312C21.9047 23.2312 23.2174 21.926 23.2174 19.4255L23.2174 16.5851C23.2174 15.8311 22.808 15.4195 22.0657 15.4195C21.3234 15.4195 20.9215 15.8332 20.9215 16.5851L20.9215 19.2326C20.9215 20.3733 20.3105 20.9353 19.2305 20.9353L16.5232 20.9353C15.7788 20.9353 15.3673 21.3448 15.3673 22.087C15.3673 22.8197 15.7788 23.2312 16.5232 23.2312Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
<path d="M11.6173 4.61436C9.45984 4.61436 7.7414 6.34241 7.7414 8.4764C7.7414 10.0875 8.68664 11.4886 10.1487 12.0809L10.1487 17.869C10.1583 18.0382 10.2253 18.1828 10.3552 18.3084L11.3672 19.309C11.5172 19.4473 11.7255 19.459 11.8798 19.3027L13.7606 17.4314C13.9329 17.2687 13.9244 17.0327 13.7585 16.8818L12.6666 15.7814L14.2029 14.2643C14.3517 14.123 14.3442 13.8827 14.1837 13.7105L12.6919 12.2091C14.4816 11.4569 15.476 10.1037 15.476 8.4764C15.476 6.35202 13.7459 4.61436 11.6173 4.61436ZM11.6077 6.47202C12.2512 6.47202 12.7744 7.00053 12.7744 7.64835C12.7744 8.30905 12.2512 8.84179 11.6077 8.84179C10.9662 8.84179 10.4238 8.30905 10.4238 7.64835C10.4238 7.00053 10.9566 6.47202 11.6077 6.47202Z" fill="#5e5ce6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.5 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 19.262 11.8418">
|
||||
<g>
|
||||
<rect height="11.8418" opacity="0" width="19.262" x="0" y="0"/>
|
||||
<path d="M9.39394 11.8418C14.9986 11.8418 18.7835 7.32375 18.7835 5.92552C18.7835 4.52416 14.993 0.0117576 9.39394 0.0117576C3.87065 0.0117576 0 4.52416 0 5.92552C0 7.32375 3.86504 11.8418 9.39394 11.8418ZM9.39394 10.5304C5.02072 10.5304 1.54819 6.87832 1.54819 5.92552C1.54819 5.12681 5.02072 1.32315 9.39394 1.32315C13.7504 1.32315 17.2353 5.12681 17.2353 5.92552C17.2353 6.87832 13.7504 10.5304 9.39394 10.5304ZM9.39517 9.60285C11.4368 9.60285 13.0849 7.94733 13.0849 5.91308C13.0849 3.87392 11.4368 2.22577 9.39517 2.22577C7.35478 2.22577 5.6998 3.87269 5.6998 5.91308C5.6998 7.94733 7.35478 9.60285 9.39517 9.60285ZM9.39271 7.11389C8.73414 7.11389 8.20367 6.5785 8.20367 5.91992C8.20367 5.26134 8.73414 4.72841 9.39271 4.72841C10.055 4.72841 10.5867 5.26134 10.5867 5.91992C10.5867 6.5785 10.055 7.11389 9.39271 7.11389Z" fill="#98989d"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 16.2518 25.6631">
|
||||
<g>
|
||||
<rect height="25.6631" opacity="0" width="16.2518" x="0" y="0"/>
|
||||
<path d="M3.76475 23.2699C2.82772 23.2699 2.29592 22.7658 2.29592 21.8726L2.29592 3.75866C2.29592 2.86545 2.82772 2.36131 3.76475 2.36131L12.1472 2.36131C13.0725 2.36131 13.6043 2.86967 13.6043 3.76076L13.6043 21.8705C13.6043 22.7615 13.0725 23.2699 12.1472 23.2699Z" fill="#5e5ce6" fill-opacity="0.25"/>
|
||||
<path d="M3.37101 25.6312L12.5798 25.6312C14.5701 25.6312 15.9002 24.3365 15.9002 22.3987L15.9002 3.23249C15.9002 1.29258 14.5701 0 12.5798 0L3.37101 0C1.35117 0 0 1.29258 0 3.23038L0 22.4008C0 24.3365 1.35117 25.6312 3.37101 25.6312ZM3.76475 23.2699C2.82772 23.2699 2.29592 22.7658 2.29592 21.8726L2.29592 3.75866C2.29592 2.86545 2.82772 2.36131 3.76475 2.36131L12.1472 2.36131C13.0725 2.36131 13.6043 2.86967 13.6043 3.76076L13.6043 21.8705C13.6043 22.7615 13.0725 23.2699 12.1472 23.2699ZM5.43045 22.3575L10.489 22.3575C10.8307 22.3575 11.0723 22.1201 11.0723 21.7645C11.0723 21.4069 10.8307 21.1769 10.489 21.1769L5.43045 21.1769C5.08874 21.1769 4.83538 21.4069 4.83538 21.7645C4.83538 22.1201 5.08874 22.3575 5.43045 22.3575ZM6.48209 5.02919L9.42772 5.02919C9.93115 5.02919 10.3219 4.63849 10.3219 4.12544C10.3219 3.62201 9.92904 3.2292 9.42772 3.2292L6.48209 3.2292C5.97116 3.2292 5.57835 3.62201 5.57835 4.12544C5.57835 4.63849 5.96905 5.02919 6.48209 5.02919Z" fill="#5e5ce6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 9.75228 17.2569">
|
||||
<g>
|
||||
<rect height="17.2569" opacity="0" width="9.75228" x="0" y="0"/>
|
||||
<path d="M4.11564 16.9569C4.43228 17.222 4.86007 17.2562 5.15935 16.9638L7.27043 14.8515C7.5641 14.5534 7.55042 14.1014 7.26359 13.8078L6.25324 12.8018L7.75017 11.3105C8.04015 11.0236 8.0314 10.5648 7.74457 10.2668L6.37861 8.89519C8.23265 7.97685 9.27376 6.43945 9.27376 4.62601C9.27376 2.06664 7.20152 0 4.63968 0C2.05857 0 0 2.0542 0 4.62601C0 6.46789 1.05027 8.10838 2.71387 8.85951L2.71387 15.277C2.71387 15.5185 2.78961 15.7992 3.00016 15.9856ZM4.63968 15.7501L3.89443 15.0117L3.89443 7.96811C2.33351 7.62672 1.21679 6.25625 1.21679 4.62601C1.21679 2.74394 2.74394 1.22363 4.63968 1.22363C6.52982 1.22363 8.04891 2.74394 8.04891 4.62601C8.04891 6.24381 6.92658 7.62795 5.2206 8.0098L5.2206 9.48171L6.52846 10.7989L5.12846 12.1784L5.12846 13.4116L6.05117 14.3194ZM4.63968 4.44431C5.24685 4.44431 5.74465 3.94529 5.74465 3.33375C5.74465 2.72535 5.24685 2.23439 4.63968 2.23439C4.02131 2.23439 3.53472 2.72412 3.53472 3.33375C3.53472 3.94529 4.02691 4.44431 4.63968 4.44431Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 10.0446 13.9699">
|
||||
<g>
|
||||
<rect height="13.9699" opacity="0" width="10.0446" x="0" y="0"/>
|
||||
<path d="M1.58238 13.6144L7.98368 13.6144C9.02179 13.6144 9.56606 13.0577 9.56606 11.937L9.56606 7.09543C9.56606 5.98404 9.02179 5.42733 7.98368 5.42733L1.58238 5.42733C0.543046 5.42733 0 5.98404 0 7.09543L0 11.937C0 13.0577 0.543046 13.6144 1.58238 13.6144ZM1.77652 12.389C1.49241 12.389 1.33013 12.2155 1.33013 11.8929L1.33013 7.14204C1.33013 6.81816 1.49241 6.65273 1.77652 6.65273L7.79078 6.65273C8.08049 6.65273 8.23471 6.81816 8.23471 7.14204L8.23471 11.8929C8.23471 12.2155 8.08049 12.389 7.79078 12.389ZM1.25043 6.00168L2.53052 6.00168L2.53052 3.70166C2.53052 2.09644 3.55728 1.22417 4.78023 1.22417C6.00072 1.22417 7.04238 2.09644 7.04238 3.70166L7.04238 6.00168L8.31687 6.00168L8.31687 3.82307C8.31687 1.30074 6.64753 0 4.78023 0C2.91853 0 1.25043 1.30074 1.25043 3.82307Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 11.3389 15.9656">
|
||||
<g>
|
||||
<rect height="15.9656" opacity="0" width="11.3389" x="0" y="0"/>
|
||||
<path d="M1.80844 15.5594L9.12421 15.5594C10.3106 15.5594 10.9326 14.9231 10.9326 13.6423L10.9326 8.10906C10.9326 6.83891 10.3106 6.20266 9.12421 6.20266L1.80844 6.20266C0.620625 6.20266 0 6.83891 0 8.10906L0 13.6423C0 14.9231 0.620625 15.5594 1.80844 15.5594ZM2.0303 14.1589C1.70562 14.1589 1.52015 13.9606 1.52015 13.5919L1.52015 8.16234C1.52015 7.79218 1.70562 7.60312 2.0303 7.60312L8.90375 7.60312C9.23484 7.60312 9.41109 7.79218 9.41109 8.16234L9.41109 13.5919C9.41109 13.9606 9.23484 14.1589 8.90375 14.1589ZM1.42906 6.85906L2.89202 6.85906L2.89202 4.23047C2.89202 2.39593 4.06546 1.39905 5.46312 1.39905C6.85797 1.39905 8.04844 2.39593 8.04844 4.23047L8.04844 6.85906L9.50499 6.85906L9.50499 4.36922C9.50499 1.48656 7.59718 0 5.46312 0C3.33547 0 1.42906 1.48656 1.42906 4.36922Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
BIN
public/logo.png
|
Before Width: | Height: | Size: 85 KiB |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 32.5814 30.6542">
|
||||
<g>
|
||||
<rect height="30.6542" opacity="0" width="32.5814" x="0" y="0"/>
|
||||
<path d="M21.5071 18.3514C21.5716 20.1403 22.4633 21.7234 23.8807 22.701L23.8807 25.9112L8.31866 25.9112C7.00147 25.9112 6.2135 25.2873 6.2135 24.2514C6.2135 21.2364 10.0387 17.0898 16.109 17.0898C18.199 17.0898 20.0223 17.5795 21.5071 18.3514ZM20.8462 9.80192C20.8462 12.7379 18.6729 15.009 16.1187 15.009C13.5569 15.009 11.3932 12.7379 11.3932 9.82325C11.3932 6.96059 13.5708 4.727 16.1187 4.727C18.6665 4.727 20.8462 6.92216 20.8462 9.80192Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
<path d="M27.0447 14.3176C24.9082 14.3176 23.2137 16.0356 23.2137 18.1349C23.2137 19.7413 24.1448 21.11 25.5862 21.6979L25.5862 27.4143C25.5862 27.5781 25.665 27.7173 25.7798 27.8493L26.7937 28.84C26.9405 28.9804 27.1572 28.9963 27.3199 28.8336L29.179 26.9842C29.3383 26.8152 29.3299 26.5857 29.1769 26.4285L28.1032 25.3464L29.6179 23.8541C29.7731 23.7064 29.7752 23.4674 29.5987 23.2846L28.1381 21.8357C29.8879 21.0763 30.8683 19.7554 30.8683 18.1349C30.8683 16.0356 29.1621 14.3176 27.0447 14.3176ZM27.0351 16.0764C27.6722 16.0764 28.1941 16.5983 28.1941 17.2396C28.1941 17.8895 27.6722 18.4157 27.0351 18.4157C26.4002 18.4157 25.8687 17.8895 25.8687 17.2396C25.8687 16.5983 26.3906 16.0764 27.0351 16.0764Z" fill="#5e5ce6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 39.3163 27.8386">
|
||||
<g>
|
||||
<rect height="27.8386" opacity="0" width="39.3163" x="0" y="0"/>
|
||||
<path d="M24.843 10.0397L5.28206 10.0397C4.36002 10.0397 3.83877 10.5567 3.83877 11.4809L3.83877 21.2583C3.83877 22.1719 4.36002 22.6995 5.28206 22.6995L28.9387 22.6995C29.8607 22.6995 30.3841 22.1719 30.3841 21.2583L30.3841 13.3841L32.6969 11.0698C32.7036 11.1454 32.7053 11.2254 32.7053 11.3068L32.7053 21.4345C32.7053 23.6494 31.4772 24.8391 29.256 24.8391L4.97645 24.8391C2.75316 24.8391 1.51754 23.6494 1.51754 21.4345L1.51754 11.3068C1.51754 9.08769 2.75316 7.90222 4.97645 7.90222L26.9788 7.90222Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
<path d="M22.7723 16.9573L25.2355 15.9005L35.3446 5.80103L33.5289 3.99284L23.4273 14.0944L22.3129 16.4958C22.1981 16.7625 22.4957 17.0754 22.7723 16.9573ZM36.2392 4.89471L37.0886 4.03783C37.559 3.55572 37.5674 2.90346 37.113 2.46284L36.7959 2.14784C36.385 1.74331 35.7168 1.79534 35.272 2.24018L34.431 3.07151Z" fill="#5e5ce6"/>
|
||||
<path d="M18.5315 18.106C19.4931 18.106 20.2748 17.3264 20.2748 16.3648C20.2748 15.4032 19.4931 14.6236 18.5315 14.6236C17.5699 14.6236 16.7903 15.4032 16.7903 16.3648C16.7903 17.3264 17.5699 18.106 18.5315 18.106Z" fill="#5e5ce6"/>
|
||||
<path d="M13.3816 18.106C14.3432 18.106 15.1227 17.3264 15.1227 16.3648C15.1227 15.4032 14.3432 14.6236 13.3816 14.6236C12.4199 14.6236 11.6404 15.4032 11.6404 16.3648C11.6404 17.3264 12.4199 18.106 13.3816 18.106Z" fill="#5e5ce6"/>
|
||||
<path d="M8.23166 18.106C9.1933 18.106 9.97283 17.3264 9.97283 16.3648C9.97283 15.4032 9.1933 14.6236 8.23166 14.6236C7.27002 14.6236 6.49049 15.4032 6.49049 16.3648C6.49049 17.3264 7.27002 18.106 8.23166 18.106Z" fill="#5e5ce6"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 12.0289 12.3668">
|
||||
<g>
|
||||
<rect height="12.3668" opacity="0" width="12.0289" x="0" y="0"/>
|
||||
<path d="M1.22801 12.3575L10.3236 12.3575C11.092 12.3575 11.5504 11.9935 11.5504 11.3892C11.5504 9.63047 9.32025 7.21164 5.7724 7.21164C2.23139 7.21164 0 9.63047 0 11.3892C0 11.9935 0.459648 12.3575 1.22801 12.3575ZM5.77801 5.99785C7.26797 5.99785 8.53576 4.67305 8.53576 2.96037C8.53576 1.28051 7.26428 0 5.77801 0C4.29174 0 3.02148 1.30293 3.02148 2.97281C3.02148 4.67305 4.28367 5.99785 5.77801 5.99785Z" fill="black"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 826 B |
@ -1,16 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 26.5504 25.7892">
|
||||
<g>
|
||||
<rect height="25.7892" opacity="0" width="26.5504" x="0" y="0"/>
|
||||
<path d="M1.43975 16.9425C1.80912 16.8265 1.99006 16.4433 1.89209 16.0132C1.5949 14.8127 1.42334 13.6116 1.47561 11.973C1.48522 11.5205 1.22975 11.2502 0.848656 11.2162C0.366314 11.1726 0.0717063 11.4729 0.0344408 11.8931C-0.0668094 13.2384 0.0449873 14.7762 0.506237 16.4198C0.631862 16.852 1.04272 17.0702 1.43975 16.9425ZM0.903032 9.83999C1.29561 9.98788 1.74818 9.8039 1.92279 9.33749C3.79897 4.14421 8.26428 1.43906 12.9391 1.43906C15.0166 1.43906 16.6319 1.8546 18.037 2.62617C18.4882 2.87531 18.9609 2.87531 19.1536 2.45062C19.3474 2.02992 19.1449 1.72781 18.7992 1.51078C16.9926 0.397501 15.1879 0 12.9391 0C7.49038 0 2.6878 2.92125 0.525221 8.64234C0.294127 9.24726 0.521002 9.70804 0.903032 9.83999ZM25.529 14.1994C25.942 14.1994 26.2347 13.863 26.1975 13.4191C25.687 8.58632 24.0977 5.58515 21.3654 3.15047C21.0086 2.82469 20.5542 2.91422 20.3276 3.18445C20.0754 3.46007 20.0709 3.88265 20.4394 4.22015C22.7901 6.44811 24.4174 9.23718 24.8095 13.451C24.8606 13.8757 25.1245 14.1994 25.529 14.1994Z" fill="#5e5ce6"/>
|
||||
<path d="M2.75061 20.3557C2.99647 20.692 3.48397 20.774 3.8374 20.4717C4.69592 19.7463 5.13467 18.5747 5.13467 17.3351C5.13467 15.6968 4.42522 14.8617 4.42522 12.8545C4.42522 8.23124 8.25818 4.39827 12.8815 4.39827C18.4287 4.39827 22.0261 8.9482 22.0357 15.6902C22.0357 18.3478 21.5937 20.2015 21.1664 21.2468C21.0014 21.6354 21.1504 22.0601 21.4889 22.2368C21.8508 22.4006 22.3479 22.2602 22.499 21.8419C22.954 20.6144 23.4747 18.5543 23.4747 15.6785C23.4747 8.09132 19.2876 2.96133 12.8815 2.96133C7.46412 2.96133 2.98616 7.44679 2.98616 12.8545C2.98616 14.7954 3.72116 16.1067 3.73288 17.3234C3.73288 18.1223 3.43569 18.8672 2.90178 19.3455C2.59733 19.6158 2.54202 20.0587 2.75061 20.3557Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
<path d="M8.15061 16.0235C7.85342 15.2245 7.39826 14.2399 7.39826 12.9122C7.39826 9.80765 9.83459 7.36921 12.9391 7.36921C14.1239 7.36921 14.4926 7.4939 15.5144 7.97882C15.9072 8.15554 16.267 8.04163 16.4107 7.75218C16.6033 7.39242 16.5011 6.95601 16.0711 6.70687C15.2829 6.2414 14.1309 5.92054 12.9391 5.92054C9.04053 5.92054 5.94749 9.0157 5.94749 12.9122C5.94749 14.3676 6.3417 15.5925 6.7371 16.5738C6.9117 16.9985 7.30967 17.1944 7.76319 17.0081C8.14639 16.8569 8.29006 16.4461 8.15061 16.0235Z" fill="#5e5ce6"/>
|
||||
<path d="M5.61537 22.5712C6.51631 22.1112 7.69147 20.5814 8.03014 19.3765C8.13865 19.0104 7.961 18.5803 7.5724 18.4526C7.21053 18.3248 6.82733 18.543 6.68366 18.9518C6.36514 19.9577 5.82491 20.7101 4.97037 21.2939C4.53373 21.6016 4.44983 21.9881 4.59139 22.2977C4.74889 22.6329 5.16608 22.799 5.61537 22.5712ZM17.4239 9.6103C18.5395 11.269 19.0999 13.5225 19.0999 16.3071C19.0999 19.2813 18.2796 22.0957 16.9959 23.6873C16.7597 23.9981 16.7447 24.4324 17.0236 24.7071C17.3344 24.9923 17.8549 24.9722 18.1104 24.6283C19.5696 22.6589 20.5155 19.5337 20.5155 16.2602C20.5155 12.6187 19.7369 10.5537 18.6874 8.84179C18.4308 8.43726 17.9805 8.30742 17.6229 8.49796C17.2429 8.70234 17.1417 9.18445 17.4239 9.6103Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
<path d="M8.10795 24.1329C10.0594 22.5628 11.1372 20.0576 11.1255 17.2828C11.1138 15.1844 10.3671 13.8944 10.3671 12.7716C10.3671 11.3098 11.3974 10.3401 12.9103 10.3401C15.1844 10.3401 15.9841 12.8981 16.0629 16.1845C16.1726 19.8155 14.8694 22.9535 13.1034 24.498C12.7969 24.7535 12.7319 25.1526 12.9363 25.473C13.1822 25.8211 13.6835 25.886 14.0135 25.5773C15.8965 23.8669 17.4349 20.1178 17.4136 16.0844C17.3997 11.955 16.1915 8.9032 12.9103 8.9032C10.661 8.9032 8.95358 10.4048 8.95358 12.6459C8.95358 13.9997 9.66303 15.6455 9.67686 17.2828C9.68858 19.6158 8.81717 21.7038 7.2117 23.0046C6.87209 23.2835 6.81678 23.6901 7.03498 24.0148C7.30217 24.3862 7.76623 24.4235 8.10795 24.1329Z" fill="#5e5ce6"/>
|
||||
<path d="M10.8881 25.0809C11.8223 24.4024 12.9307 22.9073 13.4013 21.4179C13.5108 21.0989 13.4184 20.6358 13.0181 20.5005C12.5817 20.3451 12.2165 20.5655 12.0837 20.9208C11.5134 22.3898 10.8089 23.2945 10.0132 23.9229C9.69491 24.1805 9.60866 24.603 9.82475 24.9351C10.0312 25.2865 10.5283 25.346 10.8881 25.0809ZM14.1199 18.5271C14.4396 16.9071 14.3149 14.8861 13.7365 13.0987C13.5778 12.6335 13.2042 12.427 12.8072 12.5548C12.4315 12.6783 12.2079 13.0326 12.3431 13.4702C12.878 15.1884 12.9696 16.8326 12.7064 18.3281C12.634 18.7176 12.7999 19.0891 13.2417 19.1541C13.6526 19.2286 14.0358 18.9806 14.1199 18.5271Z" fill="#5e5ce6" fill-opacity="0.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.7 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 15.8083 19.0759">
|
||||
<g>
|
||||
<rect height="19.0759" opacity="0" width="15.8083" x="0" y="0"/>
|
||||
<path d="M5.40046 14.9841C5.72968 14.9841 5.94187 14.7755 5.93405 14.4683L5.69468 6.27765C5.68546 5.97327 5.47046 5.77109 5.15546 5.77109C4.82906 5.77109 4.61828 5.97968 4.62609 6.28546L4.85906 14.4725C4.86828 14.7833 5.08468 14.9841 5.40046 14.9841ZM7.70171 14.9841C8.02531 14.9841 8.25312 14.7783 8.25312 14.4739L8.25312 6.28265C8.25312 5.97827 8.02531 5.77109 7.70171 5.77109C7.37671 5.77109 7.15531 5.97827 7.15531 6.28265L7.15531 14.4739C7.15531 14.7783 7.37671 14.9841 7.70171 14.9841ZM10.008 14.9841C10.3187 14.9841 10.5337 14.7833 10.543 14.4725L10.7759 6.28546C10.7837 5.97968 10.573 5.77109 10.2466 5.77109C9.93156 5.77109 9.71656 5.97327 9.70734 6.28405L9.47437 14.4683C9.46656 14.7755 9.67875 14.9841 10.008 14.9841ZM4.1525 3.59422L5.59437 3.59422L5.59437 1.97156C5.59437 1.56703 5.87734 1.30749 6.31234 1.30749L9.07687 1.30749C9.51187 1.30749 9.79484 1.56703 9.79484 1.97156L9.79484 3.59422L11.2367 3.59422L11.2367 1.89906C11.2367 0.704374 10.4742 0 9.17968 0L6.20953 0C4.915 0 4.1525 0.704374 4.1525 1.89906ZM0.691402 4.44421L14.717 4.44421C15.102 4.44421 15.402 4.13843 15.402 3.75343C15.402 3.37125 15.102 3.07187 14.717 3.07187L0.691402 3.07187C0.312811 3.07187 0 3.37125 0 3.75343C0 4.14483 0.312811 4.44421 0.691402 4.44421ZM4.11374 17.6291L11.2947 17.6291C12.4906 17.6291 13.2673 16.8969 13.3234 15.6995L13.8605 4.29436L12.4114 4.29436L11.8978 15.4669C11.8822 15.9356 11.573 16.2469 11.1275 16.2469L4.26812 16.2469C3.83546 16.2469 3.52624 15.9278 3.50421 15.4669L2.96499 4.29436L1.54156 4.29436L2.085 15.7059C2.1425 16.9033 2.905 17.6291 4.11374 17.6291Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1,7 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 265 301" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
|
||||
<path d="M22.672,60.656C24.219,60.656 25.188,59.672 25.156,58.203L24.219,26.641C24.172,25.203 23.172,24.234 21.672,24.234C20.156,24.234 19.203,25.219 19.234,26.672L20.156,58.25C20.203,59.703 21.219,60.656 22.672,60.656ZM31.797,60.656C33.281,60.656 34.313,59.703 34.313,58.266L34.313,26.641C34.313,25.203 33.281,24.234 31.797,24.234C30.297,24.234 29.281,25.203 29.281,26.641L29.281,58.266C29.281,59.703 30.297,60.656 31.797,60.656ZM40.922,60.656C42.375,60.656 43.375,59.703 43.422,58.25L44.344,26.672C44.375,25.219 43.422,24.234 41.906,24.234C40.406,24.234 39.406,25.203 39.359,26.656L38.438,58.203C38.406,59.672 39.375,60.656 40.922,60.656ZM16.75,14.547L23.938,14.547L23.938,8.406C23.938,7.078 24.859,6.25 26.359,6.25L37.188,6.25C38.688,6.25 39.609,7.078 39.609,8.406L39.609,14.547L46.797,14.547L46.797,8.156C46.797,2.938 43.547,0 37.719,0L25.828,0C20,0 16.75,2.938 16.75,8.156L16.75,14.547ZM3.516,19.547L60.078,19.547C62.078,19.547 63.578,18.094 63.578,16.094C63.578,14.125 62.078,12.688 60.078,12.688L3.516,12.688C1.531,12.688 0,14.125 0,16.094C0,18.109 1.531,19.547 3.516,19.547ZM17.375,72.156L46.219,72.156C51.563,72.156 54.859,69.188 55.094,63.828L57.172,18.938L50.016,18.938L48.031,62.188C47.969,64.063 46.922,65.188 45.25,65.188L18.313,65.188C16.672,65.188 15.625,64.031 15.547,62.188L13.5,18.938L6.406,18.938L8.5,63.844C8.75,69.203 12,72.156 17.375,72.156Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.9 KiB |
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,3.0625,3.65625)">
|
||||
<rect x="0" y="0" width="57.875" height="56.688" style="fill-opacity:0;"/>
|
||||
<path d="M22.484,56.688C24.609,56.688 26.281,55.859 27.422,54.141L56.516,9.609C57.344,8.359 57.688,7.188 57.688,6.109C57.688,3.188 55.484,1.063 52.5,1.063C50.438,1.063 49.172,1.781 47.906,3.75L22.359,44.094L9.375,28C8.234,26.594 6.969,25.969 5.219,25.969C2.188,25.969 0,28.141 0,31.078C0,32.375 0.406,33.531 1.516,34.828L17.656,54.375C18.984,55.953 20.5,56.688 22.484,56.688Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1018 B |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 221 286" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
|
||||
<g>
|
||||
<rect x="0" y="0" width="52.969" height="68.438" style="fill-opacity:0;"/>
|
||||
<path d="M52.781,64.406L52.781,8.828C52.781,3.391 49.391,0 43.875,0L8.922,0C3.406,0 0,3.391 0,8.828L0,64.406C0,66.625 1.813,68.438 4.031,68.438C6.25,68.438 8.063,66.625 8.063,64.406L8.063,9.844C8.063,8.75 8.75,8.063 9.797,8.063L42.984,8.063C44.031,8.063 44.719,8.75 44.719,9.844L44.719,64.406C44.719,66.625 46.531,68.438 48.766,68.438C50.984,68.438 52.781,66.625 52.781,64.406Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<path d="M40.906,65.75L40.906,12.766C40.906,11.953 40.281,11.563 39.422,11.906L29.906,15.969C29.109,16.281 28.766,16.672 28.766,17.438L28.766,61.063C28.766,61.828 29.109,62.203 29.922,62.547L39.422,66.578C40.281,66.938 40.906,66.547 40.906,65.75Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(0.967407,0,0,0.967407,-8.88178e-16,0.226712)">
|
||||
<rect x="0" y="0" width="66.156" height="65.688" style="fill-opacity:0;"/>
|
||||
<path d="M32.984,6.766C34.859,6.766 36.375,5.25 36.375,3.375C36.375,1.516 34.859,0 32.984,0C31.125,0 29.609,1.516 29.609,3.375C29.609,5.25 31.125,6.766 32.984,6.766ZM42.094,8.234C43.953,8.234 45.484,6.719 45.484,4.859C45.484,2.984 43.953,1.469 42.094,1.469C40.234,1.469 38.703,2.984 38.703,4.859C38.703,6.719 40.234,8.234 42.094,8.234ZM50.328,12.422C52.203,12.422 53.719,10.891 53.719,9.031C53.719,7.172 52.203,5.641 50.328,5.641C48.469,5.641 46.953,7.172 46.953,9.031C46.953,10.891 48.469,12.422 50.328,12.422ZM56.859,18.953C58.719,18.953 60.25,17.438 60.25,15.563C60.25,13.703 58.719,12.188 56.859,12.188C55,12.188 53.469,13.703 53.469,15.563C53.469,17.438 55,18.953 56.859,18.953ZM61.016,27.094C62.875,27.094 64.406,25.563 64.406,23.703C64.406,21.844 62.875,20.313 61.016,20.313C59.156,20.313 57.625,21.844 57.625,23.703C57.625,25.563 59.156,27.094 61.016,27.094ZM62.578,36.219C64.438,36.219 65.969,34.688 65.969,32.828C65.969,30.969 64.438,29.438 62.578,29.438C60.719,29.438 59.188,30.969 59.188,32.828C59.188,34.688 60.719,36.219 62.578,36.219ZM61.016,45.328C62.875,45.328 64.406,43.813 64.406,41.953C64.406,40.078 62.875,38.563 61.016,38.563C59.156,38.563 57.625,40.078 57.625,41.953C57.625,43.813 59.156,45.328 61.016,45.328ZM56.859,53.484C58.719,53.484 60.25,51.969 60.25,50.094C60.25,48.234 58.719,46.719 56.859,46.719C55,46.719 53.469,48.234 53.469,50.094C53.469,51.969 55,53.484 56.859,53.484ZM50.328,60.016C52.203,60.016 53.719,58.484 53.719,56.625C53.719,54.766 52.203,53.234 50.328,53.234C48.469,53.234 46.953,54.766 46.953,56.625C46.953,58.484 48.469,60.016 50.328,60.016ZM42.094,64.188C43.953,64.188 45.484,62.656 45.484,60.797C45.484,58.938 43.953,57.406 42.094,57.406C40.234,57.406 38.703,58.938 38.703,60.797C38.703,62.656 40.234,64.188 42.094,64.188ZM32.984,65.656C34.859,65.656 36.375,64.141 36.375,62.266C36.375,60.406 34.859,58.891 32.984,58.891C31.125,58.891 29.609,60.406 29.609,62.266C29.609,64.141 31.125,65.656 32.984,65.656ZM23.891,64.188C25.75,64.188 27.281,62.656 27.281,60.797C27.281,58.938 25.75,57.406 23.891,57.406C22.016,57.406 20.5,58.938 20.5,60.797C20.5,62.656 22.016,64.188 23.891,64.188ZM15.641,60.016C17.516,60.016 19.031,58.484 19.031,56.625C19.031,54.766 17.516,53.234 15.641,53.234C13.766,53.234 12.266,54.766 12.266,56.625C12.266,58.484 13.766,60.016 15.641,60.016ZM9.109,53.484C10.969,53.484 12.5,51.969 12.5,50.094C12.5,48.234 10.969,46.719 9.109,46.719C7.25,46.719 5.719,48.234 5.719,50.094C5.719,51.969 7.25,53.484 9.109,53.484ZM9.109,18.953C10.969,18.953 12.5,17.438 12.5,15.563C12.5,13.703 10.969,12.188 9.109,12.188C7.25,12.188 5.719,13.703 5.719,15.563C5.719,17.438 7.25,18.953 9.109,18.953ZM15.641,12.422C17.516,12.422 19.031,10.891 19.031,9.031C19.031,7.172 17.516,5.641 15.641,5.641C13.766,5.641 12.266,7.172 12.266,9.031C12.266,10.891 13.766,12.422 15.641,12.422ZM23.891,8.234C25.75,8.234 27.281,6.719 27.281,4.859C27.281,2.984 25.75,1.469 23.891,1.469C22.016,1.469 20.5,2.984 20.5,4.859C20.5,6.719 22.016,8.234 23.891,8.234Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
<g transform="matrix(1,0,0,1,-5.16845,0)">
|
||||
<path d="M48.531,32.813C48.531,31.859 48.219,30.953 47.328,30.047L37.719,20.031C37.109,19.422 36.469,19.094 35.563,19.094C33.844,19.094 32.641,20.531 32.641,22.141C32.641,23.016 33.016,23.828 33.656,24.406L37.625,28.141L39.828,29.625L35.359,29.297L14.329,29.297C12.47,29.297 10.876,30.844 10.876,32.813C10.876,34.75 12.47,36.297 14.329,36.297L35.359,36.297L39.828,36L37.625,37.484L33.656,41.203C33.016,41.797 32.641,42.625 32.641,43.5C32.641,45.094 33.844,46.531 35.563,46.531C36.469,46.531 37.109,46.188 37.719,45.578L47.328,35.578C48.219,34.672 48.531,33.734 48.531,32.813Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.2 KiB |
@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generator: Apple Native CoreSVG 326-->
|
||||
<!DOCTYPE svg
|
||||
PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 21.8682 20.7181">
|
||||
<g>
|
||||
<rect height="20.7181" opacity="0" width="21.8682" x="0" y="0"/>
|
||||
<path d="M14.2757 12.3369C14.2924 13.578 14.8884 14.6853 15.8553 15.3753L15.8553 17.4432L5.56472 17.4432C4.65426 17.4432 4.10954 17.0078 4.10954 16.2849C4.10954 14.2792 6.68018 11.5278 10.7271 11.5278C12.0946 11.5278 13.2931 11.8407 14.2757 12.3369ZM13.9046 6.66377C13.9046 8.62435 12.4589 10.1492 10.7324 10.1492C9.00301 10.1492 7.56266 8.62435 7.56266 6.67691C7.56266 4.77278 9.01331 3.26216 10.7324 3.26216C12.4515 3.26216 13.9046 4.75147 13.9046 6.66377Z" fill="white"/>
|
||||
<path d="M18.0725 9.68825C16.6223 9.68825 15.471 10.8551 15.471 12.2795C15.471 13.3569 16.0886 14.2801 17.0495 14.685L17.0495 18.4528C17.0495 18.5738 17.1063 18.6839 17.1936 18.7794L17.8847 19.4431C17.9976 19.5486 18.1691 19.5614 18.2948 19.4357L19.532 18.2038C19.6598 18.0706 19.6498 17.8843 19.5295 17.7589L18.8579 17.0774L19.8214 16.1225C19.9442 16.0025 19.9467 15.8107 19.8107 15.6673L18.893 14.7574C20.0316 14.2404 20.6712 13.3555 20.6712 12.2795C20.6712 10.8551 19.5121 9.68825 18.0725 9.68825ZM18.0672 10.8672C18.494 10.8672 18.8452 11.2184 18.8452 11.6502C18.8452 12.0803 18.494 12.4365 18.0672 12.4365C17.6428 12.4365 17.2863 12.0803 17.2863 11.6502C17.2863 11.2184 17.6375 10.8672 18.0672 10.8672Z" fill="white"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 276 292" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
|
||||
<g>
|
||||
<rect x="0" y="0" width="66.203" height="70" style="fill-opacity:0;"/>
|
||||
<path d="M33,68C51.234,68 66.016,53.203 66.016,35C66.016,25.625 62.125,17.5 56.531,11.969C52.484,7.781 46.344,12.984 50.594,17.563C55.047,22.016 57.797,28.172 57.797,35C57.797,48.703 46.703,59.781 33,59.781C19.297,59.781 8.219,48.703 8.219,35C8.219,28.125 10.984,22 15.422,17.547C19.688,12.953 13.563,7.844 9.484,11.984C3.859,17.453 0,25.781 0,35C0,53.203 14.781,68 33,68ZM33,35.781C35.641,35.781 37.391,33.922 37.391,31.141L37.391,4.625C37.391,1.844 35.641,0 33,0C30.375,0 28.641,1.844 28.641,4.625L28.641,31.141C28.641,33.922 30.375,35.781 33,35.781Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(1,0,0,1,1.4844,6.7969)">
|
||||
<rect x="0" y="0" width="61.031" height="50.406" style="fill-opacity:0;"/>
|
||||
<path d="M60.844,25.094C60.844,23.859 60.328,22.594 59.469,21.75L39.125,1.453C38.156,0.469 37.047,0 35.938,0C33.281,0 31.484,1.844 31.484,4.297C31.484,5.656 32.063,6.703 32.906,7.531L39.906,14.563L50.25,24.078L52.156,21.219L40.016,20.563L4.688,20.563C1.875,20.563 0,22.391 0,25.094C0,27.797 1.875,29.625 4.688,29.625L40.016,29.625L52.156,28.969L50.25,26.141L39.906,35.625L32.906,42.641C32.063,43.438 31.484,44.516 31.484,45.875C31.484,48.328 33.281,50.172 35.938,50.172C37.047,50.172 38.156,49.688 39.094,48.75L59.469,28.438C60.328,27.594 60.844,26.328 60.844,25.094Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 217 216" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(4.16667,0,0,4.16667,0,0)">
|
||||
<g>
|
||||
<rect x="0" y="0" width="51.887" height="51.762" style="fill-opacity:0;"/>
|
||||
<path d="M1.342,50.412C3.17,52.209 6.311,52.162 8.014,50.459L25.857,32.615L43.67,50.443C45.436,52.209 48.514,52.209 50.326,50.397C52.139,48.568 52.154,45.522 50.389,43.74L32.576,25.897L50.389,8.084C52.154,6.318 52.154,3.256 50.326,1.443C48.498,-0.385 45.436,-0.4 43.67,1.381L25.857,19.193L8.014,1.365C6.311,-0.338 3.154,-0.416 1.342,1.428C-0.455,3.256 -0.408,6.365 1.295,8.068L19.139,25.897L1.295,43.772C-0.408,45.459 -0.471,48.6 1.342,50.412Z" style="fill:white;fill-rule:nonzero;"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@ -1,6 +1,5 @@
|
||||
// This file is auto-generated by the `update-kc-gen` command. Do not edit it manually.
|
||||
// Hash: 612dc705f216ef0750f6fe7480dd13bcc1c0af561a77e7ce5ffd6a39e53d185d
|
||||
|
||||
// Hash: 09b09a6c36072d5cf2f8484ab3dc720d28ec8c126df1bafb0b2214a0139848c7
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
@ -10,9 +9,9 @@
|
||||
|
||||
import { lazy, Suspense, type ReactNode } from "react";
|
||||
|
||||
export type ThemeName = "auth-tombutcher-work";
|
||||
export type ThemeName = "keycloakify-starter";
|
||||
|
||||
export const themeNames: ThemeName[] = ["auth-tombutcher-work"];
|
||||
export const themeNames: ThemeName[] = ["keycloakify-starter"];
|
||||
|
||||
export type KcEnvName = never;
|
||||
|
||||
@ -25,9 +24,7 @@ export const kcEnvDefaults: Record<KcEnvName, string> = {};
|
||||
* If you need to import the KcContext import it either from src/login/KcContext.ts or src/account/KcContext.ts.
|
||||
* Depending on the theme type you are working on.
|
||||
*/
|
||||
export type KcContext =
|
||||
| import("./login/KcContext").KcContext
|
||||
;
|
||||
export type KcContext = import("./login/KcContext").KcContext;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -37,18 +34,14 @@ declare global {
|
||||
|
||||
export const KcLoginPage = lazy(() => import("./login/KcPage"));
|
||||
|
||||
export function KcPage(
|
||||
props: {
|
||||
kcContext: KcContext;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
) {
|
||||
export function KcPage(props: { kcContext: KcContext; fallback?: ReactNode }) {
|
||||
const { kcContext, fallback } = props;
|
||||
return (
|
||||
<Suspense fallback={fallback}>
|
||||
{(() => {
|
||||
switch (kcContext.themeType) {
|
||||
case "login": return <KcLoginPage kcContext={kcContext} />;
|
||||
case "login":
|
||||
return <KcLoginPage kcContext={kcContext} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
|
||||
@ -3,51 +3,10 @@ import type { ClassKey } from "keycloakify/login";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { useI18n } from "./i18n";
|
||||
import DefaultPage from "keycloakify/login/DefaultPage";
|
||||
import Template from "./Template";
|
||||
const UserProfileFormFields = lazy(() => import("./UserProfileFormFields"));
|
||||
|
||||
import "./custom.css";
|
||||
import "./fonts.css";
|
||||
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
const LoginUsername = lazy(() => import("./pages/LoginUsername"));
|
||||
const LoginPassword = lazy(() => import("./pages/LoginPassword"));
|
||||
const Register = lazy(() => import("./pages/Register"));
|
||||
const Error = lazy(() => import("./pages/Error"));
|
||||
const WebauthnAuthenticate = lazy(() => import("./pages/WebauthnAuthenticate"));
|
||||
const WebauthnRegister = lazy(() => import("./pages/WebauthnRegister"));
|
||||
const LoginResetPassword = lazy(() => import("./pages/LoginResetPassword"));
|
||||
const Info = lazy(() => import("./pages/Info"));
|
||||
const Terms = lazy(() => import("./pages/Terms"));
|
||||
const LogoutConfirm = lazy(() => import("./pages/LogoutConfirm"));
|
||||
const LoginUpdatePassword = lazy(() => import("./pages/LoginUpdatePassword"));
|
||||
const LoginUpdateProfile = lazy(() => import("./pages/LoginUpdateProfile"));
|
||||
const LoginOauth2DeviceVerifyUserCode = lazy(
|
||||
() => import("./pages/LoginOauth2DeviceVerifyUserCode")
|
||||
import Template from "keycloakify/login/Template";
|
||||
const UserProfileFormFields = lazy(
|
||||
() => import("keycloakify/login/UserProfileFormFields")
|
||||
);
|
||||
const LoginVerifyEmail = lazy(() => import("./pages/LoginVerifyEmail"));
|
||||
const LoginOtp = lazy(() => import("./pages/LoginOtp"));
|
||||
const LoginIdpLinkConfirm = lazy(() => import("./pages/LoginIdpLinkConfirm"));
|
||||
const LoginIdpLinkEmail = lazy(() => import("./pages/LoginIdpLinkEmail"));
|
||||
const LoginOauthGrant = lazy(() => import("./pages/LoginOauthGrant"));
|
||||
const LoginConfigTotp = lazy(() => import("./pages/LoginConfigTotp"));
|
||||
const UpdateEmail = lazy(() => import("./pages/UpdateEmail"));
|
||||
const SelectAuthenticator = lazy(() => import("./pages/SelectAuthenticator"));
|
||||
const SamlPostForm = lazy(() => import("./pages/SamlPostForm"));
|
||||
const DeleteCredential = lazy(() => import("./pages/DeleteCredential"));
|
||||
const Code = lazy(() => import("./pages/Code"));
|
||||
const DeleteAccountConfirm = lazy(() => import("./pages/DeleteAccountConfirm"));
|
||||
const FrontchannelLogout = lazy(() => import("./pages/FrontchannelLogout"));
|
||||
const LoginRecoveryAuthnCodeConfig = lazy(
|
||||
() => import("./pages/LoginRecoveryAuthnCodeConfig")
|
||||
);
|
||||
const LoginRecoveryAuthnCodeInput = lazy(
|
||||
() => import("./pages/LoginRecoveryAuthnCodeInput")
|
||||
);
|
||||
const LoginPageExpired = lazy(() => import("./pages/LoginPageExpired"));
|
||||
const IdpReviewUserProfile = lazy(() => import("./pages/IdpReviewUserProfile"));
|
||||
const LoginResetOtp = lazy(() => import("./pages/LoginResetOtp"));
|
||||
const WebauthnError = lazy(() => import("./pages/WebauthnError"));
|
||||
|
||||
const doMakeUserConfirmPassword = true;
|
||||
|
||||
@ -60,304 +19,6 @@ export default function KcPage(props: { kcContext: KcContext }) {
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl":
|
||||
return (
|
||||
<Login
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-username.ftl":
|
||||
return (
|
||||
<LoginUsername
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-password.ftl":
|
||||
return (
|
||||
<LoginPassword
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "register.ftl":
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
classes={{}}
|
||||
headerNode={
|
||||
kcContext.messageHeader !== undefined
|
||||
? i18n.advancedMsg(kcContext.messageHeader)
|
||||
: i18n.msg("registerTitle")
|
||||
}
|
||||
displayMessage={kcContext.messagesPerField.exists(
|
||||
"global"
|
||||
)}
|
||||
>
|
||||
<Register
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={false}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
</Template>
|
||||
);
|
||||
|
||||
case "error.ftl":
|
||||
return (
|
||||
<Error
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "webauthn-authenticate.ftl":
|
||||
return (
|
||||
<WebauthnAuthenticate
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "webauthn-register.ftl":
|
||||
return (
|
||||
<WebauthnRegister
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "webauthn-error.ftl":
|
||||
return (
|
||||
<WebauthnError
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-reset-password.ftl":
|
||||
return (
|
||||
<LoginResetPassword
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "info.ftl":
|
||||
return (
|
||||
<Info
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "terms.ftl":
|
||||
return (
|
||||
<Terms
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "logout-confirm.ftl":
|
||||
return (
|
||||
<LogoutConfirm
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-update-password.ftl":
|
||||
return (
|
||||
<LoginUpdatePassword
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-update-profile.ftl":
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
headerNode={i18n.msg("loginProfileTitle")}
|
||||
displayMessage={kcContext.messagesPerField.exists(
|
||||
"global"
|
||||
)}
|
||||
>
|
||||
<LoginUpdateProfile
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
</Template>
|
||||
);
|
||||
case "login-oauth2-device-verify-user-code.ftl":
|
||||
return (
|
||||
<LoginOauth2DeviceVerifyUserCode
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-verify-email.ftl":
|
||||
return (
|
||||
<LoginVerifyEmail
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-otp.ftl":
|
||||
return (
|
||||
<LoginOtp
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-idp-link-confirm.ftl":
|
||||
return (
|
||||
<LoginIdpLinkConfirm
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-idp-link-email.ftl":
|
||||
return (
|
||||
<LoginIdpLinkEmail
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-oauth-grant.ftl":
|
||||
return (
|
||||
<LoginOauthGrant
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-config-totp.ftl":
|
||||
return (
|
||||
<LoginConfigTotp
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "update-email.ftl":
|
||||
return (
|
||||
<UpdateEmail
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
);
|
||||
case "select-authenticator.ftl":
|
||||
return (
|
||||
<SelectAuthenticator
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "saml-post-form.ftl":
|
||||
return (
|
||||
<SamlPostForm
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "delete-credential.ftl":
|
||||
return (
|
||||
<DeleteCredential
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "code.ftl":
|
||||
return (
|
||||
<Code
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "delete-account-confirm.ftl":
|
||||
return (
|
||||
<DeleteAccountConfirm
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "frontchannel-logout.ftl":
|
||||
return (
|
||||
<FrontchannelLogout
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-recovery-authn-code-config.ftl":
|
||||
return (
|
||||
<LoginRecoveryAuthnCodeConfig
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-recovery-authn-code-input.ftl":
|
||||
return (
|
||||
<LoginRecoveryAuthnCodeInput
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "login-page-expired.ftl":
|
||||
return (
|
||||
<LoginPageExpired
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
case "idp-review-user-profile.ftl":
|
||||
return (
|
||||
<IdpReviewUserProfile
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
UserProfileFormFields={UserProfileFormFields}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
);
|
||||
case "login-reset-otp.ftl":
|
||||
return (
|
||||
<LoginResetOtp
|
||||
{...{ kcContext, i18n, classes }}
|
||||
Template={Template}
|
||||
doUseDefaultCss={true}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<DefaultPage
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import Particles, { initParticlesEngine } from "@tsparticles/react";
|
||||
import { type Container, type ISourceOptions } from "@tsparticles/engine";
|
||||
// import { loadAll } from "@tsparticles/all"; // if you are going to use `loadAll`, install the "@tsparticles/all" package too.
|
||||
// import { loadFull } from "tsparticles"; // if you are going to use `loadFull`, install the "tsparticles" package too.
|
||||
import { loadSlim } from "@tsparticles/slim"; // if you are going to use `loadSlim`, install the "@tsparticles/slim" package too.
|
||||
// import { loadBasic } from "@tsparticles/basic"; // if you are going to use `loadBasic`, install the "@tsparticles/basic" package too.
|
||||
|
||||
const ParticlesBackground = () => {
|
||||
const [init, setInit] = useState(false);
|
||||
|
||||
// this should be run only once per application lifetime
|
||||
useEffect(() => {
|
||||
initParticlesEngine(async engine => {
|
||||
// you can initiate the tsParticles instance (engine) here, adding custom shapes or presets
|
||||
// this loads the tsparticles package bundle, it's the easiest method for getting everything ready
|
||||
// starting from v2 you can add only the features you need reducing the bundle size
|
||||
//await loadAll(engine);
|
||||
//await loadFull(engine);
|
||||
await loadSlim(engine);
|
||||
//await loadBasic(engine);
|
||||
}).then(() => {
|
||||
setInit(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const particlesLoaded = async (container?: Container): Promise<void> => {
|
||||
console.log(container);
|
||||
};
|
||||
|
||||
const options: ISourceOptions = useMemo(
|
||||
() => ({
|
||||
background: {
|
||||
color: {
|
||||
value: "#FF00A1"
|
||||
}
|
||||
},
|
||||
fpsLimit: 120,
|
||||
interactivity: {
|
||||
events: {
|
||||
onClick: {
|
||||
enable: false,
|
||||
mode: "push"
|
||||
},
|
||||
onHover: {
|
||||
enable: true,
|
||||
mode: "repulse"
|
||||
}
|
||||
},
|
||||
modes: {
|
||||
push: {
|
||||
quantity: 4
|
||||
},
|
||||
repulse: {
|
||||
distance: 100,
|
||||
duration: 5
|
||||
}
|
||||
}
|
||||
},
|
||||
particles: {
|
||||
color: {
|
||||
value: [
|
||||
"#FF00A1",
|
||||
"#0310FF",
|
||||
"#2DE2FF",
|
||||
"#6E00FF",
|
||||
"#0310FF",
|
||||
"#FF00A1"
|
||||
]
|
||||
},
|
||||
links: {
|
||||
color: "#ffffff",
|
||||
distance: 150,
|
||||
enable: true,
|
||||
opacity: 0.0,
|
||||
width: 0
|
||||
},
|
||||
move: {
|
||||
direction: "none",
|
||||
enable: true,
|
||||
outModes: {
|
||||
default: "out"
|
||||
},
|
||||
random: true,
|
||||
speed: 1,
|
||||
straight: false
|
||||
},
|
||||
number: {
|
||||
density: {
|
||||
enable: true
|
||||
},
|
||||
value: 400
|
||||
},
|
||||
opacity: {
|
||||
value: 1
|
||||
},
|
||||
shape: {
|
||||
type: "circle"
|
||||
},
|
||||
size: {
|
||||
value: { min: 100, max: 300 }
|
||||
}
|
||||
},
|
||||
detectRetina: true
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
if (init) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: "100vw",
|
||||
height: "var(--unit-100vh)",
|
||||
position: "absolute",
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Particles
|
||||
id="tsparticles"
|
||||
particlesLoaded={particlesLoaded}
|
||||
options={options}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
background: "rgba(255, 255, 255, 0.0)",
|
||||
backdropFilter: "blur(50px)",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
display: "inline"
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default ParticlesBackground;
|
||||
@ -1,224 +0,0 @@
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useMediaQuery } from "react-responsive";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { TemplateProps } from "keycloakify/login/TemplateProps";
|
||||
import { useSetClassName } from "keycloakify/tools/useSetClassName";
|
||||
import { useInitialize } from "keycloakify/login/Template.useInitialize";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { Layout, Typography, Dropdown, Button, Alert, Space, Divider, ConfigProvider, Flex, theme } from "antd";
|
||||
import { GlobalOutlined } from "@ant-design/icons";
|
||||
import ParticlesBackground from "./ParticlesBackground";
|
||||
|
||||
const { Sider, Content } = Layout;
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
const {
|
||||
displayInfo = false,
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
headerNode,
|
||||
infoNode = null,
|
||||
documentTitle,
|
||||
bodyClassName,
|
||||
kcContext,
|
||||
i18n,
|
||||
doUseDefaultCss,
|
||||
children
|
||||
} = props;
|
||||
|
||||
const { msg, msgStr, currentLanguage, enabledLanguages } = i18n;
|
||||
|
||||
const { realm, auth, url, message, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const isMobile = useMediaQuery({ maxWidth: 600 });
|
||||
|
||||
const [darkMode, setDarkMode] = useState(false);
|
||||
const windowQuery = window.matchMedia("(prefers-color-scheme:dark)");
|
||||
|
||||
const darkModeChange = useCallback((event: MediaQueryListEvent) => {
|
||||
console.log(event.matches ? true : false);
|
||||
setDarkMode(event.matches ? true : false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
windowQuery.addEventListener("change", darkModeChange);
|
||||
return () => {
|
||||
windowQuery.removeEventListener("change", darkModeChange);
|
||||
};
|
||||
}, [windowQuery, darkModeChange]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(windowQuery.matches ? true : false);
|
||||
setDarkMode(windowQuery.matches ? true : false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.title = documentTitle ?? msgStr("loginTitle", realm.displayName);
|
||||
}, []);
|
||||
|
||||
useSetClassName({
|
||||
qualifiedName: "body",
|
||||
className: bodyClassName ?? ""
|
||||
});
|
||||
|
||||
const { isReadyToRender } = useInitialize({ kcContext, doUseDefaultCss });
|
||||
|
||||
if (!isReadyToRender) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Language menu items for dropdown
|
||||
const languageItems = {
|
||||
items: enabledLanguages.map((language, index) => ({
|
||||
key: `language-${index}`,
|
||||
label: <a href={language.href}>{language.label}</a>
|
||||
}))
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "rgba(212, 0, 255, 1)",
|
||||
colorLink: "#6E00FF",
|
||||
colorLinkHover: "#b175ff"
|
||||
},
|
||||
algorithm: darkMode ? theme.darkAlgorithm : theme.defaultAlgorithm
|
||||
}}
|
||||
>
|
||||
<Layout style={{ minHeight: "var(--unit-100vh)", maxHeight: "var(--unit-100vh)" }}>
|
||||
<ParticlesBackground />
|
||||
<Sider
|
||||
width={isMobile ? "100%" : "500px"}
|
||||
style={{
|
||||
backgroundColor: darkMode ? "#141414" : "#fff",
|
||||
boxShadow: "2px 0 8px rgba(0,0,0,0.1)",
|
||||
padding: "40px 40px",
|
||||
paddingRight: "20px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
zIndex: 1
|
||||
}}
|
||||
>
|
||||
<Flex style={{ height: "100%" }} vertical>
|
||||
<div style={{ marginRight: "20px" }}>
|
||||
<img src="https://cdn.tombutcher.work/logos/logo-auth.png" alt="Logo" style={{ width: "220px" }} />
|
||||
<Divider style={{ margin: "24px 0" }} />
|
||||
|
||||
<div style={{ marginBottom: "24px" }}>
|
||||
{(() => {
|
||||
const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
<Title level={2} style={{ marginBottom: "16px" }}>
|
||||
{headerNode}
|
||||
</Title>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: "24px" }}>
|
||||
<Flex gap={"small"}>
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/c-person.svg"}
|
||||
width={14}
|
||||
style={{ marginTop: "3px" }}
|
||||
/>
|
||||
<Text strong>{auth.attemptedUsername}</Text>
|
||||
</Flex>
|
||||
<Button
|
||||
type="link"
|
||||
href={url.loginRestartFlowUrl}
|
||||
title={msgStr("restartLoginTooltip")}
|
||||
style={{ marginLeft: "12px" }}
|
||||
>
|
||||
{msg("restartLoginTooltip")}
|
||||
</Button>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
);
|
||||
|
||||
if (displayRequiredFields) {
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: "12px" }}>
|
||||
<Text type="secondary">
|
||||
<span style={{ color: "#ff4d4f" }}>*</span> {msg("requiredFields")}
|
||||
</Text>
|
||||
</div>
|
||||
{node}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<Alert
|
||||
message={<span dangerouslySetInnerHTML={{ __html: kcSanitize(message.summary) }} />}
|
||||
type={
|
||||
message.type === "error"
|
||||
? "error"
|
||||
: message.type === "success"
|
||||
? "success"
|
||||
: message.type === "warning"
|
||||
? "warning"
|
||||
: "info"
|
||||
}
|
||||
showIcon
|
||||
style={{ marginBottom: "24px" }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ overflowY: "auto", paddingRight: "20px", height: "100%" }}>
|
||||
{children}
|
||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<div style={{ textAlign: "center", margin: "16px 0" }}>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => {
|
||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{msg("doTryAnotherWay")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
{displayInfo && <div style={{ marginTop: "24px", textAlign: "center" }}>{infoNode}</div>}
|
||||
</div>
|
||||
|
||||
{enabledLanguages.length > 1 && (
|
||||
<div style={{ paddingRight: "20px" }}>
|
||||
<Divider style={{ margin: "24px 0" }} />
|
||||
<Dropdown menu={languageItems} trigger={["click"]}>
|
||||
<Button style={{ width: "100%", textAlign: "left" }}>
|
||||
<Space>
|
||||
{currentLanguage.label}
|
||||
<GlobalOutlined />
|
||||
</Space>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Flex>
|
||||
</Sider>
|
||||
|
||||
<Content
|
||||
style={{
|
||||
background: "#f5f5f5",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
></Content>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@ -1,536 +0,0 @@
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||
import {
|
||||
useUserProfileForm,
|
||||
getButtonToDisplayForMultivaluedAttributeField,
|
||||
type FormAction,
|
||||
type FormFieldError
|
||||
} from "keycloakify/login/lib/useUserProfileForm";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { Attribute } from "keycloakify/login/KcContext";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import type { I18n } from "./i18n";
|
||||
import { Form, FormItemProps, Input, InputProps, Typography, Select, Radio, Checkbox, Button, Space, Divider } from "antd";
|
||||
import { EyeOutlined, EyeInvisibleOutlined, PlusOutlined, MinusOutlined } from "@ant-design/icons";
|
||||
|
||||
const { Text, Title } = Typography;
|
||||
const { TextArea } = Input;
|
||||
const { Group: RadioGroup } = Radio;
|
||||
const { Group: CheckboxGroup } = Checkbox;
|
||||
|
||||
export default function UserProfileFormFields(props: UserProfileFormFieldsProps<KcContext, I18n> & FormItemProps) {
|
||||
const { kcContext, i18n, kcClsx, doMakeUserConfirmPassword, BeforeField, AfterField, ...rest } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
const {
|
||||
formState: { formFieldStates },
|
||||
dispatchFormAction
|
||||
} = useUserProfileForm({
|
||||
kcContext,
|
||||
i18n,
|
||||
doMakeUserConfirmPassword
|
||||
});
|
||||
|
||||
const groupNameRef = { current: "" };
|
||||
|
||||
return (
|
||||
<>
|
||||
{formFieldStates.map(({ attribute, displayableErrors, valueOrValues }) => {
|
||||
return (
|
||||
<>
|
||||
<GroupLabel attribute={attribute} groupNameRef={groupNameRef} i18n={i18n} />
|
||||
<Form.Item
|
||||
label={advancedMsg(attribute.displayName ?? "")}
|
||||
validateStatus={displayableErrors.length > 0 ? "error" : undefined}
|
||||
help={
|
||||
<>
|
||||
{attribute.annotations.inputHelperTextBefore !== undefined && (
|
||||
<div id={`form-help-text-before-${attribute.name}`} aria-live="polite">
|
||||
{advancedMsg(attribute.annotations.inputHelperTextBefore)}
|
||||
</div>
|
||||
)}
|
||||
<FieldErrors attribute={attribute} displayableErrors={displayableErrors} fieldIndex={undefined} />
|
||||
{attribute.annotations.inputHelperTextAfter !== undefined && (
|
||||
<div id={`form-help-text-after-${attribute.name}`} aria-live="polite">
|
||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
rules={[{ required: true }]}
|
||||
name={attribute.name}
|
||||
shouldUpdate
|
||||
key={attribute.name}
|
||||
{...rest}
|
||||
>
|
||||
{BeforeField !== undefined && (
|
||||
<BeforeField
|
||||
attribute={attribute}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
displayableErrors={displayableErrors}
|
||||
valueOrValues={valueOrValues}
|
||||
kcClsx={kcClsx}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
|
||||
<InputFieldByType
|
||||
attribute={attribute}
|
||||
valueOrValues={valueOrValues}
|
||||
displayableErrors={displayableErrors}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
i18n={i18n}
|
||||
/>
|
||||
{AfterField !== undefined && (
|
||||
<AfterField
|
||||
attribute={attribute}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
displayableErrors={displayableErrors}
|
||||
valueOrValues={valueOrValues}
|
||||
kcClsx={kcClsx}
|
||||
i18n={i18n}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupLabel(props: {
|
||||
attribute: Attribute;
|
||||
groupNameRef: {
|
||||
current: string;
|
||||
};
|
||||
i18n: I18n;
|
||||
}) {
|
||||
const { attribute, groupNameRef, i18n } = props;
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
if (attribute.group?.name !== groupNameRef.current) {
|
||||
groupNameRef.current = attribute.group?.name ?? "";
|
||||
|
||||
if (groupNameRef.current !== "") {
|
||||
assert(attribute.group !== undefined);
|
||||
|
||||
return (
|
||||
<div {...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 (
|
||||
<>
|
||||
<Divider />
|
||||
<Title level={4} id={`header-${attribute.group.name}`}>
|
||||
{groupHeaderText}
|
||||
</Title>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
{(() => {
|
||||
const groupDisplayDescription = attribute.group.displayDescription ?? "";
|
||||
|
||||
if (groupDisplayDescription !== "") {
|
||||
const groupDescriptionText = advancedMsg(groupDisplayDescription);
|
||||
|
||||
return <Text id={`description-${attribute.group.name}`}>{groupDescriptionText}</Text>;
|
||||
}
|
||||
|
||||
return null;
|
||||
})()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function FieldErrors(props: { attribute: Attribute; displayableErrors: FormFieldError[]; fieldIndex: number | undefined }) {
|
||||
const { attribute, fieldIndex } = props;
|
||||
|
||||
const displayableErrors = props.displayableErrors.filter(error => error.fieldIndex === fieldIndex);
|
||||
|
||||
if (displayableErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id={`input-error-${attribute.name}${fieldIndex === undefined ? "" : `-${fieldIndex}`}`} aria-live="polite">
|
||||
{displayableErrors
|
||||
.filter(error => error.fieldIndex === fieldIndex)
|
||||
.map(({ errorMessage }, i) => (
|
||||
<span key={i}>{errorMessage}</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type InputFieldByTypeProps = {
|
||||
attribute: Attribute;
|
||||
valueOrValues: string | string[];
|
||||
displayableErrors: FormFieldError[];
|
||||
dispatchFormAction: React.Dispatch<FormAction>;
|
||||
i18n: I18n;
|
||||
};
|
||||
|
||||
function InputFieldByType(props: InputFieldByTypeProps & InputProps) {
|
||||
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 i18n={props.i18n} passwordInputId={attribute.name}>
|
||||
{inputNode}
|
||||
</PasswordWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return inputNode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function PasswordWrapper(props: { i18n: I18n; passwordInputId: string; children: JSX.Element }) {
|
||||
const { passwordInputId } = props;
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// const { msgStr } = i18n;
|
||||
|
||||
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||
|
||||
return (
|
||||
<Input.Password
|
||||
id={passwordInputId}
|
||||
iconRender={visible => (visible ? <EyeOutlined /> : <EyeInvisibleOutlined />)}
|
||||
onChange={e => form.setFieldsValue({ [passwordInputId]: e.target.value })}
|
||||
visibilityToggle={{
|
||||
visible: isPasswordRevealed,
|
||||
onVisibleChange: toggleIsPasswordRevealed
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputTag(props: InputFieldByTypeProps & { fieldIndex: number | undefined } & InputProps) {
|
||||
const { attribute, fieldIndex, dispatchFormAction, valueOrValues, i18n, displayableErrors, ...rest } = props;
|
||||
|
||||
const { advancedMsgStr } = i18n;
|
||||
|
||||
const inputValue = (() => {
|
||||
if (fieldIndex !== undefined) {
|
||||
assert(valueOrValues instanceof Array);
|
||||
return valueOrValues[fieldIndex];
|
||||
}
|
||||
|
||||
assert(typeof valueOrValues === "string");
|
||||
return valueOrValues;
|
||||
})();
|
||||
|
||||
const inputType = (() => {
|
||||
const { inputType } = attribute.annotations;
|
||||
|
||||
if (inputType?.startsWith("html5-")) {
|
||||
return inputType.slice(6);
|
||||
}
|
||||
|
||||
return inputType ?? "text";
|
||||
})();
|
||||
|
||||
// For password fields, we're using the PasswordWrapper component
|
||||
if (attribute.name === "password" || attribute.name === "password-confirm") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const inputElement = (
|
||||
<Input
|
||||
form=""
|
||||
type={inputType}
|
||||
disabled={attribute.readOnly}
|
||||
defaultValue={inputValue}
|
||||
placeholder={
|
||||
attribute.annotations.inputTypePlaceholder === undefined ? undefined : advancedMsgStr(attribute.annotations.inputTypePlaceholder)
|
||||
}
|
||||
pattern={attribute.annotations.inputTypePattern}
|
||||
min={attribute.annotations.inputTypeMin}
|
||||
max={attribute.annotations.inputTypeMax}
|
||||
step={attribute.annotations.inputTypeStep}
|
||||
status={displayableErrors.find(error => error.fieldIndex === fieldIndex) ? "error" : undefined}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{inputElement}
|
||||
{(() => {
|
||||
if (fieldIndex === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assert(valueOrValues instanceof Array);
|
||||
const values = valueOrValues;
|
||||
|
||||
return (
|
||||
<>
|
||||
<FieldErrors attribute={attribute} displayableErrors={displayableErrors} fieldIndex={fieldIndex} />
|
||||
<AddRemoveButtonsMultiValuedAttribute
|
||||
attribute={attribute}
|
||||
values={values}
|
||||
fieldIndex={fieldIndex}
|
||||
dispatchFormAction={dispatchFormAction}
|
||||
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 });
|
||||
|
||||
return (
|
||||
<Space>
|
||||
{hasRemove && (
|
||||
<Button
|
||||
id={`kc-remove-${attribute.name}-${fieldIndex + 1}`}
|
||||
type="link"
|
||||
icon={<MinusOutlined />}
|
||||
onClick={() =>
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: values.filter((_, i) => i !== fieldIndex)
|
||||
})
|
||||
}
|
||||
>
|
||||
{msg("remove")}
|
||||
</Button>
|
||||
)}
|
||||
{hasAdd && (
|
||||
<Button
|
||||
id={`kc-add-${attribute.name}-${fieldIndex + 1}`}
|
||||
type="link"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() =>
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: [...values, ""]
|
||||
})
|
||||
}
|
||||
>
|
||||
{msg("addValue")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
function InputTagSelects(props: InputFieldByTypeProps) {
|
||||
const { attribute, dispatchFormAction, i18n, valueOrValues } = props;
|
||||
|
||||
const { inputType } = attribute.annotations;
|
||||
|
||||
assert(inputType === "select-radiobuttons" || inputType === "multiselect-checkboxes");
|
||||
|
||||
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 ?? [];
|
||||
})();
|
||||
|
||||
const antOptions = options.map(option => ({
|
||||
label: inputLabel(i18n, attribute, option),
|
||||
value: option
|
||||
}));
|
||||
|
||||
const handleChange = (checkedValue: string | string[]) => {
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: checkedValue
|
||||
});
|
||||
};
|
||||
|
||||
if (inputType === "select-radiobuttons") {
|
||||
return (
|
||||
<RadioGroup
|
||||
name={attribute.name}
|
||||
options={antOptions}
|
||||
defaultValue={valueOrValues as string}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
disabled={attribute.readOnly}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// For multiselect-checkboxes
|
||||
return (
|
||||
<CheckboxGroup
|
||||
name={attribute.name}
|
||||
options={antOptions}
|
||||
defaultValue={valueOrValues as string[]}
|
||||
onChange={checkedValues => handleChange(checkedValues as string[])}
|
||||
disabled={attribute.readOnly}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function TextareaTag(props: InputFieldByTypeProps) {
|
||||
const { attribute, dispatchFormAction, displayableErrors, valueOrValues } = props;
|
||||
|
||||
assert(typeof valueOrValues === "string");
|
||||
|
||||
const value = valueOrValues;
|
||||
|
||||
return (
|
||||
<TextArea
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
status={displayableErrors.length > 0 ? "error" : undefined}
|
||||
disabled={attribute.readOnly}
|
||||
rows={attribute.annotations.inputTypeRows === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeRows}`)}
|
||||
maxLength={attribute.annotations.inputTypeMaxlength === undefined ? undefined : parseInt(`${attribute.annotations.inputTypeMaxlength}`)}
|
||||
defaultValue={value}
|
||||
onChange={event =>
|
||||
dispatchFormAction({
|
||||
action: "update",
|
||||
name: attribute.name,
|
||||
valueOrValues: event.target.value
|
||||
})
|
||||
}
|
||||
onBlur={() =>
|
||||
dispatchFormAction({
|
||||
action: "focus lost",
|
||||
name: attribute.name,
|
||||
fieldIndex: undefined
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTag(props: InputFieldByTypeProps) {
|
||||
const { attribute, displayableErrors, i18n, valueOrValues, ...rest } = props;
|
||||
|
||||
const isMultiple = attribute.annotations.inputType === "multiselect";
|
||||
|
||||
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 (
|
||||
<Select
|
||||
id={attribute.name}
|
||||
status={displayableErrors.length > 0 ? "error" : undefined}
|
||||
disabled={attribute.readOnly}
|
||||
mode={isMultiple ? "multiple" : undefined}
|
||||
size={attribute.annotations.inputTypeSize === undefined ? undefined : undefined} // Ant Design uses 'large', 'middle', 'small'
|
||||
defaultValue={valueOrValues}
|
||||
placeholder={isMultiple ? undefined : ""}
|
||||
{...rest}
|
||||
>
|
||||
{options.map(option => (
|
||||
<Select.Option key={option} value={option}>
|
||||
{inputLabel(i18n, attribute, option)}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
function inputLabel(i18n: I18n, attribute: Attribute, option: string) {
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
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;
|
||||
}
|
||||
@ -1,204 +0,0 @@
|
||||
:root {
|
||||
--unit-100vh: 100vh;
|
||||
}
|
||||
@supports (height: 100dvh) {
|
||||
:root {
|
||||
--unit-100vh: 100dvh;
|
||||
}
|
||||
}
|
||||
|
||||
/* Targeting the scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 1px; /* Width of the scrollbar */
|
||||
height: 10px; /* Height of the scrollbar for horizontal scrolling */
|
||||
}
|
||||
|
||||
/* The track of the scrollbar (background) */
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent; /* Light grey background */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* The handle of the scrollbar (the draggable part) */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #80808020; /* Grey color for the scrollbar thumb */
|
||||
border-radius: 10px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
/* Hover effect on the scrollbar thumb */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #80808080;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
h2 span {
|
||||
background: rgb(110, 0, 255);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(110, 0, 255, 1) 0%,
|
||||
rgba(212, 0, 255, 1) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.ant-typography a,
|
||||
a.ant-typography,
|
||||
.ant-alert-message a,
|
||||
.ant-btn-color-link.ant-btn-variant-link,
|
||||
.ant-btn-color-link.ant-btn-variant-link:hover,
|
||||
.ant-typography code {
|
||||
color: black;
|
||||
background: rgb(110, 0, 255);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(110, 0, 255, 1) 0%,
|
||||
rgba(212, 0, 255, 1) 100%
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.ant-checkbox-checked .ant-checkbox-inner {
|
||||
background: rgb(110, 0, 255);
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
rgba(160, 0, 255, 1) 0%,
|
||||
rgba(212, 0, 255, 1) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.ant-checkbox:not(.ant-checkbox-disabled):hover .ant-checkbox-inner {
|
||||
}
|
||||
|
||||
.kctbform > div,
|
||||
.kctbform > div {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.kctbform > div > div,
|
||||
.kctbform > div > div {
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kctbform > div > div > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.kctbform > div > div > div > input {
|
||||
border-top-right-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.kctbform > div > div > div > button {
|
||||
background: #ffffff;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #d9d9d9;
|
||||
border-left: 0;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.kctbform > div > div > div > button i {
|
||||
background-image: url("/eye.svg");
|
||||
width: 14px;
|
||||
display: block;
|
||||
height: 9px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.kctbform div > div input,
|
||||
.kctbform div > div input {
|
||||
background: #ffffff;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: #d9d9d9;
|
||||
padding: 4px 11px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
line-height: 1.5714285714285714;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.kctbform div span > span,
|
||||
.kctbform div > div span > span {
|
||||
color: rgb(255, 77, 79);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.icon24 {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.kcAuthenticatorOTPClass {
|
||||
background-image: url("https://cdn.tombutcher.work/icons/auth/c-phone-v2.svg");
|
||||
}
|
||||
|
||||
.kcAuthenticatorWebAuthnClass {
|
||||
background-image: url("https://cdn.tombutcher.work/icons/auth/c-passkey-v2.svg");
|
||||
}
|
||||
|
||||
.kcAuthenticatorPasswordClass {
|
||||
background-image: url("https://cdn.tombutcher.work/icons/auth/c-pencil-v2.svg");
|
||||
}
|
||||
|
||||
.kcAuthenticatorWebAuthnPasswordlessClass {
|
||||
background-image: url("https://cdn.tombutcher.work/icons/auth/c-passkey-v2.svg");
|
||||
}
|
||||
|
||||
.kcAuthenticatorDefaultClass {
|
||||
background-image: url("https://cdn.tombutcher.work/icons/auth/c-default-v2.svg");
|
||||
}
|
||||
|
||||
.ant-btn-icon > img {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ant-steps-item
|
||||
> .ant-steps-item-container
|
||||
> .ant-steps-item-content
|
||||
> .ant-steps-item-title::after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-steps
|
||||
.ant-steps-item-finish
|
||||
> .ant-steps-item-container
|
||||
> .ant-steps-item-tail::after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.ant-steps
|
||||
.ant-steps-item-finish
|
||||
> .ant-steps-item-container
|
||||
> .ant-steps-item-tail::after {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: #ffffff;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
box-shadow: inset 0 0 20px 20px #23232329;
|
||||
}
|
||||
@ -1,317 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Black-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Bold-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Heavy-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-weight: 200;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Light-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Medium-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Regular-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Semibold-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Thin-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Pro-Rounded-Ultralight-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Grold-Rounded-Slim";
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/Grold-Rounded-Slim-Bold-Subset.otf")
|
||||
format("truetype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Black-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Black-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Black-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Bold-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Bold-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Bold-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Heavy-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Heavy-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Heavy-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Light-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Light-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Light-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Medium-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Medium-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Medium-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Regular-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Regular-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Regular-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Semibold-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Semibold-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Semibold-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Thin-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Thin-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Thin-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "SF-Compact-Rounded";
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
src:
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Ultralight-Subset.woff2")
|
||||
format("woff2"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Ultralight-Subset.woff")
|
||||
format("woff"),
|
||||
url("https://cdn.tombutcher.work/fonts/SF-Compact-Rounded-Ultralight-Subset.otf")
|
||||
format("opentype");
|
||||
}
|
||||
|
||||
p,
|
||||
span,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
input,
|
||||
a,
|
||||
strong {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
font-family: "SF-Pro-Rounded" !important;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
p,
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
h4 span {
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h4 {
|
||||
line-height: 1 !important;
|
||||
margin-bottom: 0.5em !important;
|
||||
}
|
||||
|
||||
h2 span,
|
||||
h2 span * {
|
||||
font-family: "Grold-Rounded-Slim" !important;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
line-height: 0.9px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
h1 span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
h4 span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ant-alert-message * {
|
||||
font-family: "SF-Pro-Rounded";
|
||||
letter-spacing: 0.04em;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ant-alert-message span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-steps-item-title {
|
||||
line-height: 1.5 !important;
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.ant-typography h3 {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
@ -1,56 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "code.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/code.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithErrorCode: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
code: {
|
||||
success: false,
|
||||
error: "Failed to generate code"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithFrenchLanguage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: {
|
||||
currentLanguageTag: "fr"
|
||||
},
|
||||
code: {
|
||||
success: true,
|
||||
code: "XYZ789"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithHtmlErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
code: {
|
||||
success: false,
|
||||
error: "Something went wrong. <a href='https://example.com'>Try again</a>"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,52 +0,0 @@
|
||||
import { Typography, Alert, Space } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>): JSX.Element {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { code } = kcContext;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={code.success ? msg("codeSuccessTitle") : msg("codeErrorTitle", code.error)}
|
||||
>
|
||||
{code.success ? (
|
||||
<>
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Space>
|
||||
<Paragraph style={{ margin: 0 }}>{msg("copyCodeInstruction")}</Paragraph>
|
||||
</Space>
|
||||
|
||||
<Text strong code style={{ fontSize: "18px" }}>
|
||||
{code.code}
|
||||
</Text>
|
||||
</Space>
|
||||
</>
|
||||
) : (
|
||||
code.error && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={
|
||||
<div
|
||||
id="error"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(code.error)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "delete-account-confirm.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/delete-account-confirm.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithAIAFlow: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
triggered_from_aia: true,
|
||||
url: { loginAction: "/login-action" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithoutAIAFlow: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
triggered_from_aia: false,
|
||||
url: { loginAction: "/login-action" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithCustomButtonStyle: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
triggered_from_aia: true,
|
||||
url: { loginAction: "/login-action" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,112 +0,0 @@
|
||||
import { Form, Button, Typography, Alert, Divider, List, Flex, FormProps } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { useState } from "react";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>, I18n>): JSX.Element {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const [form] = Form.useForm();
|
||||
type FieldType = {
|
||||
"cancel-aia": string;
|
||||
};
|
||||
|
||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||
const [isCancelLoading, setIsCancelLoading] = useState(false);
|
||||
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async values => {
|
||||
console.log("Submitting form:", values);
|
||||
|
||||
// Create a new form element
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.loginAction; // URL where the form should be submitted
|
||||
|
||||
// Append input fields to the form
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden"; // Hidden inputs for all form values
|
||||
input.name = key;
|
||||
input.value = value.toString();
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
// Append form to the body and submit
|
||||
document.body.appendChild(form);
|
||||
form.submit(); // Simulate actual form submission
|
||||
};
|
||||
|
||||
const handleSubmit = (values: FieldType) => {
|
||||
setIsSubmitLoading(true);
|
||||
onFinish(values);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsCancelLoading(true);
|
||||
onFinish({ "cancel-aia": "true" });
|
||||
};
|
||||
|
||||
const { url, triggered_from_aia } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("deleteAccountConfirm")}>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} style={{ width: "100%" }}>
|
||||
<Alert message={msg("irreversibleAction")} type="warning" showIcon style={{ marginBottom: 16 }} />
|
||||
|
||||
<Text>{msg("deletingImplies")}</Text>
|
||||
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={[msg("loggingOutImmediately"), msg("errasingData")]}
|
||||
renderItem={item => (
|
||||
<List.Item style={{ padding: "8px 0" }}>
|
||||
<Text type="secondary">• {item}</Text>
|
||||
</List.Item>
|
||||
)}
|
||||
style={{ margin: "16px 0" }}
|
||||
/>
|
||||
|
||||
<Text strong style={{ marginTop: 8 }}>
|
||||
{msg("finalDeletionConfirmation")}
|
||||
</Text>
|
||||
<Divider />
|
||||
|
||||
<Flex gap={"middle"}>
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ flexGrow: 2 }}
|
||||
danger
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
disabled={isSubmitLoading || isCancelLoading}
|
||||
loading={isSubmitLoading}
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-bin.svg"}
|
||||
width={14}
|
||||
style={{ marginTop: "0px", marginBottom: "3px" }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{msgStr("doConfirmDelete")}
|
||||
</Button>
|
||||
|
||||
{triggered_from_aia && (
|
||||
<Button
|
||||
size="large"
|
||||
style={{ flexGrow: 1 }}
|
||||
disabled={isSubmitLoading || isCancelLoading}
|
||||
loading={isSubmitLoading}
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{msgStr("doCancel")}
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "delete-credential.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/delete-credential.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithCustomCredentialLabel: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
credentialLabel: "Test Credential",
|
||||
url: { loginAction: "/login-action" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,51 +0,0 @@
|
||||
import { Button, Flex, Alert, Divider } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { msgStr, msg } = i18n;
|
||||
const { url, credentialLabel } = kcContext;
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
displayMessage={false}
|
||||
headerNode={msg("deleteCredentialTitle", credentialLabel)}
|
||||
>
|
||||
<Alert
|
||||
message={msg("deleteCredentialMessage", credentialLabel)}
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{
|
||||
marginBottom: 24
|
||||
}}
|
||||
/>
|
||||
<Divider />
|
||||
<form action={url.loginAction} method="POST">
|
||||
<Flex gap="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ flexGrow: 2 }}
|
||||
danger
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
name="accept"
|
||||
icon={<img src={"/w-bin.svg"} width={14} style={{ marginTop: "0px", marginBottom: "3px" }} />}
|
||||
id="kc-accept"
|
||||
>
|
||||
{msgStr("doConfirmDelete")}
|
||||
</Button>
|
||||
|
||||
<Button size="large" style={{ flexGrow: 1 }} htmlType="submit" name="cancel-aia" id="kc-decline">
|
||||
{msgStr("doCancel")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "error.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/error.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithAnotherMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "With another error message" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithHtmlErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "<strong>Error:</strong> Something went wrong. <a href='https://example.com'>Go back</a>"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const FrenchError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
locale: { currentLanguageTag: "fr" },
|
||||
message: { summary: "Une erreur s'est produite" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithSkipLink: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: { summary: "An error occurred" },
|
||||
skipLink: true,
|
||||
client: {
|
||||
baseUrl: "https://example.com"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,36 +0,0 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Alert, Button, Typography, Space, Divider } from "antd";
|
||||
|
||||
const { Title } = Typography;
|
||||
|
||||
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { message, client, skipLink } = kcContext;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
displayMessage={false}
|
||||
headerNode={<Title level={2}>{msg("errorTitle")}</Title>}
|
||||
>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Alert message={<div dangerouslySetInnerHTML={{ __html: kcSanitize(message.summary) }} />} type="error" showIcon />
|
||||
</Space>
|
||||
{!skipLink && client !== undefined && client.baseUrl !== undefined && (
|
||||
<>
|
||||
<Divider />
|
||||
<Button type="primary" id="backToApplication" size={"large"} block href={client.baseUrl}>
|
||||
{msg("backToApplication")}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,28 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "frontchannel-logout.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/frontchannel-logout.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithoutRedirectUrl: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
logout: {
|
||||
clients: []
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,50 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Button, List, Typography, Space } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { logout } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
useEffect(() => {
|
||||
if (logout.logoutRedirectUri) {
|
||||
window.location.replace(logout.logoutRedirectUri);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
documentTitle={msgStr("frontchannel-logout.title")}
|
||||
headerNode={<Title level={3}>{msg("frontchannel-logout.title")}</Title>}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Paragraph>{msg("frontchannel-logout.message")}</Paragraph>
|
||||
|
||||
<List
|
||||
dataSource={logout.clients}
|
||||
renderItem={client => (
|
||||
<List.Item>
|
||||
<Typography.Text>{client.name}</Typography.Text>
|
||||
<iframe src={client.frontChannelLogoutUrl} style={{ display: "none" }} />
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
{logout.logoutRedirectUri && (
|
||||
<Button type="primary" id="continue" href={logout.logoutRedirectUri}>
|
||||
{msg("doContinue")}
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "idp-review-user-profile.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/idp-review-user-profile.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
export const WithFormValidationErrors: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messagesPerField: {
|
||||
existsError: (fieldName: string) => ["email", "firstName"].includes(fieldName),
|
||||
get: (fieldName: string) => {
|
||||
if (fieldName === "email") return "Invalid email format.";
|
||||
if (fieldName === "firstName") return "First name is required.";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithReadOnlyFields: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
email: { value: "jane.doe@example.com", readOnly: true },
|
||||
firstName: { value: "Jane", readOnly: false }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithPrefilledFormFields: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
firstName: { value: "Jane" },
|
||||
lastName: { value: "Doe" },
|
||||
email: { value: "jane.doe@example.com" }
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,53 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Form, Button, Space, Divider } from "antd";
|
||||
import type { JSX } from "keycloakify/tools/JSX";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { CheckOutlined } from "@ant-design/icons";
|
||||
|
||||
type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n> & {
|
||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||
doMakeUserConfirmPassword: boolean;
|
||||
};
|
||||
|
||||
export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, doMakeUserConfirmPassword } = props;
|
||||
const { msg, msgStr } = i18n;
|
||||
const { url, messagesPerField } = kcContext;
|
||||
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
displayMessage={messagesPerField.exists("global")}
|
||||
displayRequiredFields
|
||||
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||
>
|
||||
<Form id="kc-idp-review-profile-form" layout="vertical" method="post" action={url.loginAction} size="large">
|
||||
<div className="kctbform" style={{ paddingBottom: 0 }}>
|
||||
<UserProfileFormFields
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
onIsFormSubmittableValueChange={setIsFormSubmittable}
|
||||
kcClsx={() => ""} // Placeholder for compatibility
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
<Space direction="vertical" style={{ width: "100%" }}>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" icon={<CheckOutlined />} block size="large" disabled={!isFormSubmittable}>
|
||||
{msgStr("doSubmit")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,59 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "info.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/info.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messageHeader: "Message header",
|
||||
message: {
|
||||
summary: "Server info message"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithLinkBack: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messageHeader: "Message header",
|
||||
message: {
|
||||
summary: "Server message"
|
||||
},
|
||||
actionUri: undefined
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithRequiredActions: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messageHeader: "Message header",
|
||||
message: {
|
||||
summary: "Required actions: "
|
||||
},
|
||||
requiredActions: ["CONFIGURE_TOTP", "UPDATE_PROFILE", "VERIFY_EMAIL", "CUSTOM_ACTION"],
|
||||
"x-keycloakify": {
|
||||
messages: {
|
||||
"requiredAction.CUSTOM_ACTION": "Custom action"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,82 +0,0 @@
|
||||
import { Typography, Button, Space, Divider } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
const { advancedMsgStr, msg } = i18n;
|
||||
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
|
||||
|
||||
// Prepare the message content with required actions if present
|
||||
const getMessageContent = () => {
|
||||
let html = message.summary;
|
||||
if (requiredActions) {
|
||||
html += "<b>";
|
||||
html += requiredActions.map(requiredAction => advancedMsgStr(`requiredAction.${requiredAction}`)).join(", ");
|
||||
html += "</b>";
|
||||
}
|
||||
return html;
|
||||
};
|
||||
|
||||
// Determine which action link to show
|
||||
const renderActionLink = () => {
|
||||
if (skipLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pageRedirectUri) {
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Button type="primary" block size={"large"} href={pageRedirectUri}>
|
||||
{msg("backToApplication")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (actionUri) {
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Button type="primary" block size={"large"} href={actionUri}>
|
||||
{msg("proceedWithAction")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (client.baseUrl) {
|
||||
return (
|
||||
<>
|
||||
<Divider />
|
||||
<Button type="primary" block size={"large"} href={client.baseUrl}>
|
||||
{msg("backToApplication")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
displayMessage={false}
|
||||
headerNode={<Title level={3}>{messageHeader}</Title>}
|
||||
>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Text className="instruction">
|
||||
<span dangerouslySetInnerHTML={{ __html: getMessageContent() }}></span>
|
||||
</Text>
|
||||
</Space>
|
||||
{renderActionLink()}
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,360 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithInvalidCredential: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
login: {
|
||||
username: "johndoe"
|
||||
},
|
||||
messagesPerField: {
|
||||
// NOTE: The other functions of messagesPerField are derived from get() and
|
||||
// existsError() so they are the only ones that need to mock.
|
||||
existsError: (fieldName: string, ...otherFieldNames: string[]) => {
|
||||
const fieldNames = [fieldName, ...otherFieldNames];
|
||||
return fieldNames.includes("username") || fieldNames.includes("password");
|
||||
},
|
||||
get: (fieldName: string) => {
|
||||
if (fieldName === "username" || fieldName === "password") {
|
||||
return "Invalid username or password.";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutRegistration: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { registrationAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutRememberMe: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { rememberMe: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordReset: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { resetPasswordAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { loginWithEmailAllowed: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPresetUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
login: { username: "max.mustermann@mail.com" }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithImmutablePresetUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
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: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
},
|
||||
{
|
||||
loginUrl: "facebook",
|
||||
alias: "facebook",
|
||||
providerId: "facebook",
|
||||
displayName: "Facebook",
|
||||
iconClasses: "fa fa-facebook"
|
||||
},
|
||||
{
|
||||
loginUrl: "instagram",
|
||||
alias: "instagram",
|
||||
providerId: "instagram",
|
||||
displayName: "Instagram",
|
||||
iconClasses: "fa fa-instagram"
|
||||
},
|
||||
{
|
||||
loginUrl: "twitter",
|
||||
alias: "twitter",
|
||||
providerId: "twitter",
|
||||
displayName: "Twitter",
|
||||
iconClasses: "fa fa-twitter"
|
||||
},
|
||||
{
|
||||
loginUrl: "linkedin",
|
||||
alias: "linkedin",
|
||||
providerId: "linkedin",
|
||||
displayName: "LinkedIn",
|
||||
iconClasses: "fa fa-linkedin"
|
||||
},
|
||||
{
|
||||
loginUrl: "stackoverflow",
|
||||
alias: "stackoverflow",
|
||||
providerId: "stackoverflow",
|
||||
displayName: "Stackoverflow",
|
||||
iconClasses: "fa fa-stack-overflow"
|
||||
},
|
||||
{
|
||||
loginUrl: "github",
|
||||
alias: "github",
|
||||
providerId: "github",
|
||||
displayName: "Github",
|
||||
iconClasses: "fa fa-github"
|
||||
},
|
||||
{
|
||||
loginUrl: "gitlab",
|
||||
alias: "gitlab",
|
||||
providerId: "gitlab",
|
||||
displayName: "Gitlab",
|
||||
iconClasses: "fa fa-gitlab"
|
||||
},
|
||||
{
|
||||
loginUrl: "bitbucket",
|
||||
alias: "bitbucket",
|
||||
providerId: "bitbucket",
|
||||
displayName: "Bitbucket",
|
||||
iconClasses: "fa fa-bitbucket"
|
||||
},
|
||||
{
|
||||
loginUrl: "paypal",
|
||||
alias: "paypal",
|
||||
providerId: "paypal",
|
||||
displayName: "PayPal",
|
||||
iconClasses: "fa fa-paypal"
|
||||
},
|
||||
{
|
||||
loginUrl: "openshift",
|
||||
alias: "openshift",
|
||||
providerId: "openshift",
|
||||
displayName: "OpenShift",
|
||||
iconClasses: "fa fa-cloud"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPasswordField: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: { password: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "The time allotted for the connection has elapsed.<br/>The login process will restart from the beginning.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithOneSocialProvider: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithTwoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithNoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: []
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithMoreThanTwoSocialProviders: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
},
|
||||
{
|
||||
loginUrl: "microsoft",
|
||||
alias: "microsoft",
|
||||
providerId: "microsoft",
|
||||
displayName: "Microsoft",
|
||||
iconClasses: "fa fa-windows"
|
||||
},
|
||||
{
|
||||
loginUrl: "facebook",
|
||||
alias: "facebook",
|
||||
providerId: "facebook",
|
||||
displayName: "Facebook",
|
||||
iconClasses: "fa fa-facebook"
|
||||
},
|
||||
{
|
||||
loginUrl: "twitter",
|
||||
alias: "twitter",
|
||||
providerId: "twitter",
|
||||
displayName: "Twitter",
|
||||
iconClasses: "fa fa-twitter"
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithSocialProvidersAndWithoutRememberMe: Story = {
|
||||
render: args => (
|
||||
<KcPageStory
|
||||
{...args}
|
||||
kcContext={{
|
||||
social: {
|
||||
displayInfo: true,
|
||||
providers: [
|
||||
{
|
||||
loginUrl: "google",
|
||||
alias: "google",
|
||||
providerId: "google",
|
||||
displayName: "Google",
|
||||
iconClasses: "fa fa-google"
|
||||
}
|
||||
]
|
||||
},
|
||||
realm: { rememberMe: false }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,201 +0,0 @@
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Form, Input, Button, Checkbox, Space, Typography, Divider, Row, Col, FormProps } from "antd";
|
||||
import { EyeTwoTone, EyeInvisibleOutlined } from "@ant-design/icons";
|
||||
import * as FaIcons from "react-icons/fa6"; // Import all FA6 icons dynamically
|
||||
import type { IconType } from "react-icons";
|
||||
import { useState } from "react";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
type FieldType = {
|
||||
string: string;
|
||||
};
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async values => {
|
||||
console.log("Submitting form:", values);
|
||||
setIsLoading(true);
|
||||
|
||||
// Create a new form element
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.loginAction; // URL where the form should be submitted
|
||||
|
||||
// Append input fields to the form
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden"; // Hidden inputs for all form values
|
||||
input.name = key;
|
||||
input.value = value.toString();
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
// Append form to the body and submit
|
||||
document.body.appendChild(form);
|
||||
form.submit(); // Simulate actual form submission
|
||||
};
|
||||
|
||||
const { social, realm, url, usernameHidden, login, auth, registrationDisabled, client } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [form] = Form.useForm(); // Get the form instance
|
||||
|
||||
// Function to dynamically fetch the correct icon
|
||||
const getIconComponent = (alias: string): IconType | undefined => {
|
||||
const formattedAlias = alias
|
||||
.split("-") // Convert kebab-case to PascalCase
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join("");
|
||||
const iconName = `Fa${formattedAlias}`; // Construct the react-icons name
|
||||
return (FaIcons as Record<string, IconType>)[iconName];
|
||||
};
|
||||
|
||||
const inputPrefix = !realm.loginWithEmailAllowed ? (
|
||||
<img src={"https://cdn.tombutcher.work/icons/auth/c-at.svg"} width={14} style={{ marginRight: "3px" }} />
|
||||
) : (
|
||||
<img src={"https://cdn.tombutcher.work/icons/auth/c-person.svg"} width={14} style={{ marginRight: "3px" }} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
headerNode={
|
||||
<>
|
||||
{client.attributes.logoUri ? (
|
||||
<Space align="start" direction="vertical">
|
||||
<img
|
||||
src={client.attributes.logoUri}
|
||||
alt={client.name || client.clientId}
|
||||
style={{ maxHeight: "64px", maxWidth: "100%", marginBottom: "20px" }}
|
||||
/>
|
||||
<Title level={3}>{msg("loginAccountTitle")}</Title>
|
||||
</Space>
|
||||
) : (
|
||||
<Title level={3}>{msg("loginAccountTitle")}</Title>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
|
||||
infoNode={
|
||||
<Space>
|
||||
<Text>{msg("noAccount")}</Text>
|
||||
<Link href={url.registrationUrl}>{msg("doRegister")}</Link>
|
||||
</Space>
|
||||
}
|
||||
socialProvidersNode={
|
||||
<>
|
||||
{realm.password && social?.providers !== undefined && social.providers.length !== 0 && (
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Divider>
|
||||
<Text type="secondary">{msg("identity-provider-login-label")}</Text>
|
||||
</Divider>
|
||||
<Space wrap style={{ width: "100%", justifyContent: "center" }}>
|
||||
{social.providers.map(p => {
|
||||
const IconComponent = getIconComponent(p.alias);
|
||||
return (
|
||||
<Button
|
||||
key={p.alias}
|
||||
id={`social-${p.alias}`}
|
||||
href={p.loginUrl}
|
||||
icon={IconComponent ? <IconComponent /> : undefined}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: kcSanitize(p.displayName) }} />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{realm.password && (
|
||||
<Form
|
||||
form={form}
|
||||
initialValues={{ username: login.username || "", rememberMe: !!login.rememberMe }}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={onFinish}
|
||||
>
|
||||
<input type="hidden" name="credentialId" value={auth.selectedCredential} />
|
||||
|
||||
{!usernameHidden && (
|
||||
<Form.Item
|
||||
name="username"
|
||||
label={
|
||||
!realm.loginWithEmailAllowed
|
||||
? msg("username")
|
||||
: !realm.registrationEmailAsUsername
|
||||
? msg("usernameOrEmail")
|
||||
: msg("email")
|
||||
}
|
||||
>
|
||||
<Input id="username" name="username" size="large" prefix={inputPrefix} autoFocus autoComplete="username" />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="password" label={msg("password")}>
|
||||
<Input.Password
|
||||
id="password"
|
||||
name="password"
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-lock.svg"} style={{ marginRight: "3px" }} height={14} />}
|
||||
autoComplete="current-password"
|
||||
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Row justify="space-between" align="middle">
|
||||
{realm.rememberMe && !usernameHidden && (
|
||||
<Col>
|
||||
<Form.Item name="rememberMe" valuePropName="checked" noStyle>
|
||||
<Checkbox id="rememberMe" name="rememberMe">
|
||||
{msg("rememberMe")}
|
||||
</Checkbox>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{realm.resetPasswordAllowed && (
|
||||
<Col>
|
||||
<Link href={url.loginResetCredentialsUrl}>{msg("doForgotPassword")}</Link>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
<Divider />
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
id="kc-login"
|
||||
name="login"
|
||||
block
|
||||
size="large"
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
iconPosition={"end"}
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-right.svg"}
|
||||
style={{ marginTop: "4px", marginBottom: 0 }}
|
||||
height={14}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{msgStr("doLogIn")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-config-totp.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-config-totp.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
export const WithManualSetUp: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
mode: "manual"
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
messagesPerField: {
|
||||
get: (fieldName: string) => (fieldName === "totp" ? "Invalid TOTP" : undefined),
|
||||
exists: (fieldName: string) => fieldName === "totp",
|
||||
existsError: (fieldName: string) => fieldName === "totp",
|
||||
printIfExists: <T,>(fieldName: string, x: T) => (fieldName === "totp" ? x : undefined)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
export const WithAppInitiatedAction: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
isAppInitiatedAction: true
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPreFilledUserLabel: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
totp: {
|
||||
otpCredentials: [{ userLabel: "MyDevice" }]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,295 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { getKcClsx, KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Typography, List, Button, Form, Input, Checkbox, Space, Steps, Divider, Flex } from "antd";
|
||||
|
||||
const { Title, Text, Paragraph, Link } = Typography;
|
||||
|
||||
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
type FieldType = {
|
||||
"cancel-aia": string;
|
||||
};
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||
const [isCancelLoading, setIsCancelLoading] = useState(false);
|
||||
|
||||
const onFinish = (values: Record<string, string>) => {
|
||||
console.log("Submitting form:", values);
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.loginAction;
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
if (mode) {
|
||||
const modeInput = document.createElement("input");
|
||||
modeInput.type = "hidden";
|
||||
modeInput.id = "mode";
|
||||
modeInput.value = mode;
|
||||
form.appendChild(modeInput);
|
||||
}
|
||||
|
||||
const totpSecretInput = document.createElement("input");
|
||||
totpSecretInput.type = "hidden";
|
||||
totpSecretInput.name = "totpSecret";
|
||||
totpSecretInput.value = totp.totpSecret;
|
||||
form.appendChild(totpSecretInput);
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
const handleSubmit = (values: FieldType) => {
|
||||
setIsSubmitLoading(true);
|
||||
onFinish(values);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsCancelLoading(true);
|
||||
onFinish({ "cancel-aia": "true" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={<Title level={3}>{msg("loginTotpTitle")}</Title>}
|
||||
displayMessage={!messagesPerField.existsError("totp", "userLabel")}
|
||||
>
|
||||
<Steps
|
||||
direction="vertical"
|
||||
current={3}
|
||||
items={[
|
||||
{
|
||||
title: msg("loginTotpStep1"),
|
||||
icon: <img src={"https://cdn.tombutcher.work/icons/auth/c-download-128.svg"} height={14} />,
|
||||
description: (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Paragraph>{msg("loginTotpStep1")}</Paragraph>
|
||||
<List
|
||||
id="kc-totp-supported-apps"
|
||||
size="small"
|
||||
bordered
|
||||
dataSource={totp.supportedApplications}
|
||||
renderItem={app => (
|
||||
<List.Item>
|
||||
<Space>
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/c-phone.svg"}
|
||||
width={14}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
{advancedMsg(app)}
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: mode === "manual" ? msg("loginTotpManualStep2") : msg("loginTotpStep2"),
|
||||
icon: <img src={"https://cdn.tombutcher.work/icons/auth/c-qrcode-128.svg"} height={14} />,
|
||||
description: (
|
||||
<>
|
||||
{mode === "manual" ? (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Paragraph>{msg("loginTotpManualStep2")}</Paragraph>
|
||||
<Paragraph>
|
||||
<Text code>{totp.totpSecretEncoded}</Text>
|
||||
</Paragraph>
|
||||
<Paragraph>
|
||||
<Link href={totp.qrUrl} id="mode-barcode">
|
||||
{msg("loginTotpScanBarcode")}
|
||||
</Link>
|
||||
</Paragraph>
|
||||
<Divider />
|
||||
<Paragraph>{msg("loginTotpManualStep3")}</Paragraph>
|
||||
<List size="small" bordered>
|
||||
<List.Item id="kc-totp-type">
|
||||
<Text strong>{msg("loginTotpType")}:</Text> {msg(`loginTotp.${totp.policy.type}`)}
|
||||
</List.Item>
|
||||
<List.Item id="kc-totp-algorithm">
|
||||
<Text strong>{msg("loginTotpAlgorithm")}:</Text> {totp.policy.getAlgorithmKey()}
|
||||
</List.Item>
|
||||
<List.Item id="kc-totp-digits">
|
||||
<Text strong>{msg("loginTotpDigits")}:</Text> {totp.policy.digits}
|
||||
</List.Item>
|
||||
{totp.policy.type === "totp" ? (
|
||||
<List.Item id="kc-totp-period">
|
||||
<Text strong>{msg("loginTotpInterval")}:</Text> {totp.policy.period}
|
||||
</List.Item>
|
||||
) : (
|
||||
<List.Item id="kc-totp-counter">
|
||||
<Text strong>{msg("loginTotpCounter")}:</Text> {totp.policy.initialCounter}
|
||||
</List.Item>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<Paragraph>{msg("loginTotpStep2")}</Paragraph>
|
||||
<div style={{ textAlign: "center", margin: "20px 0" }}>
|
||||
<img
|
||||
id="kc-totp-secret-qr-code"
|
||||
src={`data:image/png;base64, ${totp.totpSecretQrCode}`}
|
||||
alt="QR Code"
|
||||
style={{ maxWidth: "200px" }}
|
||||
/>
|
||||
</div>
|
||||
<Paragraph>
|
||||
<Link href={totp.manualUrl} id="mode-manual">
|
||||
{msg("loginTotpUnableToScan")}
|
||||
</Link>
|
||||
</Paragraph>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: msg("loginTotpStep3"),
|
||||
icon: <img src={"https://cdn.tombutcher.work/icons/auth/c-check-128.svg"} height={14} />,
|
||||
description: (
|
||||
<div>
|
||||
<Paragraph>{msg("loginTotpStep3")}</Paragraph>
|
||||
<Text>{msg("loginTotpStep3DeviceName")}</Text>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Divider />
|
||||
<Form layout="vertical" onFinish={handleSubmit} style={{ margin: "0 auto" }}>
|
||||
<Form.Item
|
||||
label={
|
||||
<>
|
||||
{msg("authenticatorCode")} <span className="required">*</span>
|
||||
</>
|
||||
}
|
||||
name="totp"
|
||||
validateStatus={messagesPerField.existsError("totp") ? "error" : undefined}
|
||||
help={
|
||||
messagesPerField.existsError("totp") && (
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("totp"))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id="totp"
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-hash.svg"} width={14} style={{ marginRight: "3px" }} />}
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label={
|
||||
<>
|
||||
{msg("loginTotpDeviceName")} {totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
</>
|
||||
}
|
||||
name="userLabel"
|
||||
validateStatus={messagesPerField.existsError("userLabel") ? "error" : undefined}
|
||||
help={
|
||||
messagesPerField.existsError("userLabel") && (
|
||||
<span
|
||||
id="input-error-otp-label"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("userLabel"))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id="userLabel"
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-phone.svg"} width={14} style={{ marginRight: "3px" }} />}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} />
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.Item>
|
||||
<Flex gap={"middle"}>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
style={{ flexGrow: 2 }}
|
||||
size="large"
|
||||
id="saveTOTPBtn"
|
||||
iconPosition="end"
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-checkmark.svg"}
|
||||
style={{ marginTop: "0px", marginBottom: "3px" }}
|
||||
width={14}
|
||||
/>
|
||||
}
|
||||
loading={isSubmitLoading}
|
||||
disabled={isCancelLoading || isSubmitLoading}
|
||||
>
|
||||
{msgStr("doSubmit")}
|
||||
</Button>
|
||||
{isAppInitiatedAction ? (
|
||||
<Button
|
||||
type="default"
|
||||
htmlType="submit"
|
||||
size="large"
|
||||
style={{ flexGrow: 1 }}
|
||||
id="cancelTOTPBtn"
|
||||
onClick={handleCancel}
|
||||
loading={isCancelLoading}
|
||||
disabled={isCancelLoading || isSubmitLoading}
|
||||
>
|
||||
{msg("doCancel")}
|
||||
</Button>
|
||||
) : null}
|
||||
</Flex>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoutOtherSessions(props: { kcClsx: KcClsx; i18n: I18n }) {
|
||||
const { i18n } = props;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Checkbox id="logout-sessions" name="logout-sessions" value="on" defaultChecked={true}>
|
||||
{msg("logoutOtherSessions")}
|
||||
</Checkbox>
|
||||
);
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
// Mock kcContext to avoid the TS2304 error
|
||||
const mockKcContext = {
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
idpAlias: "mockIdpAlias"
|
||||
};
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-idp-link-confirm.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Default:
|
||||
* - Purpose: Tests standard behavior with mock data.
|
||||
* - Scenario: The component renders with a mocked identity provider alias (`mockIdpAlias`) and a login action URL (`/login-action`).
|
||||
* - Key Aspect: Ensures the default behavior of the component with standard values for kcContext.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory kcContext={mockKcContext} />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithFormSubmissionError:
|
||||
* - Purpose: Tests how the component handles form submission errors.
|
||||
* - Scenario: Simulates a form submission error by setting the login action URL to `/error` and displays an error message.
|
||||
* - Key Aspect: Verifies that the component can display error messages during form submission failure, ensuring proper error handling.
|
||||
*/
|
||||
export const WithFormSubmissionError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
url: {
|
||||
loginAction: "/error"
|
||||
},
|
||||
message: {
|
||||
type: "error",
|
||||
summary: "An error occurred during form submission."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,29 +0,0 @@
|
||||
import { Button, Divider, Form, Space } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
const { url, idpAlias } = kcContext;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={false} classes={{}} headerNode={msg("confirmLinkIdpTitle")}>
|
||||
<Divider />
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Form id="kc-register-form" action={url.loginAction} method="post">
|
||||
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||
<Button type="default" htmlType="submit" id="updateProfile" name="submitAction" value="updateProfile" block size="large">
|
||||
{msg("confirmLinkIdpReviewProfile")}
|
||||
</Button>
|
||||
|
||||
<Button type="primary" htmlType="submit" id="linkAccount" name="submitAction" value="linkAccount" block size="large">
|
||||
{msg("confirmLinkIdpContinue", idpAlias)}
|
||||
</Button>
|
||||
</Space>
|
||||
</Form>
|
||||
</Space>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm-override.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-idp-link-confirm-override.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
@ -1,106 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
// Mock kcContext to avoid TS2304 error and to simulate the real environment
|
||||
const mockKcContext = {
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
idpAlias: "mockIdpAlias",
|
||||
brokerContext: {
|
||||
username: "mockUser"
|
||||
},
|
||||
realm: {
|
||||
displayName: "MockRealm"
|
||||
}
|
||||
};
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-email.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-idp-link-email.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Default:
|
||||
* - Purpose: Tests the default behavior with mock data.
|
||||
* - Scenario: The component renders with a mocked identity provider alias (`mockIdpAlias`), a default broker username (`mockUser`), and a default realm name (`MockRealm`).
|
||||
* - Key Aspect: Ensures the default behavior of the component with typical kcContext values.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory kcContext={mockKcContext} />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithIdpAlias:
|
||||
* - Purpose: Tests behavior when the idpAlias is set to "Google".
|
||||
* - Scenario: Simulates the component being used with a Google identity provider, showing the username "john.doe" and realm "MyRealm".
|
||||
* - Key Aspect: Ensures the correct identity provider alias ("Google") and broker context (user info) are displayed in the email linking instructions.
|
||||
*/
|
||||
export const WithIdpAlias: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
idpAlias: "Google",
|
||||
brokerContext: {
|
||||
username: "john.doe"
|
||||
},
|
||||
realm: {
|
||||
displayName: "MyRealm"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithCustomRealmDisplayName:
|
||||
* - Purpose: Tests behavior when the realm display name is customized.
|
||||
* - Scenario: Simulates the component with a Facebook identity provider, a broker username "jane.doe", and a custom realm name "CustomRealm".
|
||||
* - Key Aspect: Ensures that custom realm display names are rendered correctly alongside the idpAlias and broker context.
|
||||
*/
|
||||
export const WithCustomRealmDisplayName: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
idpAlias: "Facebook",
|
||||
brokerContext: {
|
||||
username: "jane.doe"
|
||||
},
|
||||
realm: {
|
||||
displayName: "CUSTOM REALM DISPLAY NAME"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithFormSubmissionError:
|
||||
* - Purpose: Tests how the component handles form submission errors.
|
||||
* - Scenario: Simulates a form submission error by setting the login action URL to `/error` and displays an error message.
|
||||
* - Key Aspect: Verifies that the component can display error messages during form submission failure, ensuring proper error handling.
|
||||
*/
|
||||
export const WithFormSubmissionError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
url: {
|
||||
loginAction: "/error"
|
||||
},
|
||||
message: {
|
||||
type: "error",
|
||||
summary: "An error occurred during form submission."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,49 +0,0 @@
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Typography, Space, Button } from "antd";
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { url, realm, brokerContext, idpAlias } = kcContext;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={
|
||||
<Space align="center">
|
||||
<Title level={2} style={{ margin: 0 }}>
|
||||
{msg("emailLinkIdpTitle", idpAlias)}
|
||||
</Title>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Text id="instruction1" className="instruction">
|
||||
<span style={{ fontWeight: "800" }}>{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}</span>
|
||||
</Text>
|
||||
|
||||
<Text id="instruction2" className="instruction">
|
||||
{msg("emailLinkIdp2")}{" "}
|
||||
<Button type="link" href={url.loginAction} style={{ padding: 0 }}>
|
||||
{msg("doClickHere")}
|
||||
</Button>{" "}
|
||||
{msg("emailLinkIdp3")}
|
||||
</Text>
|
||||
|
||||
<Text id="instruction3" className="instruction">
|
||||
{msg("emailLinkIdp4")}{" "}
|
||||
<Button type="link" href={url.loginAction} style={{ padding: 0 }}>
|
||||
{msg("doClickHere")}
|
||||
</Button>{" "}
|
||||
{msg("emailLinkIdp5")}
|
||||
</Text>
|
||||
</Space>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-oauth2-device-verify-user-code.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-oauth2-device-verify-user-code.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorMessage:
|
||||
* - Purpose: Tests when there is an error with the OAuth2 device user code entry.
|
||||
* - Scenario: The component renders with an error message displayed to the user.
|
||||
* - Key Aspect: Ensures the error message is properly shown when the user enters an invalid code.
|
||||
*/
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
oauth2DeviceVerificationAction: "/mock-oauth2-device-verification"
|
||||
},
|
||||
message: {
|
||||
summary: "The user code you entered is invalid. Please try again.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithEmptyInputField:
|
||||
* - Purpose: Tests when the user code field is left empty.
|
||||
* - Scenario: The component renders the form, and the user tries to submit without entering any code.
|
||||
* - Key Aspect: Ensures the form displays validation errors when the field is left empty.
|
||||
*/
|
||||
export const WithEmptyInputField: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
oauth2DeviceVerificationAction: "/mock-oauth2-device-verification"
|
||||
},
|
||||
message: {
|
||||
summary: "User code cannot be empty. Please enter a valid code.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,80 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Button, Divider, Form, Input } from "antd";
|
||||
import { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LoginOauth2DeviceVerifyUserCode(
|
||||
props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>
|
||||
) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
const { url } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
type FieldType = {
|
||||
"cancel-aia": string;
|
||||
};
|
||||
|
||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||
|
||||
const onFinish = (values: Record<string, string>) => {
|
||||
console.log("Submitting form:", values);
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.oauth2DeviceVerificationAction;
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
const handleSubmit = (values: FieldType) => {
|
||||
setIsSubmitLoading(true);
|
||||
onFinish(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={false} classes={{}} headerNode={msg("oauth2DeviceVerificationTitle")}>
|
||||
<Form id="kc-user-verify-device-user-code-form" layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item label={msg("verifyOAuth2DeviceUserCode")} name="device_user_code" rules={[{ required: true, message: "Required" }]}>
|
||||
<Input
|
||||
id="device-user-code"
|
||||
name="device_user_code"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-hash.svg"} width={14} style={{ marginRight: "3px" }} />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
block
|
||||
size="large"
|
||||
iconPosition="end"
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-checkmark.svg"}
|
||||
style={{ marginTop: "3px", marginBottom: "0px" }}
|
||||
width={14}
|
||||
/>
|
||||
}
|
||||
loading={isSubmitLoading}
|
||||
disabled={isSubmitLoading}
|
||||
>
|
||||
{msgStr("doSubmit")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
// Mock kcContext to simulate real environment
|
||||
const mockKcContext = {
|
||||
url: {
|
||||
oauthAction: "/oauth-action"
|
||||
},
|
||||
oauth: {
|
||||
clientScopesRequested: [{ consentScreenText: "Scope1", dynamicScopeParameter: "dynamicScope1" }, { consentScreenText: "Scope2" }],
|
||||
code: "mockCode"
|
||||
},
|
||||
client: {
|
||||
attributes: {
|
||||
logoUri: "https://cdn.tombutcher.work/logos/grafana-logo.png",
|
||||
policyUri: "https://twitter.com/en/tos",
|
||||
tosUri: "https://twitter.com/en/privacy"
|
||||
},
|
||||
name: "Twitter",
|
||||
clientId: "twitter-client-id"
|
||||
}
|
||||
};
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-oauth-grant.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-oauth-grant.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
/**
|
||||
* Default:
|
||||
* - Purpose: Tests the default behavior with meaningful logo (Twitter).
|
||||
* - Scenario: The component renders with Twitter as the client, displaying its logo, policy, and terms of service links.
|
||||
* - Key Aspect: Ensures the component works with a realistic `logoUri` and client name.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory kcContext={mockKcContext} />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithoutScopes:
|
||||
* - Purpose: Tests the component when no OAuth scopes are requested.
|
||||
* - Scenario: The component renders with no scopes listed under the consent screen.
|
||||
* - Key Aspect: Ensures the component renders correctly when there are no requested scopes.
|
||||
*/
|
||||
export const WithoutScopes: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
oauth: {
|
||||
...mockKcContext.oauth,
|
||||
clientScopesRequested: []
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithFormSubmissionError:
|
||||
* - Purpose: Tests how the component handles form submission errors.
|
||||
* - Scenario: The `oauthAction` URL is set to an error route and an error message is displayed.
|
||||
* - Key Aspect: Ensures that the component can display error messages when form submission fails.
|
||||
*/
|
||||
export const WithFormSubmissionError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
...mockKcContext,
|
||||
url: {
|
||||
oauthAction: "/error"
|
||||
},
|
||||
message: {
|
||||
type: "error",
|
||||
summary: "An error occurred during form submission."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,170 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
import { Typography, Space, List, Button, Flex, Divider, Alert } from "antd";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
|
||||
const { url, oauth, client } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
|
||||
|
||||
const [isAcceptLoading, setIsAcceptLoading] = useState(false);
|
||||
const [isDeclineLoading, setIsDeclineLoading] = useState(false);
|
||||
|
||||
const handleSubmit = (action: "accept" | "cancel") => {
|
||||
// Create a new form element
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.oauthAction;
|
||||
|
||||
// Add the code value
|
||||
const codeInput = document.createElement("input");
|
||||
codeInput.type = "hidden";
|
||||
codeInput.name = "code";
|
||||
codeInput.value = oauth.code;
|
||||
form.appendChild(codeInput);
|
||||
|
||||
// Add the action value
|
||||
const actionInput = document.createElement("input");
|
||||
actionInput.type = "hidden";
|
||||
actionInput.name = action;
|
||||
actionInput.value = action === "accept" ? msgStr("doYes") : msgStr("doNo");
|
||||
form.appendChild(actionInput);
|
||||
|
||||
// Append form to the body and submit
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
bodyClassName="oauth"
|
||||
headerNode={
|
||||
<>
|
||||
{client.attributes.logoUri ? (
|
||||
<Space align="start" direction="vertical">
|
||||
<img
|
||||
src={client.attributes.logoUri}
|
||||
alt={client.name || client.clientId}
|
||||
style={{ maxHeight: "64px", maxWidth: "100%", marginBottom: "20px" }}
|
||||
/>
|
||||
<Title level={3}>
|
||||
{client.name ? msg("oauthGrantTitle", advancedMsgStr(client.name)) : msg("oauthGrantTitle", client.clientId)}
|
||||
</Title>
|
||||
</Space>
|
||||
) : (
|
||||
<Title level={3}>
|
||||
{client.name ? msg("oauthGrantTitle", advancedMsgStr(client.name)) : msg("oauthGrantTitle", client.clientId)}
|
||||
</Title>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Alert message={msg("oauthGrantRequest")} type="warning" style={{ marginBottom: "20px" }} showIcon></Alert>
|
||||
{(client.attributes.policyUri || client.attributes.tosUri) && (
|
||||
<Alert
|
||||
message={client.name ? msg("oauthGrantInformation", advancedMsgStr(client.name)) : msg("oauthGrantInformation", client.clientId)}
|
||||
type="info"
|
||||
style={{ marginBottom: "20px" }}
|
||||
showIcon
|
||||
></Alert>
|
||||
)}
|
||||
|
||||
<List
|
||||
dataSource={oauth.clientScopesRequested}
|
||||
bordered={true}
|
||||
renderItem={clientScope => (
|
||||
<List.Item key={clientScope.consentScreenText}>
|
||||
<Space>
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/c-check-128.svg"}
|
||||
style={{ marginTop: 0, marginBottom: "3px" }}
|
||||
width={14}
|
||||
/>
|
||||
<Text>
|
||||
{advancedMsg(clientScope.consentScreenText)}
|
||||
{clientScope.dynamicScopeParameter && (
|
||||
<>
|
||||
: <Text strong>{clientScope.dynamicScopeParameter}</Text>
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
|
||||
{(client.attributes.policyUri || client.attributes.tosUri) && (
|
||||
<>
|
||||
<Space direction="vertical" style={{ marginTop: "12px" }}>
|
||||
{client.attributes.tosUri && (
|
||||
<Text>
|
||||
{msg("oauthGrantReview")}{" "}
|
||||
<Link href={client.attributes.tosUri} target="_blank">
|
||||
{msg("oauthGrantTos")}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
{client.attributes.policyUri && (
|
||||
<Text>
|
||||
{msg("oauthGrantReview")}{" "}
|
||||
<Link href={client.attributes.policyUri} target="_blank">
|
||||
{msg("oauthGrantPolicy")}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Space>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Flex gap="middle">
|
||||
<Button
|
||||
type="primary"
|
||||
style={{ flexGrow: 1 }}
|
||||
size="large"
|
||||
id="kc-login"
|
||||
loading={isAcceptLoading}
|
||||
disabled={isAcceptLoading || isDeclineLoading}
|
||||
iconPosition="end"
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-checkmark.svg"}
|
||||
style={{ marginTop: "0px", marginBottom: "3px" }}
|
||||
width={14}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
setIsAcceptLoading(true);
|
||||
handleSubmit("accept");
|
||||
}}
|
||||
>
|
||||
{msgStr("doYes")}
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
style={{ flexGrow: 1 }}
|
||||
size="large"
|
||||
id="kc-cancel"
|
||||
loading={isDeclineLoading}
|
||||
disabled={isAcceptLoading || isDeclineLoading}
|
||||
onClick={() => {
|
||||
setIsDeclineLoading(true);
|
||||
handleSubmit("cancel");
|
||||
}}
|
||||
>
|
||||
{msgStr("doNo")}
|
||||
</Button>
|
||||
</Flex>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,127 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-otp.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-otp.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* MultipleOtpCredentials:
|
||||
* - Purpose: Tests the behavior when the user has multiple OTP credentials to choose from.
|
||||
* - Scenario: Simulates the scenario where the user is presented with multiple OTP credentials and must select one to proceed.
|
||||
* - Key Aspect: Ensures that multiple OTP credentials are listed and selectable, and the correct credential is selected by default.
|
||||
*/
|
||||
export const MultipleOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: [
|
||||
{ id: "credential1", userLabel: "Device 1" },
|
||||
{ id: "credential2", userLabel: "Device 2" },
|
||||
{ id: "credential2", userLabel: "Device 3" },
|
||||
{ id: "credential2", userLabel: "Device 4" },
|
||||
{ id: "credential2", userLabel: "Device 5" },
|
||||
{ id: "credential2", userLabel: "Device 6" }
|
||||
],
|
||||
selectedCredentialId: "credential1"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithOtpError:
|
||||
* - Purpose: Tests the behavior when an error occurs with the OTP field (e.g., invalid OTP code).
|
||||
* - Scenario: Simulates an invalid OTP code scenario where an error message is displayed.
|
||||
* - Key Aspect: Ensures that the OTP input displays error messages correctly and the error is visible.
|
||||
*/
|
||||
export const WithOtpError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: []
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "totp",
|
||||
get: () => "Invalid OTP code"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* NoOtpCredentials:
|
||||
* - Purpose: Tests the behavior when no OTP credentials are provided for the user.
|
||||
* - Scenario: Simulates the scenario where the user is not presented with any OTP credentials, and only the OTP input is displayed.
|
||||
* - Key Aspect: Ensures that the component handles cases where there are no user OTP credentials, and the user is only prompted for the OTP code.
|
||||
*/
|
||||
export const NoOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: []
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorAndMultipleOtpCredentials:
|
||||
* - Purpose: Tests behavior when there is both an error in the OTP field and multiple OTP credentials.
|
||||
* - Scenario: Simulates the case where the user has multiple OTP credentials and encounters an error with the OTP input.
|
||||
* - Key Aspect: Ensures that the component can handle both multiple OTP credentials and display an error message simultaneously.
|
||||
*/
|
||||
export const WithErrorAndMultipleOtpCredentials: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
otpLogin: {
|
||||
userOtpCredentials: [
|
||||
{ id: "credential1", userLabel: "Device 1" },
|
||||
{ id: "credential2", userLabel: "Device 2" }
|
||||
],
|
||||
selectedCredentialId: "credential1"
|
||||
},
|
||||
url: {
|
||||
loginAction: "/login-action"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "totp",
|
||||
get: () => "Invalid OTP code"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,126 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Form, Input, Button, Radio, List, Typography, Space, Divider } from "antd";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
const { otpLogin, url, messagesPerField } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
type FieldType = {
|
||||
"cancel-aia": string;
|
||||
};
|
||||
|
||||
const [isSubmitLoading, setIsSubmitLoading] = useState(false);
|
||||
|
||||
const onFinish = (values: Record<string, string>) => {
|
||||
console.log("Submitting form:", values);
|
||||
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.loginAction;
|
||||
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
const handleSubmit = (values: FieldType) => {
|
||||
setIsSubmitLoading(true);
|
||||
onFinish(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
classes={{}}
|
||||
displayMessage={!messagesPerField.existsError("totp")}
|
||||
headerNode={msg("doLogIn")}
|
||||
>
|
||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
||||
<Form id="kc-otp-login-form" layout="vertical" onFinish={handleSubmit}>
|
||||
{otpLogin.userOtpCredentials.length > 1 && (
|
||||
<Form.Item name="selectedCredentialId">
|
||||
<Radio.Group defaultValue={otpLogin.selectedCredentialId} style={{ width: "100%" }}>
|
||||
<List
|
||||
dataSource={otpLogin.userOtpCredentials}
|
||||
bordered
|
||||
style={{ width: "100%", lineHeight: 1 }}
|
||||
renderItem={(otpCredential, index) => (
|
||||
<List.Item key={index}>
|
||||
<Radio id={`kc-otp-credential-${index}`} value={otpCredential.id} style={{ display: "block" }}>
|
||||
<Text style={{ position: "relative", top: "-3px" }}>{otpCredential.userLabel}</Text>
|
||||
</Radio>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item
|
||||
label={msg("loginOtpOneTime")}
|
||||
validateStatus={messagesPerField.existsError("totp") ? "error" : ""}
|
||||
name="otp"
|
||||
help={
|
||||
messagesPerField.existsError("totp") && (
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("totp"))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Input
|
||||
id="otp"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-hash.svg"} width={14} style={{ marginRight: "3px" }} />}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Divider />
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
id="kc-login"
|
||||
name="login"
|
||||
block
|
||||
size="large"
|
||||
disabled={isSubmitLoading}
|
||||
loading={isSubmitLoading}
|
||||
iconPosition={"end"}
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-right.svg"}
|
||||
style={{ marginTop: "4px", marginBottom: 0 }}
|
||||
height={14}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{msgStr("doLogIn")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Space>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-page-expired.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-page-expired.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorMessage:
|
||||
* - Purpose: Tests behavior when an error message is displayed along with the page expiration message.
|
||||
* - Scenario: Simulates a case where the session expired due to an error, and an error message is displayed alongside the expiration message.
|
||||
* - Key Aspect: Ensures that error messages are displayed correctly in addition to the page expiration notice.
|
||||
*/
|
||||
export const WithErrorMessage: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginRestartFlowUrl: "/mock-restart-flow",
|
||||
loginAction: "/mock-continue-login"
|
||||
},
|
||||
message: {
|
||||
type: "error",
|
||||
summary: "An error occurred while processing your session."
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import { Typography, Space } from "antd";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Title, Text, Link } = Typography;
|
||||
|
||||
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { url } = kcContext;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={<Title level={3}>{msg("pageExpiredTitle")}</Title>}
|
||||
>
|
||||
<Space direction="vertical" size="small" style={{ width: "100%" }}>
|
||||
<Text id="instruction1">
|
||||
{msg("pageExpiredMsg1")}{" "}
|
||||
<Link id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||
{msg("doClickHere")}
|
||||
</Link>{" "}
|
||||
.
|
||||
</Text>
|
||||
|
||||
<Text>
|
||||
{msg("pageExpiredMsg2")}{" "}
|
||||
<Link id="loginContinueLink" href={url.loginAction}>
|
||||
{msg("doClickHere")}
|
||||
</Link>{" "}
|
||||
.
|
||||
</Text>
|
||||
</Space>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-passkeys-conditional-authenticate.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-passkeys-conditional-authenticate.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
@ -1,68 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-password.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-password.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithPasswordError:
|
||||
* - Purpose: Tests the behavior when an error occurs in the password field (e.g., incorrect password).
|
||||
* - Scenario: Simulates a scenario where an invalid password is entered, and an error message is displayed.
|
||||
* - Key Aspect: Ensures that the password input field displays error messages correctly.
|
||||
*/
|
||||
export const WithPasswordError: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
resetPasswordAllowed: true
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login",
|
||||
loginResetCredentialsUrl: "/mock-reset-password"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: (field: string) => field === "password",
|
||||
get: () => "Invalid password"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* WithoutResetPasswordOption:
|
||||
* - Purpose: Tests the behavior when the reset password option is disabled.
|
||||
* - Scenario: Simulates a scenario where the `resetPasswordAllowed` is set to `false`, and the "Forgot Password" link is not rendered.
|
||||
* - Key Aspect: Ensures that the component handles cases where resetting the password is not allowed.
|
||||
*/
|
||||
export const WithoutResetPasswordOption: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
resetPasswordAllowed: false
|
||||
},
|
||||
url: {
|
||||
loginAction: "/mock-login",
|
||||
loginResetCredentialsUrl: "/mock-reset-password"
|
||||
},
|
||||
messagesPerField: {
|
||||
existsError: () => false
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,127 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Form, Input, Button, Divider, Typography, Space, FormProps } from "antd";
|
||||
import { EyeTwoTone, EyeInvisibleOutlined } from "@ant-design/icons";
|
||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Title, Link } = Typography;
|
||||
|
||||
export default function LoginPassword(props: PageProps<Extract<KcContext, { pageId: "login-password.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, Template } = props;
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const { realm, url, messagesPerField, client } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
type FieldType = {
|
||||
string: string;
|
||||
};
|
||||
|
||||
const onFinish: FormProps<FieldType>["onFinish"] = async values => {
|
||||
setIsLoading(true);
|
||||
|
||||
// Create a new form element
|
||||
const form = document.createElement("form");
|
||||
form.method = "POST";
|
||||
form.action = url.loginAction;
|
||||
|
||||
// Append input fields to the form
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = key;
|
||||
input.value = value.toString();
|
||||
form.appendChild(input);
|
||||
});
|
||||
|
||||
// Append form to the body and submit
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={false}
|
||||
headerNode={
|
||||
<>
|
||||
{client.attributes.logoUri ? (
|
||||
<Space align="start" direction="vertical">
|
||||
<img
|
||||
src={client.attributes.logoUri}
|
||||
alt={client.name || client.clientId}
|
||||
style={{ maxHeight: "64px", maxWidth: "100%", marginBottom: "20px" }}
|
||||
/>
|
||||
<Title level={3}>{msg("loginAccountTitle")}</Title>
|
||||
</Space>
|
||||
) : (
|
||||
<Title level={3}>{msg("loginAccountTitle")}</Title>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
displayMessage={!messagesPerField.existsError("password")}
|
||||
>
|
||||
<Form form={form} layout="vertical" requiredMark={false} onFinish={onFinish}>
|
||||
<Form.Item
|
||||
name="password"
|
||||
label={msg("password")}
|
||||
validateStatus={messagesPerField.existsError("password") ? "error" : ""}
|
||||
help={
|
||||
messagesPerField.existsError("password") && (
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: kcSanitize(messagesPerField.get("password"))
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Input.Password
|
||||
id="password"
|
||||
name="password"
|
||||
size="large"
|
||||
prefix={<img src={"https://cdn.tombutcher.work/icons/auth/c-lock.svg"} style={{ marginRight: "3px" }} height={14} />}
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
iconRender={visible => (visible ? <EyeTwoTone /> : <EyeInvisibleOutlined />)}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{realm.resetPasswordAllowed && (
|
||||
<div style={{ textAlign: "right", marginBottom: 16 }}>
|
||||
<Link href={url.loginResetCredentialsUrl}>{msg("doForgotPassword")}</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Divider />
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
id="kc-login"
|
||||
name="login"
|
||||
block
|
||||
size="large"
|
||||
disabled={isLoading}
|
||||
loading={isLoading}
|
||||
iconPosition="end"
|
||||
icon={
|
||||
<img
|
||||
src={"https://cdn.tombutcher.work/icons/auth/w-right.svg"}
|
||||
style={{ marginTop: "4px", marginBottom: 0 }}
|
||||
height={14}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{msgStr("doLogIn")}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-recovery-authn-code-config.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-recovery-authn-code-config.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
||||
|
||||
/**
|
||||
* WithErrorDuringCodeGeneration:
|
||||
* - Purpose: Tests when an error occurs while generating recovery authentication codes.
|
||||
* - Scenario: The component renders an error message to inform the user of the failure during code generation.
|
||||
* - Key Aspect: Ensures that error messages are properly displayed when recovery code generation fails.
|
||||
*/
|
||||
export const WithErrorDuringCodeGeneration: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
url: {
|
||||
loginAction: "/mock-login-action"
|
||||
},
|
||||
message: {
|
||||
summary: "An error occurred during recovery code generation. Please try again.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
@ -1,109 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import { Alert, Flex, Button, Checkbox, Form, Input, Space, Typography, Divider } from "antd";
|
||||
import { PrinterOutlined, SaveOutlined, CopyOutlined } from "@ant-design/icons";
|
||||
import { useScript } from "keycloakify/login/pages/LoginRecoveryAuthnCodeConfig.useScript";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
const { recoveryAuthnCodesConfigBean, isAppInitiatedAction } = kcContext;
|
||||
const { msg, msgStr } = i18n;
|
||||
const [isConfirmed, setIsConfirmed] = useState(false);
|
||||
|
||||
const olRecoveryCodesListId = "kc-recovery-codes-list";
|
||||
|
||||
useScript({ olRecoveryCodesListId, i18n });
|
||||
|
||||
const formatRecoveryCode = (code: string) => (
|
||||
<>
|
||||
{code.slice(0, 4)}-{code.slice(4, 8)}-{code.slice(8)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={msg("recovery-code-config-header")}
|
||||
>
|
||||
<Space direction="vertical" size="middle">
|
||||
<Space direction="vertical" size="large">
|
||||
<Alert message={msg("recovery-code-config-warning-title")} type="warning" showIcon />
|
||||
<Text>{msg("recovery-code-config-warning-message")}</Text>
|
||||
</Space>
|
||||
<Flex id={olRecoveryCodesListId} gap="middle" wrap>
|
||||
{recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList.map((code, index) => (
|
||||
<Text key={index} strong code style={{ fontSize: "18px" }}>
|
||||
{formatRecoveryCode(code)}
|
||||
</Text>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Space>
|
||||
<Button type="link" icon={<PrinterOutlined />} id="printRecoveryCodes">
|
||||
{msg("recovery-codes-print")}
|
||||
</Button>
|
||||
<Button type="link" icon={<SaveOutlined />} id="downloadRecoveryCodes">
|
||||
{msg("recovery-codes-download")}
|
||||
</Button>
|
||||
<Button type="link" icon={<CopyOutlined />} id="copyRecoveryCodes">
|
||||
{msg("recovery-codes-copy")}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Checkbox
|
||||
id="kcRecoveryCodesConfirmationCheck"
|
||||
name="kcRecoveryCodesConfirmationCheck"
|
||||
onChange={e => setIsConfirmed(e.target.checked)}
|
||||
style={{ marginBottom: "0px" }}
|
||||
>
|
||||
{msg("recovery-codes-confirmation-message")}
|
||||
</Checkbox>
|
||||
</Space>
|
||||
|
||||
<Form id="kc-recovery-codes-settings-form" method="post" action={kcContext.url.loginAction}>
|
||||
<Input type="hidden" name="generatedRecoveryAuthnCodes" value={recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesAsString} />
|
||||
<Input type="hidden" name="generatedAt" value={recoveryAuthnCodesConfigBean.generatedAt} />
|
||||
<Input type="hidden" id="userLabel" name="userLabel" value={msgStr("recovery-codes-label-default")} />
|
||||
|
||||
<LogoutOtherSessions i18n={i18n} />
|
||||
|
||||
<Divider />
|
||||
|
||||
{isAppInitiatedAction ? (
|
||||
<Space>
|
||||
<Button type="primary" size="large" id="saveRecoveryAuthnCodesBtn" htmlType="submit" disabled={!isConfirmed}>
|
||||
{msg("recovery-codes-action-complete")}
|
||||
</Button>
|
||||
<Button size="large" id="cancelRecoveryAuthnCodesBtn" name="cancel-aia" value="true" htmlType="submit">
|
||||
{msg("recovery-codes-action-cancel")}
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button type="primary" size="large" id="saveRecoveryAuthnCodesBtn" htmlType="submit" disabled={!isConfirmed} block>
|
||||
{msg("recovery-codes-action-complete")}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
</Template>
|
||||
);
|
||||
}
|
||||
|
||||
function LogoutOtherSessions(props: { i18n: I18n }) {
|
||||
const { i18n } = props;
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: "16px" }}>
|
||||
<Checkbox id="logout-sessions" name="logout-sessions" value="on" defaultChecked>
|
||||
{msg("logoutOtherSessions")}
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
}
|
||||