Compare commits

..

136 Commits

Author SHA1 Message Date
93bb3ebd69 Bump version 2023-04-28 18:47:55 +02:00
e8e516159c Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-28 18:47:30 +02:00
1431c031a0 #340 2023-04-28 18:47:25 +02:00
209c2183e1 Bump version 2023-04-28 17:58:09 +02:00
0c98c282a0 Merge pull request #339 from keycloakify/fix/fix-broken-jar
fix: fix broken jar
2023-04-28 17:57:41 +02:00
58c10796a1 fix: fix broken jar
Many tools will handle zipfiles which lack directory entries
just fine, others will not. Looks like the JDKs JAR libs are
not  handling  it well. This commit will make sure to create
folder entries.
2023-04-28 16:59:06 +02:00
603e6a99f3 Update package.json 2023-04-27 18:00:14 +02:00
6622ebc04e Merge pull request #337 from keycloakify/fix/restore-missing-pom-xml-in-jar
Fix/restore missing pom xml in jar
2023-04-27 17:59:53 +02:00
465dbb4a8d fix formatting 2023-04-27 14:47:15 +02:00
08ae908453 fix: adjust test after adjusting jar.ts 2023-04-27 14:44:45 +02:00
c35a1e7c50 fix: fix paths after changing root path param meaning 2023-04-27 14:33:21 +02:00
ecb22c3829 fix: restore missing pom.xml in jar archive 2023-04-27 14:33:19 +02:00
eebf969f7e Bump version 2023-04-27 11:52:28 +02:00
5816f25c3e #334 2023-04-27 11:52:02 +02:00
b2a81d880d fmt 2023-04-25 01:42:42 +02:00
b10c1476a6 Bump version 2023-04-25 01:40:07 +02:00
e11cd09a12 Bump version 2023-04-25 01:40:07 +02:00
27575eda68 fix: collapse empty ca list to undefined 2023-04-25 01:40:07 +02:00
f33b9a1ec6 feat: honor npmrc settings for ssl ca, cert and strict ssh handling 2023-04-25 01:40:07 +02:00
7c45fff7ba Update README.md 2023-04-25 01:39:45 +02:00
ecdb0775cd Update CloudIAM logo 2023-04-25 01:39:21 +02:00
6ef90a56ed Bump version 2023-04-21 01:00:39 +02:00
71b86ff43b Realtime validation for account/password.ftl 2023-04-21 01:00:18 +02:00
0535e06ae1 Update Cloud IAM wording 2023-04-20 22:15:02 +02:00
6261f5e7cc Update CloudIAM logo and referal link 2023-04-20 22:11:52 +02:00
f256b74929 Bump version 2023-04-20 20:52:23 +02:00
4f1182a230 Feat sml-post-form.ftl #277 2023-04-20 20:51:46 +02:00
e7c20547f8 Bump version 2023-04-20 20:09:29 +02:00
9ab4c510fe #209 2023-04-20 20:08:47 +02:00
7d78c52064 Bump version 2023-04-20 18:15:18 +02:00
6223d91291 Update post build instructions 2023-04-20 18:15:03 +02:00
840b5e1312 Bump version 2023-04-20 13:18:17 +02:00
e69813f6e3 Fix small logical error 2023-04-20 13:17:57 +02:00
3c0c057e06 Bump version 2023-04-20 13:10:46 +02:00
984d12b3f2 Fix bad types in Account kcContext 2023-04-20 13:10:33 +02:00
61dc54f115 Bump version 2023-04-20 13:05:33 +02:00
34e47cccc1 Enable to redirect back to the application from the account pages 2023-04-20 13:05:01 +02:00
c170345550 Update README 2023-04-20 05:52:04 +02:00
1e40706f72 Update CNAME 2023-04-20 05:44:16 +02:00
ea1a747ebf Add all missing pages to the storybook 2023-04-20 05:41:34 +02:00
a14e967020 Add IdpReviewUserProfile.tsx story 2023-04-20 05:19:38 +02:00
0fff10d2c6 Bump version 2023-04-20 04:18:36 +02:00
7c2123614d Remove margin for canvas container 2023-04-20 04:18:17 +02:00
d149866703 Add story for password.ftl 2023-04-20 04:17:37 +02:00
18039140db Important fix, assets common where broken 2023-04-20 04:17:12 +02:00
4de9599018 Bump version 2023-04-20 03:37:31 +02:00
bb85829d71 Add referrerURI to the base type and make it optional 2023-04-20 03:37:11 +02:00
ff077943ec Fixe syntax error in DockContainer, remove story padding 2023-04-20 02:55:05 +02:00
f057114bcc Add account/password.ftl to storybook 2023-04-20 02:54:24 +02:00
e7bfe7f80d Add account/account.ftl to storybook 2023-04-20 02:52:49 +02:00
18112a97ab Deal with story ordering 2023-04-20 02:41:06 +02:00
8ee6fb58ac Reproduce directory layout #274 2023-04-20 01:55:13 +02:00
08831fc31d Add storybook Error page #274 2023-04-20 01:53:36 +02:00
c5c25394fb Hide addon pannel by default 2023-04-20 01:28:02 +02:00
2f649c9866 Remove forgoten console.log 2023-04-20 00:14:00 +02:00
91c5dd40fa Enable the storybook to default to canevas 2023-04-20 00:13:25 +02:00
e95e688cf0 Add rotating logo for the intro 2023-04-19 23:41:25 +02:00
9845f1de08 Update prettierignore 2023-04-19 23:09:30 +02:00
07032d312d Correctly load up the fonts 2023-04-19 22:29:46 +02:00
ccb5d32763 update yarn.lock 2023-04-19 19:08:56 +02:00
bf83e4b03b docs: update .all-contributorsrc [skip ci] 2023-04-19 19:08:56 +02:00
03b491763f docs: update README.md [skip ci] 2023-04-19 19:08:56 +02:00
3abc9edf0e Rename missnamed components 2023-04-19 19:04:48 +02:00
f9accc51d3 Bump version #303 2023-04-19 17:57:21 +02:00
c3bade81b4 Restore copy assets to public in the keycloakify script for backward compatibility 2023-04-19 17:50:27 +02:00
6edd1f00dd Update prettier ignore 2023-04-19 17:36:18 +02:00
02be899629 Merge pull request #325 from Gravity-Software-srl/main
fix typing of algToKeyUriAlg + totp interface + cleanup build dir
2023-04-19 17:34:24 +02:00
8e043f289a sync with upstream main 2023-04-19 17:23:52 +02:00
30fecf8578 Revert "add build option keepBuildDir"
This reverts commit 86884607ef.
2023-04-19 17:04:46 +02:00
1112da33e3 Fix build 2023-04-19 05:32:19 +02:00
ffa65e871e Fix build 2023-04-19 05:10:25 +02:00
f49c7b465b Release beta 2023-04-19 05:05:21 +02:00
e6f75156ec New script only for copying default assets to public 2023-04-19 05:04:11 +02:00
ebafeb19ad Bump version 2023-04-19 03:21:25 +02:00
5166c719c4 Better scripts 2023-04-19 03:21:04 +02:00
bf92ea8340 Update yarn.lock 2023-04-19 03:20:47 +02:00
cf1e595ba2 Clean up dynamically inserted assets when template is unmounted #274 2023-04-19 03:20:22 +02:00
2bf3296c0f Attempt to fix ci 2023-04-18 04:35:16 +02:00
11513f73b7 Add discord 2023-04-18 04:29:02 +02:00
b6f60c6835 Update prettierignore 2023-04-18 04:16:49 +02:00
e9d276010f Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-18 04:12:07 +02:00
b08c4b0b29 Update yarn.lock 2023-04-18 04:11:53 +02:00
d684807d96 Copy keycloak assets into storybook static #274 2023-04-18 04:04:55 +02:00
9a60ef7c47 Update README.md 2023-04-17 11:13:24 +02:00
cc446059de Moving on with setup of the reference storybook #274 2023-04-17 04:02:34 +02:00
d75b809c13 Bump version 2023-04-17 04:02:33 +02:00
9fc3998cf7 Avoid deprecating getKcContext #274 2023-04-17 04:02:18 +02:00
238baa72cf Bump version 2023-04-17 01:33:01 +02:00
089f0f7a87 More explicit naming 2023-04-17 01:32:06 +02:00
aa9d3d1931 Bump version 2023-04-17 00:46:45 +02:00
2fc6aed4f1 Correct the account password page 2023-04-17 00:46:30 +02:00
c2fdea7886 Bump version 2023-04-17 00:28:34 +02:00
c8f71946d4 We where copying login theme assets into accont theme 2023-04-17 00:27:49 +02:00
d1cc6ed88d Smarter getKcContext typing 2023-04-16 03:00:03 +02:00
f6e6cf3750 Better typing for createGetKcContext 2023-04-16 02:36:15 +02:00
d1c7491704 Create a storybook friendly getKcContext 2023-04-16 02:09:26 +02:00
fd49c2fd23 Add step to build storybook 2023-04-15 22:23:09 +02:00
f7fb2efcdd Setup Storybook v6 (I spent 2hours trying to use v7 instead but it isn't worth it 2023-04-15 22:18:11 +02:00
ff0608c202 Update Contributor list 2023-04-15 01:59:01 +02:00
e63e20eade update dontributor list 2023-04-15 01:56:06 +02:00
335292cf4c Merge pull request #323 from keycloakify/all-contributors/add-asashay
docs: add asashay as a contributor for test, and code
2023-04-15 01:55:22 +02:00
7cb927c8b8 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:55:13 +00:00
802d6b3dad docs: update README.md [skip ci] 2023-04-14 23:55:12 +00:00
c16bf28369 update dontributor list 2023-04-15 01:54:05 +02:00
0de76ae613 Merge pull request #322 from keycloakify/all-contributors/add-marcmrf
docs: add marcmrf as a contributor for test, and code
2023-04-15 01:52:32 +02:00
880396e3a6 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:52:23 +00:00
879fc2812d docs: update README.md [skip ci] 2023-04-14 23:52:22 +00:00
6ac6209bd0 Merge pull request #321 from keycloakify/all-contributors/add-lazToum
docs: add lazToum as a contributor for test, and code
2023-04-15 01:50:15 +02:00
d7fd76c568 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:50:04 +00:00
543e08276f docs: update README.md [skip ci] 2023-04-14 23:50:03 +00:00
2e647b9196 Merge pull request #320 from keycloakify/all-contributors/add-juffe
docs: add juffe as a contributor for test, and code
2023-04-15 01:47:54 +02:00
69cf556582 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:47:46 +00:00
e168ee2ae6 docs: update README.md [skip ci] 2023-04-14 23:47:45 +00:00
274d758ba8 Merge pull request #319 from keycloakify/all-contributors/add-0x-Void
docs: add 0x-Void as a contributor for test, and code
2023-04-15 01:46:03 +02:00
b52e35be7d docs: update .all-contributorsrc [skip ci] 2023-04-14 23:45:51 +00:00
7f1ba8f166 docs: update README.md [skip ci] 2023-04-14 23:45:50 +00:00
72a5b9bac5 Merge pull request #318 from keycloakify/all-contributors/add-aidangilmore
docs: add aidangilmore as a contributor for test, and code
2023-04-15 01:44:54 +02:00
34fb0c2753 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:44:45 +00:00
e5f0885cb0 docs: update README.md [skip ci] 2023-04-14 23:44:44 +00:00
4f93190162 Reorder all contributor 2023-04-15 01:42:35 +02:00
9d1dcd278a Merge pull request #317 from keycloakify/all-contributors/add-revolunet
docs: add revolunet as a contributor for test, and code
2023-04-15 01:37:12 +02:00
45d4bce0e7 docs: update .all-contributorsrc [skip ci] 2023-04-14 23:36:46 +00:00
680a7206d3 docs: update README.md [skip ci] 2023-04-14 23:36:44 +00:00
8a08e9fd64 Add contributors 2023-04-15 01:35:52 +02:00
0080dabe09 Include Cloud IAM in the README 2023-04-15 00:44:48 +02:00
556ce60b27 Bump version 2023-04-13 15:10:27 +02:00
12857e3027 Update Account.tsx #306 2023-04-13 15:09:48 +02:00
10965b82a9 Merge remote-tracking branch 'upstream/main' 2023-04-12 11:45:11 +02:00
86884607ef add build option keepBuildDir
if set to true, will not cleanup build_keycloak directory
2023-04-12 11:44:37 +02:00
1ff0449332 removed "$" typo in LoginConfigTotp.tsx 2023-04-11 15:44:54 +02:00
57b056b388 Bump version 2023-04-07 02:04:17 +02:00
9058e9ac9d Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-04-07 01:39:46 +02:00
ad3de8bff5 Bump version 2023-04-07 01:39:30 +02:00
476b100b04 Remove warning about last release not working 2023-04-07 01:39:15 +02:00
b2c7c86609 Fix the ftl script 2023-04-07 01:38:45 +02:00
564ffc2be9 infer type of algToKeyUriAlg from type of kcConfig + fix totp interface
- add more yarn dirs to .gitignore
2023-04-06 17:50:26 +02:00
116 changed files with 12771 additions and 787 deletions

151
.all-contributorsrc Normal file
View File

@ -0,0 +1,151 @@
{
"files": [
"README.md"
],
"imageSize": 100,
"commit": false,
"commitConvention": "angular",
"contributors": [
{
"login": "lordvlad",
"name": "Waldemar Reusch",
"avatar_url": "https://avatars.githubusercontent.com/u/1217769?v=4",
"profile": "https://github.com/lordvlad",
"contributions": [
"code"
]
},
{
"login": "willwill96",
"name": "William Will",
"avatar_url": "https://avatars.githubusercontent.com/u/10997562?v=4",
"profile": "https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/",
"contributions": [
"code"
]
},
{
"login": "Ann2827",
"name": "Bystrova Ann",
"avatar_url": "https://avatars.githubusercontent.com/u/32645809?v=4",
"profile": "https://github.com/Ann2827",
"contributions": [
"code"
]
},
{
"login": "mkreuzmayr",
"name": "Michael Kreuzmayr",
"avatar_url": "https://avatars.githubusercontent.com/u/20108212?v=4",
"profile": "https://github.com/mkreuzmayr",
"contributions": [
"code"
]
},
{
"login": "Mstrodl",
"name": "Mary ",
"avatar_url": "https://avatars.githubusercontent.com/u/6877780?v=4",
"profile": "https://coolmathgames.tech",
"contributions": [
"code"
]
},
{
"login": "Tasyp",
"name": "German Öö",
"avatar_url": "https://avatars.githubusercontent.com/u/6623212?v=4",
"profile": "https://tasyp.xyz/",
"contributions": [
"code"
]
},
{
"login": "revolunet",
"name": "Julien Bouquillon",
"avatar_url": "https://avatars.githubusercontent.com/u/124937?v=4",
"profile": "https://revolunet.com",
"contributions": [
"code"
]
},
{
"login": "aidangilmore",
"name": "Aidan Gilmore",
"avatar_url": "https://avatars.githubusercontent.com/u/32880357?v=4",
"profile": "https://github.com/aidangilmore",
"contributions": [
"code"
]
},
{
"login": "0x-Void",
"name": "Void",
"avatar_url": "https://avatars.githubusercontent.com/u/32745739?v=4",
"profile": "https://github.com/0x-Void",
"contributions": [
"code"
]
},
{
"login": "juffe",
"name": "juffe",
"avatar_url": "https://avatars.githubusercontent.com/u/5393231?v=4",
"profile": "https://github.com/juffe",
"contributions": [
"code"
]
},
{
"login": "lazToum",
"name": "Lazaros Toumanidis",
"avatar_url": "https://avatars.githubusercontent.com/u/4764837?v=4",
"profile": "https://github.com/lazToum",
"contributions": [
"code"
]
},
{
"login": "marcmrf",
"name": "Marc",
"avatar_url": "https://avatars.githubusercontent.com/u/9928519?v=4",
"profile": "https://github.com/marcmrf",
"contributions": [
"code"
]
},
{
"login": "kasir-barati",
"name": "Kasir Barati",
"avatar_url": "https://avatars.githubusercontent.com/u/73785723?v=4",
"profile": "http://kasir-barati.github.io",
"contributions": [
"doc"
]
},
{
"login": "asashay",
"name": "Alex Oliynyk",
"avatar_url": "https://avatars.githubusercontent.com/u/10714670?v=4",
"profile": "https://github.com/asashay",
"contributions": [
"code"
]
},
{
"login": "thosil",
"name": "Thomas Silvestre",
"avatar_url": "https://avatars.githubusercontent.com/u/1140574?v=4",
"profile": "https://www.gravitysoftware.be",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,
"skipCi": true,
"repoType": "github",
"repoHost": "https://github.com",
"projectName": "keycloakify",
"projectOwner": "keycloakify"
}

View File

@ -36,6 +36,22 @@ jobs:
- run: yarn test
- run: yarn test:keycloakify-starter
storybook:
runs-on: ubuntu-latest
if: github.event_name == 'push'
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- uses: bahmutov/npm-install@v1
- run: yarn build-storybook -o ./build_storybook
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>"
check_if_version_upgraded:
name: Check if version upgrade
# When someone forks the repo and opens a PR we want to enables the tests to be run (the previous jobs)

8
.gitignore vendored
View File

@ -52,4 +52,10 @@ jspm_packages
/src/account/i18n/baseMessages/
# VS Code devcontainers
.devcontainer
.devcontainer
/.yarn
/.yarnrc.yml
/stories/assets/fonts/
/build_storybook/
/storybook-static/

View File

@ -8,8 +8,8 @@ node_modules/
/.vscode/
/src/login/i18n/baseMessages/
/src/account/i18n/baseMessages/
# Test Build Directories
/dist_test
/sample_react_project/
/sample_custom_react_project/
/keycloakify_starter_test/
/keycloakify_starter_test/
/.storybook/static/keycloak-resources/

76
.storybook/Containers.js Normal file
View File

@ -0,0 +1,76 @@
import React from "react";
import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
import { useDarkMode } from "storybook-dark-mode";
import { darkTheme, lightTheme } from "./customTheme";
import "./static/fonts/WorkSans/font.css";
export function DocsContainer({ children, context }) {
const isStorybookUiDark = useDarkMode();
const theme = isStorybookUiDark ? darkTheme : lightTheme;
const backgroundColor = theme.appBg;
return (
<>
<style>{`
body {
padding: 0 !important;
background-color: ${backgroundColor};
}
.docs-story {
background-color: ${backgroundColor};
}
[id^=story--] .container {
border: 1px dashed #e8e8e8;
}
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(3) {
visibility: collapse;
}
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
font-size: 13px;
}
`}</style>
<BaseContainer
context={{
...context,
"storyById": id => {
const storyContext = context.storyById(id);
return {
...storyContext,
"parameters": {
...storyContext?.parameters,
"docs": {
...storyContext?.parameters?.docs,
"theme": isStorybookUiDark ? darkTheme : lightTheme
}
}
};
}
}}
>
{children}
</BaseContainer>
</>
);
}
export function CanvasContainer({ children }) {
return (
<>
<style>{`
body {
padding: 0 !important;
}
`}</style>
{children}
</>
);
}

35
.storybook/customTheme.js Normal file
View File

@ -0,0 +1,35 @@
import { create } from "@storybook/theming";
const brandImage = "logo.png";
const brandTitle = "Keycloakify";
const brandUrl = "https://github.com/keycloakify/keycloakify";
const fontBase = '"Work Sans", sans-serif';
const fontCode = "monospace";
export const darkTheme = create({
"base": "dark",
"appBg": "#1E1E1E",
"appContentBg": "#161616",
"barBg": "#161616",
"colorSecondary": "#8585F6",
"textColor": "#FFFFFF",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
});
export const lightTheme = create({
"base": "light",
"appBg": "#F6F6F6",
"appContentBg": "#FFFFFF",
"barBg": "#FFFFFF",
"colorSecondary": "#000091",
"textColor": "#212121",
brandImage,
brandTitle,
brandUrl,
fontBase,
fontCode
});

15
.storybook/main.js Normal file
View File

@ -0,0 +1,15 @@
module.exports = {
"stories": [
"../stories/**/*.stories.@(ts|tsx|mdx)"
],
"addons": [
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-dark-mode",
"@storybook/addon-a11y"
],
"core": {
"builder": "webpack5"
},
"staticDirs": ["./static"]
};

View File

@ -0,0 +1,32 @@
<!-- start favicon -->
<link rel="apple-touch-icon" sizes="180x180" href="/favicon_package/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon_package/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon_package/favicon-16x16.png">
<link rel="manifest" href="/favicon_package/site.webmanifest">
<link rel="mask-icon" href="/favicon_package/safari-pinned-tab.svg" color="#5bbad5">
<!-- end favicon -->
<!-- Meta tags generated by metatags.io -->
<!-- Primary Meta Tags -->
<title>Keycloakify Storybook</title>
<meta name="title" content="Keycloakify Storybook">
<meta name="description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<!-- Facebook Meta Tags -->
<meta property="og:url" content="https://www.keycloakify.dev">
<meta property="og:type" content="website">
<meta property="og:title" content="Keycloakify Storybook">
<meta property="og:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<meta property="og:image" content="https://storybook.keycloakify.dev/preview.png">
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Keycloakify Storybook">
<meta name="twitter:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
<meta name="twitter:image" content="https://storybook.keycloakify.dev/preview.png">
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">

6
.storybook/manager.js Normal file
View File

@ -0,0 +1,6 @@
import { addons } from '@storybook/addons';
addons.setConfig({
"selectedPanel": 'storybook/a11y/panel',
"showPanel": false,
});

137
.storybook/preview.js Normal file
View File

@ -0,0 +1,137 @@
import { darkTheme, lightTheme } from "./customTheme";
import { DocsContainer, CanvasContainer } from "./Containers";
export const parameters = {
"actions": { "argTypesRegex": "^on[A-Z].*" },
"controls": {
"matchers": {
"color": /(background|color)$/i,
"date": /Date$/,
},
},
"backgrounds": { "disable": true },
"darkMode": {
"light": lightTheme,
"dark": darkTheme,
},
"docs": {
"container": DocsContainer
},
"controls": {
"disable": true,
},
"actions": {
"disable": true
},
"viewport": {
"viewports": {
"1440p": {
"name": "1440p",
"styles": {
"width": "2560px",
"height": "1440px",
},
},
"fullHD": {
"name": "Full HD",
"styles": {
"width": "1920px",
"height": "1080px",
},
},
"macBookProBig": {
"name": "MacBook Pro Big",
"styles": {
"width": "1024px",
"height": "640px",
},
},
"macBookProMedium": {
"name": "MacBook Pro Medium",
"styles": {
"width": "1440px",
"height": "900px",
},
},
"macBookProSmall": {
"name": "MacBook Pro Small",
"styles": {
"width": "1680px",
"height": "1050px",
},
},
"pcAgent": {
"name": "PC Agent",
"styles": {
"width": "960px",
"height": "540px",
},
},
"iphone12Pro": {
"name": "Iphone 12 pro",
"styles": {
"width": "390px",
"height": "844px",
},
},
"iphone5se": {
"name": "Iphone 5/SE",
"styles": {
"width": "320px",
"height": "568px",
},
},
"ipadPro": {
"name": "Ipad pro",
"styles": {
"width": "1240px",
"height": "1366px",
},
},
"Galaxy s9+": {
"name": "Galaxy S9+",
"styles": {
"width": "320px",
"height": "658px",
},
}
},
},
"options": {
"storySort": (a, b) =>
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
},
};
export const decorators = [
(Story) => (
<CanvasContainer>
<Story />
</CanvasContainer>
),
];
const { getHardCodedWeight } = (() => {
const orderedPagesPrefix = [
"Introduction",
"login/login.ftl",
"login/register-user-profile.ftl",
"login/register.ftl",
"login/terms.ftl",
"login/error.ftl",
];
function getHardCodedWeight(kind) {
for (let i = 0; i < orderedPagesPrefix.length; i++) {
if (kind.toLowerCase().startsWith(orderedPagesPrefix[i].toLowerCase())) {
return orderedPagesPrefix.length - i;
}
}
return 0;
}
return { getHardCodedWeight };
})();

1
.storybook/static/CNAME Normal file
View File

@ -0,0 +1 @@
storybook.keycloakify.dev

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,193 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="447.000000pt" height="447.000000pt" viewBox="0 0 447.000000 447.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,447.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2177 4413 c-3 -2 -17 -6 -33 -9 -85 -15 -204 -109 -286 -225 -95
-133 -229 -437 -263 -597 -4 -18 -10 -30 -13 -28 -4 2 -7 -11 -8 -30 0 -19 -3
-36 -6 -39 -3 -4 -23 1 -44 10 -51 22 -213 73 -289 92 -301 73 -516 74 -670 3
-124 -57 -186 -153 -188 -295 -1 -67 5 -128 18 -180 3 -13 15 -45 26 -70 43
-99 57 -135 53 -135 -3 0 4 -10 16 -22 11 -12 20 -26 20 -31 0 -5 9 -22 21
-38 11 -16 18 -29 14 -29 -3 0 4 -10 15 -22 11 -12 18 -28 15 -36 -2 -7 -1
-11 3 -8 4 2 19 -12 32 -32 13 -20 33 -47 44 -59 11 -13 17 -23 13 -23 -3 0
12 -19 33 -42 22 -24 38 -49 35 -55 -2 -7 -1 -12 4 -10 4 1 32 -25 62 -58 30
-33 88 -94 131 -135 l76 -75 -71 -70 c-112 -110 -174 -181 -262 -300 -106
-144 -142 -202 -203 -325 -9 -19 -21 -38 -27 -42 -5 -4 -7 -8 -3 -8 4 0 -4
-27 -18 -60 -25 -63 -58 -199 -50 -208 3 -2 1 -12 -4 -22 -5 -10 -6 -20 -3
-24 4 -4 8 -23 10 -44 9 -107 77 -201 183 -251 33 -16 56 -32 52 -36 -4 -5 -2
-5 4 -2 6 4 44 1 85 -6 41 -7 102 -12 136 -12 41 1 60 -2 55 -9 -3 -6 2 -5 11
3 11 8 44 14 86 15 38 0 67 4 64 9 -4 6 10 8 79 11 14 0 24 3 22 5 -4 4 33 14
145 37 13 3 33 10 43 15 11 6 26 8 34 5 10 -4 12 -2 8 6 -5 8 -2 9 9 5 10 -3
17 -2 17 3 0 6 8 10 18 10 9 0 36 9 58 19 32 15 45 16 54 8 9 -9 11 -9 8 2 -2
7 2 15 8 18 10 3 23 -34 38 -108 2 -8 6 -21 9 -29 15 -36 70 -206 70 -217 0
-7 4 -13 8 -13 11 0 30 -61 22 -74 -3 -6 -3 -8 2 -4 8 7 86 -135 88 -159 0 -7
4 -13 8 -13 13 0 39 -56 31 -66 -4 -5 -3 -6 2 -2 5 4 22 -12 39 -35 17 -23 49
-59 71 -80 23 -21 40 -42 39 -47 -2 -6 0 -9 5 -8 15 3 63 -22 68 -37 3 -8 10
-12 15 -10 5 3 22 -1 39 -10 17 -9 35 -13 40 -10 6 3 10 2 10 -2 0 -5 12 -8
28 -8 15 1 33 -4 41 -9 11 -8 13 -8 8 0 -4 7 7 13 33 17 21 3 53 12 71 21 17
9 36 13 42 9 5 -3 7 -1 3 5 -3 6 3 13 14 17 11 3 20 11 20 16 0 5 5 9 11 9 5
0 7 -6 3 -13 -4 -7 -3 -9 2 -4 5 5 9 12 9 17 0 4 14 21 30 37 69 67 162 196
179 251 4 12 12 20 17 16 5 -3 8 -2 7 3 -1 4 6 29 17 56 14 33 24 45 35 41 11
-4 12 -2 4 6 -14 14 -6 52 9 43 5 -3 7 -2 4 4 -3 5 8 45 24 88 17 44 33 86 35
94 7 34 56 206 59 209 1 1 15 -3 31 -9 16 -7 34 -13 39 -15 61 -18 144 -51
139 -56 -3 -4 3 -4 14 -1 11 2 23 0 26 -6 4 -5 14 -7 23 -4 9 4 14 2 10 -3 -3
-5 7 -9 21 -8 34 1 63 -6 58 -15 -2 -3 19 -7 47 -9 28 -2 55 -6 61 -10 13 -8
18 -9 79 -10 27 -1 46 -5 44 -9 -3 -5 20 -6 50 -5 44 3 54 1 48 -10 -6 -10 -5
-11 6 0 16 15 33 16 23 0 -5 -7 -3 -8 7 -3 7 5 29 10 49 12 74 5 116 11 135
18 11 4 37 13 57 21 28 10 38 11 41 1 4 -8 6 -7 6 3 1 8 21 28 46 44 43 26 92
86 109 131 28 73 27 217 -3 313 -5 18 -10 33 -9 35 2 23 -118 257 -184 356
-44 67 -124 177 -138 191 -3 3 -34 39 -70 80 -36 41 -97 107 -137 146 l-72 71
25 22 c14 12 30 19 36 15 6 -4 8 -3 4 4 -3 6 24 42 61 80 38 38 86 91 108 117
22 27 46 54 53 61 6 7 12 17 12 23 0 6 3 11 8 11 4 0 22 22 41 50 19 27 37 47
41 45 4 -3 7 3 6 13 0 9 5 16 12 14 8 -1 11 2 7 7 -5 9 36 83 55 101 5 5 101
200 121 245 7 18 26 84 36 125 2 8 4 27 5 42 0 16 5 25 11 21 6 -4 7 -1 2 7
-11 18 -11 62 0 80 5 8 4 11 -2 7 -6 -4 -12 10 -16 36 -6 51 -10 70 -18 77 -3
3 -15 21 -27 41 -42 70 -184 145 -292 155 -205 20 -451 -18 -709 -108 -30 -10
-60 -17 -68 -14 -8 3 -12 2 -9 -3 5 -7 -42 -31 -60 -31 -1 0 -5 15 -9 33 -18
86 -108 342 -156 444 -17 35 -29 66 -29 69 1 4 -2 10 -7 13 -5 3 -24 32 -42
64 -108 190 -245 296 -403 311 -20 2 -39 2 -41 -1z m53 -124 c0 -5 5 -7 10 -4
14 9 52 -4 45 -16 -3 -5 0 -6 8 -4 17 7 69 -33 61 -47 -5 -7 -2 -8 5 -4 13 8
43 -23 35 -36 -3 -4 1 -6 8 -3 7 2 24 -11 38 -31 14 -19 30 -41 35 -47 50 -56
148 -233 139 -249 -4 -6 -3 -9 2 -5 12 7 97 -192 90 -210 -3 -8 -2 -12 3 -9
11 7 45 -120 35 -135 -4 -8 -3 -9 4 -5 7 4 12 3 12 -3 0 -5 3 -16 6 -25 4 -11
-23 -30 -113 -75 -138 -71 -276 -145 -350 -189 -29 -17 -53 -29 -53 -26 0 3
-7 -1 -15 -10 -14 -14 -19 -13 -53 6 -20 11 -62 34 -92 51 -30 16 -59 33 -65
37 -5 4 -86 47 -180 95 -93 48 -173 91 -177 94 -11 10 3 52 15 44 6 -3 7 -1 3
6 -9 14 12 113 22 107 4 -2 8 7 8 20 2 24 42 134 53 144 3 3 7 12 9 20 2 8 17
44 33 80 24 50 32 61 40 50 8 -11 9 -10 4 7 -5 17 10 47 60 123 36 55 71 98
76 94 5 -3 9 -2 8 3 -4 23 2 35 17 29 8 -3 12 -2 9 3 -7 12 60 78 100 99 25
13 70 27 98 31 4 1 7 -4 7 -10z m-1411 -783 c9 -6 12 -5 8 1 -4 6 10 10 34 11
29 1 38 -1 33 -11 -5 -9 -4 -9 7 1 7 6 19 12 27 12 8 0 10 -5 6 -12 -6 -10 -5
-10 7 -1 9 7 26 10 40 8 13 -3 47 -8 74 -11 63 -7 155 -23 175 -31 15 -6 35
-10 71 -12 12 -1 17 -5 13 -13 -4 -7 -3 -8 4 -4 6 4 31 1 54 -5 24 -7 49 -13
55 -15 25 -5 73 -29 73 -37 0 -5 4 -6 9 -3 12 8 41 -4 41 -16 0 -5 -4 -6 -10
-3 -6 3 -7 -1 -4 -9 3 -9 1 -45 -5 -81 -6 -36 -14 -91 -17 -122 -4 -32 -11
-61 -18 -65 -8 -6 -7 -8 2 -8 7 0 11 -4 8 -8 -3 -4 -8 -42 -11 -83 -4 -40 -9
-81 -12 -89 -3 -8 -6 -44 -8 -80 -1 -36 -3 -65 -4 -65 -1 0 -3 -27 -4 -61 -3
-66 0 -62 -92 -139 -33 -27 -62 -53 -63 -58 -2 -4 -8 -5 -13 -1 -5 3 -9 1 -9
-3 0 -5 -41 -44 -91 -88 -50 -43 -98 -85 -106 -93 -13 -14 -23 -6 -91 64 -86
90 -172 188 -186 213 -5 9 -12 18 -16 21 -12 9 -106 154 -139 215 -18 33 -37
66 -43 72 -6 7 -8 20 -4 28 3 9 2 14 -3 11 -19 -12 -102 225 -105 296 0 27 -4
48 -7 48 -16 0 9 108 32 142 22 31 125 81 151 74 10 -2 18 0 18 5 0 5 14 7 30
5 17 -2 30 0 30 4 0 9 42 7 59 -4z m2823 1 c6 -9 8 -9 8 1 0 7 4 10 10 7 5 -3
29 -8 52 -11 113 -13 201 -66 197 -118 0 -5 4 -12 10 -15 9 -6 11 -27 11 -124
0 -16 -4 -26 -9 -23 -5 3 -7 -2 -4 -13 11 -41 -89 -307 -110 -294 -6 3 -7 1
-3 -6 13 -20 -142 -281 -166 -281 -6 0 -8 -3 -5 -7 11 -10 -51 -84 -197 -238
l-86 -89 -47 44 c-26 25 -52 50 -58 55 -7 6 -39 33 -71 61 -33 29 -86 73 -119
100 -86 69 -87 71 -90 112 -5 69 -16 207 -21 242 -4 33 -10 92 -18 170 -4 37
-14 114 -21 165 -2 17 -9 52 -14 80 -5 27 -9 50 -8 51 50 22 109 44 129 48 15
2 36 10 48 16 12 6 28 9 35 6 8 -3 15 -1 17 5 2 5 19 11 39 13 20 2 40 6 45 9
5 3 29 8 54 12 25 4 48 9 52 11 5 3 8 -1 8 -8 0 -9 2 -10 8 -2 9 16 43 21 61
10 10 -7 12 -6 6 4 -6 10 -3 12 12 8 12 -3 25 -3 31 1 20 12 206 11 214 -2z
m-1950 -189 c23 -13 46 -27 49 -31 3 -5 9 -8 13 -7 15 3 64 -25 59 -34 -3 -5
-1 -6 5 -2 14 9 63 -14 55 -27 -3 -6 -2 -7 4 -4 5 3 56 -21 113 -53 57 -33
107 -60 111 -60 4 0 11 -5 15 -12 5 -8 2 -9 -9 -5 -9 3 -16 2 -14 -2 1 -5 -48
-42 -110 -83 -61 -40 -144 -95 -183 -123 -40 -27 -80 -54 -89 -60 -9 -5 -18
-12 -21 -15 -17 -17 -80 -60 -80 -54 0 3 -9 -4 -20 -17 l-20 -24 6 105 c3 58
7 121 9 140 2 19 5 52 6 72 1 20 5 36 8 34 3 -2 7 19 7 47 2 79 15 149 31 176
8 13 10 20 4 17 -6 -4 -11 -2 -11 3 0 6 4 11 10 11 5 0 7 7 4 15 -8 20 -4 19
48 -7z m1114 -66 c3 -26 8 -56 10 -67 10 -50 19 -145 19 -192 0 -28 3 -49 7
-47 4 3 6 -16 5 -41 -1 -25 0 -45 3 -45 3 0 6 -32 7 -70 2 -39 -1 -70 -5 -70
-5 0 -18 9 -30 20 -12 11 -26 20 -31 20 -5 0 -17 10 -25 22 -9 12 -16 19 -16
15 0 -6 -92 56 -135 90 -25 20 -253 169 -278 182 -15 8 -26 15 -25 16 9 7 158
95 184 108 17 9 34 13 38 10 3 -4 6 -1 6 5 0 11 175 102 197 102 7 0 13 4 13
9 0 5 8 11 18 13 20 4 29 -14 38 -80z m-531 -268 c30 -20 55 -41 55 -45 0 -5
6 -8 13 -6 6 1 11 -4 9 -11 -1 -8 2 -11 7 -7 14 8 82 -38 76 -52 -2 -7 2 -10
11 -6 8 3 25 -3 37 -14 12 -11 35 -27 50 -37 16 -9 25 -22 21 -28 -4 -7 -3 -8
4 -4 10 6 242 -148 250 -166 2 -5 9 -8 16 -8 7 0 21 -9 31 -20 17 -19 14 -24
-10 -21 -5 1 -3 -4 5 -11 12 -10 15 -40 16 -138 0 -77 -4 -128 -10 -132 -7 -5
-7 -8 -1 -8 12 0 14 -259 2 -277 -5 -7 -4 -13 1 -13 12 0 7 -79 -5 -90 -7 -7
-244 -174 -287 -203 -10 -6 -24 -16 -30 -22 -12 -10 -34 -26 -211 -146 -96
-65 -108 -71 -131 -60 -14 6 -22 16 -19 21 3 5 0 7 -7 5 -8 -3 -23 4 -35 15
-12 11 -24 20 -27 20 -5 0 -246 166 -271 187 -5 4 -30 22 -55 38 -25 17 -49
33 -55 38 -5 4 -44 31 -85 61 l-75 54 0 337 c1 317 2 338 19 351 11 7 23 11
29 7 6 -3 7 -1 3 5 -4 7 3 17 18 24 14 6 26 15 26 20 0 4 7 8 15 8 8 0 15 5
15 11 0 6 7 8 16 5 8 -3 13 -2 9 3 -3 5 12 20 32 32 21 13 41 27 44 32 4 5 12
6 19 2 8 -5 11 -4 7 2 -9 15 38 44 60 37 12 -4 14 -3 6 3 -9 6 1 19 37 46 27
20 54 37 59 37 5 0 11 3 13 8 5 12 131 95 144 95 7 0 12 4 11 8 -2 8 51 47 66
48 4 1 32 -15 62 -35z m-822 -752 c2 -270 4 -262 -48 -215 -12 10 -41 34 -65
53 -43 33 -83 67 -157 136 l-34 31 57 49 c177 153 219 185 228 176 4 -4 7 -2
6 3 -4 17 0 29 6 23 3 -4 6 -119 7 -256z m1539 245 c7 -7 26 -20 41 -30 15
-10 25 -23 21 -29 -4 -7 -3 -8 4 -4 7 4 12 2 12 -3 0 -6 6 -10 13 -8 6 1 11
-4 9 -11 -1 -8 2 -11 7 -8 5 4 14 -2 20 -11 5 -10 13 -18 18 -18 4 0 7 -3 6
-7 -2 -5 1 -7 5 -5 5 1 37 -22 71 -52 l62 -54 -53 -49 c-29 -27 -66 -59 -81
-71 -16 -12 -34 -27 -41 -33 -43 -40 -129 -105 -133 -101 -2 3 0 14 6 25 7 13
7 23 -2 32 -9 10 -9 14 1 17 6 3 9 9 6 14 -5 9 -8 289 -4 314 1 6 2 14 1 19
-4 22 -8 86 -5 86 1 0 9 -6 16 -13z m-1862 -353 c48 -45 72 -65 109 -93 17
-13 31 -30 31 -37 0 -8 3 -13 8 -13 21 4 32 -3 32 -18 0 -9 3 -14 6 -10 4 3
13 -1 21 -9 8 -8 30 -26 49 -40 18 -15 31 -31 27 -37 -3 -5 -2 -7 4 -4 23 14
43 -19 48 -78 10 -127 16 -186 40 -410 4 -33 10 -79 14 -102 5 -27 4 -46 -3
-54 -8 -9 -8 -10 0 -6 7 4 14 -2 17 -13 3 -11 0 -20 -5 -20 -6 0 -5 -6 2 -15
7 -8 10 -26 8 -40 -3 -13 -1 -22 3 -19 5 3 9 1 9 -5 0 -8 -35 -25 -50 -23 -3
0 -14 -5 -25 -11 -11 -6 -22 -12 -25 -12 -3 -1 -36 -11 -75 -23 -61 -18 -120
-33 -190 -48 -11 -2 -40 -7 -65 -10 -25 -3 -49 -8 -53 -11 -5 -3 -70 -6 -145
-8 -122 -3 -168 0 -263 18 -72 14 -145 78 -155 136 -3 20 -8 44 -11 54 -2 9
-1 17 4 17 4 0 8 17 7 37 -1 50 6 74 20 66 8 -4 8 -3 0 6 -12 14 33 164 47
155 5 -3 6 2 3 10 -3 9 6 36 20 62 14 26 26 53 26 60 0 8 5 14 10 14 6 0 10 5
10 11 0 22 93 168 103 162 6 -3 7 -2 4 4 -10 17 64 118 77 105 4 -3 5 0 2 8
-4 9 12 35 39 65 25 28 73 80 106 117 34 38 65 65 70 62 5 -3 8 -2 7 3 -5 13
26 42 37 35 6 -3 26 -21 45 -38z m2364 -103 c-1 -3 7 -12 18 -19 10 -7 15 -18
12 -24 -4 -6 -2 -8 3 -5 6 4 21 -8 34 -25 13 -17 29 -36 34 -42 30 -33 63 -84
58 -90 -4 -3 -1 -6 5 -6 17 0 85 -108 76 -121 -4 -7 -3 -9 3 -6 13 8 106 -169
97 -185 -4 -6 -3 -8 3 -5 12 7 36 -50 28 -63 -3 -4 1 -10 9 -13 7 -3 17 -23
21 -44 5 -21 10 -45 12 -53 2 -8 4 -24 6 -35 1 -11 6 -23 10 -26 5 -3 8 -36 8
-73 0 -178 -108 -238 -417 -231 -78 2 -150 5 -161 8 -10 3 -34 8 -53 11 -19 3
-48 8 -65 11 -114 22 -337 94 -329 106 3 5 -2 6 -12 2 -12 -5 -15 -2 -11 13 6
20 16 78 22 129 2 17 6 46 9 65 3 19 8 60 11 90 3 30 8 69 11 87 2 17 7 62 10
100 3 37 10 73 15 80 7 7 6 14 -1 18 -6 3 -7 12 -3 18 4 6 6 26 5 45 -2 25 1
32 10 26 10 -6 10 -5 2 7 -16 21 -6 71 18 88 11 8 30 25 41 38 11 12 24 23 29
23 4 0 25 16 45 36 63 60 110 94 118 86 5 -4 5 -2 2 4 -4 7 9 24 29 39 20 15
49 40 66 57 l30 29 71 -72 c40 -39 71 -74 71 -78z m-1901 -294 c-3 -5 -2 -7 4
-4 12 8 83 -39 83 -55 0 -6 3 -8 6 -5 7 7 136 -80 142 -95 2 -4 10 -8 18 -8 8
0 14 -3 14 -8 0 -4 20 -18 45 -32 25 -14 45 -28 45 -32 0 -5 5 -8 10 -8 12 0
124 -72 128 -82 2 -5 11 -8 20 -8 10 0 -36 -32 -101 -70 -65 -39 -124 -67
-130 -64 -7 4 -8 3 -4 -2 5 -5 -41 -33 -110 -66 -80 -38 -120 -52 -125 -45 -3
7 -9 32 -12 57 -3 25 -8 58 -11 74 -3 15 -8 49 -11 75 -3 25 -7 62 -9 81 -2
19 -7 62 -10 94 -3 33 -8 64 -11 68 -3 4 0 8 6 8 6 0 8 5 4 11 -3 6 -8 43 -11
82 -5 65 -4 70 11 58 9 -7 13 -18 9 -24z m1261 -64 c-4 -94 -9 -162 -18 -228
-2 -16 -7 -55 -10 -85 -4 -30 -8 -62 -10 -70 -1 -8 -7 -37 -11 -65 -5 -27 -10
-53 -11 -57 -1 -5 -2 -13 -3 -20 -1 -9 -4 -9 -13 0 -7 7 -20 12 -30 12 -10 0
-18 5 -18 12 0 6 -3 9 -6 5 -4 -3 -54 19 -113 50 -58 31 -123 64 -143 75 -21
10 -38 23 -38 29 0 6 -4 8 -9 5 -5 -3 -19 2 -32 12 -13 10 -37 24 -52 32 -25
12 -26 15 -10 24 10 6 22 11 26 11 4 0 11 5 15 12 4 6 13 13 21 15 8 2 24 12
36 23 13 12 36 27 52 35 15 8 35 21 43 28 8 7 18 12 23 12 4 0 16 8 26 18 42
37 52 43 64 36 6 -4 9 -3 4 1 -9 11 43 46 56 38 6 -3 7 -2 4 4 -3 5 27 33 68
62 41 28 77 56 80 61 12 20 13 8 9 -87z m-524 -403 c8 -5 30 -17 48 -26 17 -9
32 -21 32 -26 0 -5 4 -7 9 -3 5 3 36 -11 68 -30 32 -19 63 -35 69 -35 5 0 21
-10 34 -22 14 -13 25 -21 25 -18 1 8 154 -64 155 -73 0 -5 -4 -5 -10 -2 -6 4
-7 -1 -3 -10 3 -10 0 -21 -8 -26 -11 -6 -11 -9 -1 -9 9 0 10 -5 4 -17 -5 -10
-15 -40 -21 -68 -7 -27 -18 -63 -26 -80 -7 -16 -24 -58 -36 -92 -13 -34 -28
-59 -33 -56 -5 3 -6 1 -3 -4 9 -14 -22 -75 -34 -68 -5 4 -6 -1 -3 -10 4 -9 -5
-33 -20 -55 -14 -22 -26 -42 -26 -46 0 -3 -19 -33 -42 -67 -185 -267 -312
-305 -471 -142 -56 56 -164 205 -150 205 4 0 3 4 -3 8 -14 8 -97 164 -109 202
-4 14 -16 43 -26 65 -25 55 -76 216 -81 251 -2 23 1 29 14 28 10 -1 16 1 14 5
-3 4 19 18 47 31 29 13 55 29 59 35 4 5 8 7 8 3 0 -4 23 6 50 22 28 16 50 26
50 22 0 -4 4 -2 8 3 4 6 25 19 47 30 22 11 47 25 55 31 38 28 169 92 176 86 4
-4 4 -2 1 5 -19 32 42 11 133 -47z"/>
<path d="M2556 3192 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
-9 -8z"/>
<path d="M2455 2831 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
<path d="M2500 2790 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
<path d="M2144 2592 c-70 -35 -108 -103 -100 -179 3 -38 32 -93 62 -118 9 -7
1 -43 -31 -140 -65 -196 -66 -176 5 -173 33 2 60 -1 60 -5 0 -5 4 -6 8 -3 13
8 227 10 239 3 18 -11 23 7 13 39 -6 16 -28 83 -49 149 l-39 120 29 27 c16 16
29 30 29 33 0 22 1 26 8 22 4 -3 8 20 8 51 3 88 -33 145 -108 176 -44 19 -96
18 -134 -2z"/>
<path d="M1113 2105 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
<path d="M859 1903 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
<path d="M1436 1803 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
<path d="M3760 1596 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
13z"/>
<path d="M1616 1691 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
<path d="M2710 1590 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
-4 -4 -4 -10z"/>
<path d="M1090 831 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@ -0,0 +1,37 @@
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: normal;
/*400*/
font-display: swap;
src: url("./worksans-regular-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("./worksans-medium-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("./worksans-semibold-webfont.woff2") format("woff2");
}
/* latin */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: bold;
/*700*/
font-display: swap;
src: url("./worksans-bold-webfont.woff2") format("woff2");
}

BIN
.storybook/static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -20,32 +20,108 @@
<a href="https://github.com/thomasdarimont/awesome-keycloak">
<img src="https://awesome.re/mentioned-badge.svg"/>
</a>
<a href="https://discord.gg/rBzsYtUn">
<img src="https://img.shields.io/discord/1097708346976505977"/>
</a>
<p align="center">
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
-
<a href="https://storybook.keycloakify.dev/storybook">Storybook</a>
<a href="https://storybook.keycloakify.dev">Storybook</a>
-
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
</p>
</p>
> 🗣️🔈 Sorry the latest release is broken, fixing ASAP
<p align="center">
<i>Ultimately this build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
The more ⭐️ the project gets, the more time I spend improving and maintaining it. Thank you for your support 😊
## Sponsor 👼
> 🗣 V7 have been released 🎉
> [It features major improvements](https://github.com/keycloakify/keycloakify#70-).
> Checkout [the migration guide](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), a French company offering Keycloak as a service.
Their dedicated support helps us continue the development and maintenance of this project.
[Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github) provides the following services:
- Simplify and secure your Keycloak Identity and Access Management. Keycloak as a Service.
- Custom theme building for your brand using Keycloakify.
<div align="center">
![Logo Dark](https://user-images.githubusercontent.com/6702424/234135797-c84d0a90-0526-43e5-a186-70cbebdeb278.png#gh-dark-mode-only)
</div>
<div align="center">
![Logo Light](https://user-images.githubusercontent.com/6702424/234135799-68684c33-4ec5-48d4-8763-0f3922c86643.png#gh-light-mode-only)
</div>
<p align="center">
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud IAM</a> and use promo code <code>keycloakify5</code></i>
<br/>
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
</p>
Thank you, [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), for your support!
## Contributors ✨
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lordvlad"><img src="https://avatars.githubusercontent.com/u/1217769?v=4?s=100" width="100px;" alt="Waldemar Reusch"/><br /><sub><b>Waldemar Reusch</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lordvlad" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/"><img src="https://avatars.githubusercontent.com/u/10997562?v=4?s=100" width="100px;" alt="William Will"/><br /><sub><b>William Will</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=willwill96" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ann2827"><img src="https://avatars.githubusercontent.com/u/32645809?v=4?s=100" width="100px;" alt="Bystrova Ann"/><br /><sub><b>Bystrova Ann</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Ann2827" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkreuzmayr"><img src="https://avatars.githubusercontent.com/u/20108212?v=4?s=100" width="100px;" alt="Michael Kreuzmayr"/><br /><sub><b>Michael Kreuzmayr</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=mkreuzmayr" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://coolmathgames.tech"><img src="https://avatars.githubusercontent.com/u/6877780?v=4?s=100" width="100px;" alt="Mary "/><br /><sub><b>Mary </b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Mstrodl" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://tasyp.xyz/"><img src="https://avatars.githubusercontent.com/u/6623212?v=4?s=100" width="100px;" alt="German Öö"/><br /><sub><b>German Öö</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Tasyp" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://revolunet.com"><img src="https://avatars.githubusercontent.com/u/124937?v=4?s=100" width="100px;" alt="Julien Bouquillon"/><br /><sub><b>Julien Bouquillon</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=revolunet" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aidangilmore"><img src="https://avatars.githubusercontent.com/u/32880357?v=4?s=100" width="100px;" alt="Aidan Gilmore"/><br /><sub><b>Aidan Gilmore</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=aidangilmore" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0x-Void"><img src="https://avatars.githubusercontent.com/u/32745739?v=4?s=100" width="100px;" alt="Void"/><br /><sub><b>Void</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=0x-Void" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juffe"><img src="https://avatars.githubusercontent.com/u/5393231?v=4?s=100" width="100px;" alt="juffe"/><br /><sub><b>juffe</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=juffe" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lazToum"><img src="https://avatars.githubusercontent.com/u/4764837?v=4?s=100" width="100px;" alt="Lazaros Toumanidis"/><br /><sub><b>Lazaros Toumanidis</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lazToum" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcmrf"><img src="https://avatars.githubusercontent.com/u/9928519?v=4?s=100" width="100px;" alt="Marc"/><br /><sub><b>Marc</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=marcmrf" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://kasir-barati.github.io"><img src="https://avatars.githubusercontent.com/u/73785723?v=4?s=100" width="100px;" alt="Kasir Barati"/><br /><sub><b>Kasir Barati</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kasir-barati" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asashay"><img src="https://avatars.githubusercontent.com/u/10714670?v=4?s=100" width="100px;" alt="Alex Oliynyk"/><br /><sub><b>Alex Oliynyk</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=asashay" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
</tr>
</tbody>
</table>
<!-- markdownlint-restore -->
<!-- prettier-ignore-end -->
<!-- ALL-CONTRIBUTORS-LIST:END -->
# Changelog highlights
## 7.9
- Separate script for copying the default theme static assets to the public directory.
Theses assets are only needed for testing your theme locally in Storybook or with a `mockPageId`.
You are now expected to have a `"prepare": "copy-keycloak-resources-to-public",` in your package.json scripts.
This script will create `public/keycloak-assets` when you run `yarn install` (If you are using another package manager
like `pnpm` makes sure that `"prepare"` is actually ran.)
[See the updated starter](https://github.com/keycloakify/keycloakify-starter/blob/94532fcf10bf8b19e0873be8575fd28a8958a806/package.json#L11). `public/keycloak-assets` shouldn't be tracked by GIT and is automatically ignored.
## 7.7
- Better storybook support, see [the starter project](https://github.com/keycloakify/keycloakify-starter).
## 7.0 🍾
- Account theme support 🚀

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "7.6.5",
"version": "7.11.6",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -10,8 +10,8 @@
"types": "dist/index.d.ts",
"scripts": {
"prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/tsconfig.json && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"build:watch": "tsc -p src/tsconfig.json && (concurrently \"tsc -p src/tsconfig.json -w\" \"tsc-alias -p src/tsconfig.json\")",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
@ -24,13 +24,16 @@
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "yarn link-in-app keycloakify-starter",
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w "
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
},
"bin": {
"keycloakify": "dist/bin/keycloakify/index.js",
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
"copy-keycloak-resources-to-public": "dist/bin/copy-keycloak-resources-to-public.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js"
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js",
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
"keycloakify": "dist/bin/keycloakify/index.js"
},
"lint-staged": {
"*.{ts,tsx,json,md}": [
@ -66,23 +69,39 @@
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@emotion/react": "^11.10.6",
"@storybook/addon-a11y": "^6.5.16",
"@storybook/addon-actions": "^6.5.13",
"@storybook/addon-essentials": "^6.5.13",
"@storybook/addon-interactions": "^6.5.13",
"@storybook/addon-links": "^6.5.13",
"@storybook/builder-webpack5": "^6.5.13",
"@storybook/manager-webpack5": "^6.5.13",
"@storybook/react": "^6.5.13",
"@storybook/testing-library": "^0.0.13",
"@types/make-fetch-happen": "^10.0.1",
"@types/minimist": "^1.2.2",
"@types/node": "^18.15.3",
"@types/react": "18.0.9",
"@types/react": "^18.0.35",
"@types/react-dom": "^18.0.11",
"@types/yauzl": "^2.10.0",
"concurrently": "^7.6.0",
"concurrently": "^8.0.1",
"copyfiles": "^2.4.1",
"eslint-plugin-storybook": "^0.6.7",
"husky": "^4.3.8",
"lint-staged": "^11.0.0",
"powerhooks": "^0.26.7",
"prettier": "^2.3.0",
"properties-parser": "^0.3.1",
"react": "18.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2",
"ts-node": "^10.9.1",
"tsc-alias": "^1.8.3",
"typescript": "^5.0.1-rc",
"tss-react": "^4.8.2",
"typescript": "^4.9.1-beta",
"vitest": "^0.29.8",
"zod-to-json-schema": "^3.20.4"
},

View File

@ -4,7 +4,6 @@ import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep
import { crawl } from "../src/bin/tools/crawl";
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
import { getCliOptions } from "../src/bin/tools/cliOptions";
import { getLogger } from "../src/bin/tools/logger";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
@ -13,7 +12,8 @@ import { getLogger } from "../src/bin/tools/logger";
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const isSilent = true;
const logger = getLogger({ isSilent });
async function main() {

View File

@ -60,7 +60,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
</div>
</li>
)}
{referrer?.url !== undefined && (
{referrer?.url && (
<li>
<a href={referrer.url} id="referrer">
{msg("backTo", referrer.name)}

View File

@ -211,7 +211,9 @@ const keycloakifyExtraMessages = {
"shouldBeDifferent": "{0} should be different to {1}",
"shouldMatchPattern": "Pattern should match: `/{0}/`",
"mustBeAnInteger": "Must be an integer",
"notAValidOption": "Not a valid option"
"notAValidOption": "Not a valid option",
"newPasswordSameAsOld": "New password must be different from the old one",
"passwordConfirmNotMatch": "Password confirmation does not match"
},
"fr": {
/* spell-checker: disable */
@ -223,7 +225,9 @@ const keycloakifyExtraMessages = {
"logoutConfirmTitle": "Déconnexion",
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
"doLogout": "Se déconnecter"
"doLogout": "Se déconnecter",
"newPasswordSameAsOld": "Le nouveau mot de passe doit être différent de l'ancien",
"passwordConfirmNotMatch": "La confirmation du mot de passe ne correspond pas"
/* spell-checker: enable */
}
};

View File

@ -3,6 +3,7 @@ import Fallback from "keycloakify/account/Fallback";
export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { PageProps } from "keycloakify/account/pages/PageProps";

View File

@ -1,4 +1,4 @@
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import type { AccountThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
@ -7,6 +7,7 @@ export type KcContext = KcContext.Password | KcContext.Account;
export declare namespace KcContext {
export type Common = {
keycloakifyVersion: string;
themeType: "account";
locale?: {
supported: {
url: string;
@ -26,6 +27,8 @@ export declare namespace KcContext {
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
referrerURI?: string;
getLogoutUrl: () => string;
};
features: {
@ -38,13 +41,14 @@ export declare namespace KcContext {
internationalizationEnabled: boolean;
userManagedAccessAllowed: boolean;
};
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
referrer?: {
url?: string;
name: string;
url: string; // The url of the App
name: string; // Client id
};
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
@ -71,7 +75,6 @@ export declare namespace KcContext {
export type Account = Common & {
pageId: "account.ftl";
url: {
referrerURI: string;
accountUrl: string;
};
realm: {
@ -82,4 +85,15 @@ export declare namespace KcContext {
};
}
assert<Equals<KcContext["pageId"], AccountThemePageId>>();
{
type Got = KcContext["pageId"];
type Expected = AccountThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -0,0 +1,102 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}) {
const { mockData } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
if (mockDataPick !== undefined) {
deepAssign({
"target": out,
"source": mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
"target": out,
"source": storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "account") {
return { "kcContext": undefined as any };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
return { "kcContext": realKcContext as any };
}
return { getKcContext };
}

View File

@ -1,78 +1,21 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
import { id } from "tsafe/id";
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
const { getKcContext } = createGetKcContext({
mockData
});
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
const { kcContext } = getKcContext({ mockPageId });
console.log(
[
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
`If assets are missing make sure you have built your Keycloak theme at least once.`
].join(" "),
"background: red; color: yellow; font-size: medium"
);
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined };
}
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
return { "kcContext": undefined };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
}
return { "kcContext": realKcContext };
return { kcContext };
}

View File

@ -1,5 +1,5 @@
import "minimal-polyfills/Object.fromEntries";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
@ -8,9 +8,10 @@ const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "account",
"url": {
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
"resourceUrl": "#",
"accountUrl": "#",
"applicationsUrl": "#",

View File

@ -4,7 +4,7 @@ import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
@ -15,7 +15,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
}
});
const { url, realm, messagesPerField, stateChecker, account } = kcContext;
const { url, realm, messagesPerField, stateChecker, account, referrer } = kcContext;
const { msg } = i18n;
@ -99,7 +99,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
<div className="form-group">
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
{url.referrerURI !== undefined && <a href={url.referrerURI}>${msg("backToApplication")}</a>}
{referrer !== undefined && <a href={referrer?.url}>{msg("backToApplication")}</a>}
<button
type="submit"
className={clsx(
@ -124,7 +124,6 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
>
{msg("doCancel")}
</button>
I
</div>
</div>
</div>

View File

@ -1,10 +1,11 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
@ -17,16 +18,75 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
const { url, password, account, stateChecker } = kcContext;
const { msg } = i18n;
const { msgStr, msg } = i18n;
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [newPasswordConfirm, setNewPasswordConfirm] = useState("");
const [newPasswordError, setNewPasswordError] = useState("");
const [newPasswordConfirmError, setNewPasswordConfirmError] = useState("");
const [hasNewPasswordBlurred, setHasNewPasswordBlurred] = useState(false);
const [hasNewPasswordConfirmBlurred, setHasNewPasswordConfirmBlurred] = useState(false);
const checkNewPassword = (newPassword: string) => {
if (!password.passwordSet) {
return;
}
if (newPassword === currentPassword) {
setNewPasswordError(msgStr("newPasswordSameAsOld"));
} else {
setNewPasswordError("");
}
};
const checkNewPasswordConfirm = (newPasswordConfirm: string) => {
if (newPasswordConfirm === "") {
return;
}
if (newPassword !== newPasswordConfirm) {
setNewPasswordConfirmError(msgStr("passwordConfirmNotMatch"));
} else {
setNewPasswordConfirmError("");
}
};
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
<Template
{...{
kcContext: {
...kcContext,
"message": (() => {
if (newPasswordError !== "") {
return {
"type": "error",
"summary": newPasswordError
};
}
if (newPasswordConfirmError !== "") {
return {
"type": "error",
"summary": newPasswordConfirmError
};
}
return kcContext.message;
})()
},
i18n,
doUseDefaultCss,
classes
}}
active="password"
>
<div className="row">
<div className="col-md-10">
<h2>{msg("changePasswordHtmlTitle")}</h2>
</div>
<div className="col-md-2 subtitle">
<span className="subtitle">${msg("allFieldsRequired")}</span>
<span className="subtitle">{msg("allFieldsRequired")}</span>
</div>
</div>
@ -38,7 +98,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none;" }}
style={{ "display": "none" }}
/>
{password.passwordSet && (
@ -48,9 +108,17 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
{msg("password")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
<input
type="password"
className="form-control"
id="password"
name="password"
autoFocus
autoComplete="current-password"
value={currentPassword}
onChange={event => setCurrentPassword(event.target.value)}
/>
</div>
</div>
)}
@ -63,9 +131,27 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
{msg("passwordNew")}
</label>
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
<input
type="password"
className="form-control"
id="password-new"
name="password-new"
autoComplete="new-password"
value={newPassword}
onChange={event => {
const newPassword = event.target.value;
setNewPassword(newPassword);
if (hasNewPasswordBlurred) {
checkNewPassword(newPassword);
}
}}
onBlur={() => {
setHasNewPasswordBlurred(true);
checkNewPassword(newPassword);
}}
/>
</div>
</div>
@ -77,7 +163,26 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
</div>
<div className="col-sm-10 col-md-10">
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
<input
type="password"
className="form-control"
id="password-confirm"
name="password-confirm"
autoComplete="new-password"
value={newPasswordConfirm}
onChange={event => {
const newPasswordConfirm = event.target.value;
setNewPasswordConfirm(newPasswordConfirm);
if (hasNewPasswordConfirmBlurred) {
checkNewPasswordConfirm(newPasswordConfirm);
}
}}
onBlur={() => {
setHasNewPasswordConfirmBlurred(true);
checkNewPasswordConfirm(newPasswordConfirm);
}}
/>
</div>
</div>
@ -85,6 +190,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
<div>
<button
disabled={newPasswordError !== "" || newPasswordConfirmError !== ""}
type="submit"
className={clsx(
getClassName("kcButtonClass"),

View File

@ -0,0 +1,48 @@
#!/usr/bin/env node
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
import { join as pathJoin, relative as pathRelative } from "path";
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import { themeTypes } from "./keycloakify/generateFtl";
import * as fs from "fs";
(async () => {
const projectDirPath = process.cwd();
const buildOptions = readBuildOptions({
"processArgv": process.argv.slice(2),
"projectDirPath": process.cwd()
});
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir);
if (fs.existsSync(keycloakDirInPublicDir)) {
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
return;
}
for (const themeType of themeTypes) {
await downloadKeycloakStaticResources({
"isSilent": false,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeType": themeType,
"themeDirPath": keycloakDirInPublicDir
});
}
fs.writeFileSync(
pathJoin(keycloakDirInPublicDir, "README.txt"),
Buffer.from(
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`);
})();

View File

@ -2,7 +2,6 @@
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
import { readBuildOptions } from "./keycloakify/BuildOptions";
@ -21,28 +20,22 @@ export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: st
}
async function main() {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const buildOptions = readBuildOptions({
"projectDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ "isSilent": buildOptions.isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(
readBuildOptions({
"isSilent": true,
"isExternalAssetsCliParamProvided": false,
"projectDirPath": process.cwd()
}).keycloakifyBuildDirPath,
"src",
"main",
"resources",
"theme"
);
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
destDirPath,
isSilent
"isSilent": buildOptions.isSilent
});
}

View File

@ -4,13 +4,17 @@ import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme"
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
import { getEmailThemeSrcDirPath } from "./getSrcDirPath";
export async function main() {
const { isSilent } = getCliOptions(process.argv.slice(2));
const { isSilent } = readBuildOptions({
"projectDirPath": process.cwd(),
"processArgv": process.argv.slice(2)
});
const logger = getLogger({ isSilent });
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({

View File

@ -6,6 +6,7 @@ import { symToStr } from "tsafe/symToStr";
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
import * as fs from "fs";
import { join as pathJoin, sep as pathSep } from "path";
import parseArgv from "minimist";
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
@ -53,8 +54,17 @@ export namespace BuildOptions {
}
}
export function readBuildOptions(params: { projectDirPath: string; isExternalAssetsCliParamProvided: boolean; isSilent: boolean }): BuildOptions {
const { projectDirPath, isExternalAssetsCliParamProvided, isSilent } = params;
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
const { projectDirPath, processArgv } = params;
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
const argv = parseArgv(processArgv);
return {
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
})();
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
@ -143,7 +153,7 @@ export function readBuildOptions(params: { projectDirPath: string; isExternalAss
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
isSilent,
"isSilent": isSilentCliParamProvided,
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
"reactAppBuildDirPath": (() => {
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};

View File

@ -1,4 +1,5 @@
<script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
@ -120,7 +121,8 @@
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["pageId"] = "PAGE_ID_xIgLsPgGId9D8e";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["pageId"] = "${pageId}";
return out;

View File

@ -55,8 +55,9 @@ export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
themeType: ThemeType;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType } = params;
const $ = cheerio.load(indexHtmlCode);
@ -132,7 +133,8 @@ export function generateFtlFilesCodeFactory(params: {
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
)
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType),
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",

View File

@ -21,7 +21,8 @@ export const loginThemePageIds = [
"update-user-profile.ftl",
"idp-review-user-profile.ftl",
"update-email.ftl",
"select-authenticator.ftl"
"select-authenticator.ftl",
"saml-post-form.ftl"
] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;

View File

@ -0,0 +1,47 @@
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import type { ThemeType } from "../generateFtl";
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
import {
resourcesCommonDirPathRelativeToPublicDir,
resourcesDirPathRelativeToPublicDir,
basenameOfKeycloakDirInPublicDir
} from "../../mockTestingResourcesPath";
import * as crypto from "crypto";
export async function downloadKeycloakStaticResources(
// prettier-ignore
params: {
themeType: ThemeType;
themeDirPath: string;
isSilent: boolean;
keycloakVersion: string;
}
) {
const { themeType, isSilent, themeDirPath, keycloakVersion } = params;
const tmpDirPath = pathJoin(
themeDirPath,
"..",
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir))
});
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir))
});
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
}

View File

@ -1,14 +1,14 @@
import { transformCodebase } from "../tools/transformCodebase";
import { transformCodebase } from "../../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { join as pathJoin } from "path";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl";
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath";
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -21,6 +21,7 @@ export namespace BuildOptionsLike {
isSilent: boolean;
customUserAttributes: string[];
themeVersion: string;
keycloakVersionDefaultAssets: string;
};
export type Standalone = Common & {
@ -49,15 +50,14 @@ export namespace BuildOptionsLike {
assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateKeycloakThemeResources(params: {
export async function generateTheme(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
emailThemeSrcDirPath: string | undefined;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
keycloakifyVersion: string;
}): Promise<{ doBundlesEmailTemplate: boolean }> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, keycloakVersion, buildOptions, keycloakifyVersion } = params;
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
@ -84,7 +84,7 @@ export async function generateKeycloakThemeResources(params: {
if (
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
"dirPath": pathJoin(reactAppBuildDirPath, basenameOfKeycloakDirInPublicDir),
filePath
})
) {
@ -141,7 +141,8 @@ export async function generateKeycloakThemeResources(params: {
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
"cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions,
keycloakifyVersion
keycloakifyVersion,
themeType
});
return generateFtlFilesCode;
@ -172,49 +173,47 @@ export async function generateKeycloakThemeResources(params: {
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
});
{
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
//TODO: Remove this block we left it for now only for backward compatibility
// we now have a separate script for this
copy_keycloak_resources_to_public: {
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
await downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
if (fs.existsSync(keycloakDirInPublicDir)) {
break copy_keycloak_resources_to_public;
}
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"themeDirPath": keycloakDirInPublicDir,
themeType
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
if (themeType !== themeTypes[0]) {
break copy_keycloak_resources_to_public;
}
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
pathJoin(keycloakDirInPublicDir, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(
" "
)
// prettier-ignore
[
"This is just a test folder that helps develop",
"the login and register page without having to run a Keycloak container"
].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.rmSync(tmpDirPath, { recursive: true, force: true });
fs.writeFileSync(pathJoin(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
}
await downloadKeycloakStaticResources({
"isSilent": buildOptions.isSilent,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
themeDirPath,
themeType
});
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")

View File

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

View File

@ -1,4 +1,4 @@
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
import { generateTheme } from "./generateTheme";
import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
import * as child_process from "child_process";
@ -6,7 +6,6 @@ import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTe
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
import jar from "../tools/jar";
import { assert } from "tsafe/assert";
import { Equals } from "tsafe";
@ -14,19 +13,17 @@ import { getEmailThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot";
export async function main() {
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const projectDirPath = process.cwd();
const buildOptions = readBuildOptions({
projectDirPath,
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
"processArgv": process.argv.slice(2)
});
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const { doBundlesEmailTemplate } = await generateTheme({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
"emailThemeSrcDirPath": (() => {
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath });
@ -39,7 +36,6 @@ export async function main() {
})(),
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
buildOptions,
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
"keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"];
@ -62,7 +58,7 @@ export async function main() {
case "keycloakify":
logger.log("🫶 Let keycloakify do its thang");
await jar({
"rootPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"),
"rootPath": buildOptions.keycloakifyBuildDirPath,
"version": buildOptions.themeVersion,
"groupId": buildOptions.groupId,
"artifactId": buildOptions.artifactId,
@ -132,16 +128,18 @@ export async function main() {
``,
`Once your container is up and running: `,
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
`- Create a realm: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
`- Create a client id myclient`,
` Root URL: https://www.keycloak.org/app/`,
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
` Web origins: *`,
` Login Theme: ${buildOptions.themeName}`,
` Save (button at the bottom of the page)`,
`- Create a realm: Master -> AddRealm -> Name: myrealm`,
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
`- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeName}`,
` Clients -> account -> Login theme: ${buildOptions.themeName}`,
`- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeName} (option will appear only if you have ran npx initialize-email-theme)`,
`- Create a client Clients -> Create -> Client ID: myclient`,
` Root URL: https://www.keycloak.org/app/`,
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
` Web origins: *`,
` Login Theme: ${buildOptions.themeName}`,
` Save (button at the bottom of the page)`,
``,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { exec as execCallback } from "child_process";
import { createHash } from "crypto";
import { mkdir, stat, writeFile } from "fs/promises";
import { mkdir, readFile, stat, writeFile } from "fs/promises";
import fetch, { type FetchOptions } from "make-fetch-happen";
import { dirname as pathDirname, join as pathJoin } from "path";
import { assert } from "tsafe";
@ -25,32 +25,75 @@ async function exists(path: string) {
}
}
function ensureArray<T>(arg0: T | T[]) {
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
}
function ensureSingleOrNone<T>(arg0: T | T[]) {
if (!Array.isArray(arg0)) return arg0;
if (arg0.length === 0) return undefined;
if (arg0.length === 1) return arg0[0];
throw new Error("Illegal configuration, expected a single value but found multiple: " + arg0.map(String).join(", "));
}
type NPMConfig = Record<string, string | string[]>;
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
key in cfg ? { ...cfg, [key]: [...ensureArray(cfg[key]), value] } : { ...cfg, [key]: value };
/**
* Get npm configuration as map
*/
async function getNmpConfig(): Promise<Record<string, string>> {
async function getNmpConfig() {
return readNpmConfig().then(parseNpmConfig);
}
async function readNpmConfig() {
const { stdout } = await exec("npm config get", { encoding: "utf8" });
return stdout;
}
function parseNpmConfig(stdout: string) {
return stdout
.split("\n")
.filter(line => !line.startsWith(";"))
.map(line => line.trim())
.map(line => line.split("=", 2))
.reduce((cfg, [key, value]) => ({ ...cfg, [key]: value }), {});
.map(line => line.split("=", 2) as [string, string])
.reduce(npmConfigReducer, {} as NPMConfig);
}
function maybeBoolean(arg0: string | undefined) {
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
}
function chunks<T>(arr: T[], size: number = 2) {
return arr.map((_, i) => i % size == 0 && arr.slice(i, i + size)).filter(Boolean) as T[][];
}
async function readCafile(cafile: string) {
const cafileContent = await readFile(cafile, "utf-8");
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(ca => ca.join("").replace(/^\n/, "").replace(/\n/g, "\\n"));
}
/**
* Get proxy configuration from npm config files. Note that we don't care about
* Get proxy and ssl configuration from npm config files. Note that we don't care about
* proxy config in env vars, because make-fetch-happen will do that for us.
*
* @returns proxy configuration
*/
async function getNpmProxyConfig(): Promise<Pick<FetchOptions, "proxy" | "noProxy">> {
async function getFetchOptions(): Promise<Pick<FetchOptions, "proxy" | "noProxy" | "strictSSL" | "ca" | "cert">> {
const cfg = await getNmpConfig();
const proxy = cfg["https-proxy"] ?? cfg["proxy"];
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
const cert = cfg["cert"];
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
const cafile = ensureSingleOrNone(cfg["cafile"]);
return { proxy, noProxy };
if (typeof cafile !== "undefined" && cafile !== "null") ca.push(...(await readCafile(cafile)));
return { proxy, noProxy, strictSSL, cert, ca: ca.length === 0 ? undefined : ca };
}
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
@ -63,8 +106,8 @@ export async function downloadAndUnzip(params: { url: string; destDirPath: strin
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
if (!(await exists(zipFilePath))) {
const proxyOpts = await getNpmProxyConfig();
const response = await fetch(url, proxyOpts);
const opts = await getFetchOptions();
const response = await fetch(url, opts);
await mkdir(pathDirname(zipFilePath), { "recursive": true });
/**
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5

View File

@ -1,4 +1,4 @@
import { dirname, relative, sep } from "path";
import { dirname, relative, sep, join } from "path";
import { createWriteStream } from "fs";
import walk from "./walk";
@ -48,8 +48,12 @@ export async function jarStream({ groupId, artifactId, version, asyncPathGenerat
for await (const entry of asyncPathGeneratorFn()) {
if ("buffer" in entry) {
zipFile.addBuffer(entry.buffer, entry.zipPath);
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
zipFile.addFile(entry.fsPath, entry.zipPath);
} else if ("fsPath" in entry) {
if (entry.fsPath.endsWith(sep)) {
zipFile.addEmptyDirectory(entry.zipPath);
} else {
zipFile.addFile(entry.fsPath, entry.zipPath);
}
}
}
@ -65,15 +69,23 @@ export async function jarStream({ groupId, artifactId, version, asyncPathGenerat
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
* the contents of the pom.properties file which is going to be added to the archive.
* The root directory is expectedto have a conventional maven/gradle folder structure with a
* single `pom.xml` file at the root and a `src/main/resources` directory containing all
* application resources.
*/
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
await mkdir(dirname(targetPath), { recursive: true });
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
for await (const fsPath of walk(rootPath)) {
const zipPath = relative(rootPath, fsPath).split(sep).join("/");
const resourcesPath = join(rootPath, "src", "main", "resources");
for await (const fsPath of walk(resourcesPath)) {
const zipPath = relative(resourcesPath, fsPath).split(sep).join("/");
yield { fsPath, zipPath };
}
yield {
fsPath: join(rootPath, "pom.xml"),
zipPath: `META-INF/maven/${groupId}/${artifactId}/pom.xml`
};
};
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });

View File

@ -15,7 +15,7 @@ export function usePrepareTemplate(params: {
htmlClassName: string | undefined;
bodyClassName: string | undefined;
}) {
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, htmlClassName, bodyClassName } = params;
const { doFetchDefaultThemeResources, stylesCommon = [], styles = [], url, scripts = [], htmlClassName, bodyClassName } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
@ -26,36 +26,49 @@ export function usePrepareTemplate(params: {
let isUnmounted = false;
Promise.all(
const removeArray: (() => void)[] = [];
(async () => {
const prLoadedArray: Promise<void>[] = [];
[
...(stylesCommon ?? []).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...(styles ?? []).map(relativePath => pathJoin(url.resourcesPath, relativePath))
...stylesCommon.map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...styles.map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
.forEach(href => {
const { prLoaded, remove } = headInsert({
"type": "css",
href,
"position": "prepend"
})
)
).then(() => {
"position": "prepend",
href
});
removeArray.push(remove);
prLoadedArray.push(prLoaded);
});
await Promise.all(prLoadedArray);
if (isUnmounted) {
return;
}
setReady();
});
})();
(scripts ?? []).forEach(relativePath =>
headInsert({
scripts.forEach(relativePath => {
const { remove } = headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
});
removeArray.push(remove);
});
return () => {
isUnmounted = true;
removeArray.forEach(remove => remove());
};
}, []);

View File

@ -27,6 +27,7 @@ const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserP
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
const SelectAuthenticator = lazy(() => import("keycloakify/login/pages/SelectAuthenticator"));
const SamlPostForm = lazy(() => import("keycloakify/login/pages/SamlPostForm"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;
@ -81,6 +82,8 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
return <UpdateEmail kcContext={kcContext} {...rest} />;
case "select-authenticator.ftl":
return <SelectAuthenticator kcContext={kcContext} {...rest} />;
case "saml-post-form.ftl":
return <SamlPostForm kcContext={kcContext} {...rest} />;
}
assert<Equals<typeof kcContext, never>>(false);
})()}

View File

@ -4,6 +4,7 @@ export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { PageProps } from "keycloakify/login/pages/PageProps";

View File

@ -1,4 +1,4 @@
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
import type { LoginThemePageId, ThemeType } from "keycloakify/bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
@ -32,11 +32,13 @@ export type KcContext =
| KcContext.UpdateUserProfile
| KcContext.IdpReviewUserProfile
| KcContext.UpdateEmail
| KcContext.SelectAuthenticator;
| KcContext.SelectAuthenticator
| KcContext.SamlPostForm;
export declare namespace KcContext {
export type Common = {
keycloakifyVersion: string;
themeType: "login";
url: {
loginAction: string;
resourcesPath: string;
@ -85,6 +87,16 @@ export declare namespace KcContext {
};
};
export type SamlPostForm = Common & {
pageId: "saml-post-form.ftl";
samlPost: {
url: string;
SAMLRequest?: string;
SAMLResponse?: string;
RelayState?: string;
};
};
export type Login = Common & {
pageId: "login.ftl";
url: {
@ -333,7 +345,6 @@ export declare namespace KcContext {
totpSecretEncoded: string;
qrUrl: string;
policy: {
supportedApplications: string[];
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
@ -347,6 +358,7 @@ export declare namespace KcContext {
initialCounter: number;
}
);
supportedApplications: string[];
totpSecretQrCode: string;
manualUrl: string;
totpSecret: string;
@ -543,4 +555,15 @@ export declare namespace Validators {
};
}
assert<Equals<KcContext["pageId"], LoginThemePageId>>();
{
type Got = KcContext["pageId"];
type Expected = LoginThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}
assert<KcContext["themeType"] extends ThemeType ? true : false>();

View File

@ -0,0 +1,161 @@
import type { KcContext, Attribute } from "./KcContext";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}) {
const { mockData } = params ?? {};
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
console.log(`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`, "background: red; color: yellow; font-size: medium");
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
if (mockDataPick !== undefined) {
deepAssign({
"target": out,
"source": mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
"target": out,
"source": storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
if (
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
) {
assert(
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
);
const { attributes } = kcContextDefaultMock.profile;
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes = [];
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName = {};
const partialAttributes = [
...((partialKcContextCustomMock as DeepPartial<KcContext.RegisterUserProfile>).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute
});
if (partialAttribute !== undefined) {
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute
});
}
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
}
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined as any };
}
if (realKcContext.themeType !== "login") {
return { "kcContext": undefined as any };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
}
return { "kcContext": realKcContext as any };
}
return { getKcContext };
}

View File

@ -1,136 +1,21 @@
import type { KcContext, Attribute } from "./KcContext";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { pathBasename } from "keycloakify/tools/pathBasename";
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
import { loginThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
const { getKcContext } = createGetKcContext<KcContextExtension>({
mockData
});
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
const { kcContext } = getKcContext({ mockPageId });
console.log(
[
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
`If assets are missing make sure you have built your Keycloak theme at least once.`
].join(" "),
"background: red; color: yellow; font-size: medium"
);
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock
});
if (
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
) {
assert(
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
);
const { attributes } = kcContextDefaultMock.profile;
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes = [];
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName = {};
const partialAttributes = [
...((partialKcContextCustomMock as DeepPartial<KcContext.RegisterUserProfile>).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute
});
if (partialAttribute !== undefined) {
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute
});
}
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContext.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContext.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
}
}
return { kcContext };
}
if (realKcContext === undefined) {
return { "kcContext": undefined };
}
if (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
return { "kcContext": undefined };
}
{
const { url } = realKcContext;
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
}
return { "kcContext": realKcContext };
return { kcContext };
}

View File

@ -1,8 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import type { LoginThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
@ -102,10 +104,11 @@ const attributesByName = Object.fromEntries(attributes.map(attribute => [attribu
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"themeType": "login",
"url": {
"loginAction": "#",
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
},
@ -243,7 +246,7 @@ const loginUrl = {
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
};
export const kcContextMocks: KcContext[] = [
export const kcContextMocks = [
id<KcContext.Login>({
...kcContextCommonMock,
"pageId": "login.ftl",
@ -453,8 +456,8 @@ export const kcContextMocks: KcContext[] = [
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
supportedApplications: ["FreeOTP", "Google Authenticator"],
policy: {
supportedApplications: ["FreeOTP", "Google Authenticator"],
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,
@ -519,5 +522,27 @@ export const kcContextMocks: KcContext[] = [
}
]
}
}),
id<KcContext.SamlPostForm>({
...kcContextCommonMock,
pageId: "saml-post-form.ftl",
"samlPost": {
"url": "https://saml-post-url"
}
}),
id<KcContext.LoginPageExpired>({
...kcContextCommonMock,
pageId: "login-page-expired.ftl"
})
];
{
type Got = (typeof kcContextMocks)[number]["pageId"];
type Expected = LoginThemePageId;
type OnlyInGot = Exclude<Got, Expected>;
type OnlyInExpected = Exclude<Expected, Got>;
assert<Equals<OnlyInGot, never>>();
assert<Equals<OnlyInExpected, never>>();
}

View File

@ -3,6 +3,7 @@ import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import { MessageKey } from "keycloakify/login/i18n/i18n";
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -16,7 +17,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContext.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
@ -30,8 +31,8 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
<p>{msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps">
{totp.policy.supportedApplications.map(app => (
<li>{app}</li>
{totp.supportedApplications.map(app => (
<li>{msgStr(app as MessageKey, app)}</li>
))}
</ul>
</li>
@ -169,7 +170,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
name="cancel-aia"
value="true"
>
${msg("doCancel")}
{msg("doCancel")}
</button>
</>
) : (

View File

@ -22,17 +22,24 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
useEffect(() => {
let isCleanedUp = false;
headInsert({
const { prLoaded, remove } = headInsert({
"type": "javascript",
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
}).then(() => {
if (isCleanedUp) return;
});
(async () => {
await prLoaded;
if (isCleanedUp) {
return;
}
evaluateInlineScript();
});
})();
return () => {
isCleanedUp = true;
remove();
};
}, []);

View File

@ -11,7 +11,7 @@ export default function LoginVerifyEmail(props: PageProps<Extract<KcContext, { p
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("emailVerifyTitle")}>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email ?? "")}</p>
<p className="instruction">
{msg("emailVerifyInstruction2")}
<br />

View File

@ -14,7 +14,9 @@ export default function RegisterUserProfile(props: PageProps<Extract<KcContext,
classes
});
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey, realm } = kcContext;
realm.registrationEmailAsUsername;
const { msg, msgStr } = i18n;

View File

@ -0,0 +1,27 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function SamlPostForm(props: PageProps<Extract<KcContext, { pageId: "saml-post-form.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { msgStr, msg } = i18n;
const { samlPost } = kcContext;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayMessage={false} headerNode={msg("saml.post-form.title")}>
<script dangerouslySetInnerHTML={{ "__html": `window.onload = function() {document.forms[0].submit()};` }} />
<p>{msg("saml.post-form.message")}</p>
<form name="saml-post-binding" method="post" action={samlPost.url}>
{samlPost.SAMLRequest && <input type="hidden" name="SAMLRequest" value={samlPost.SAMLRequest} />}
{samlPost.SAMLResponse && <input type="hidden" name="SAMLResponse" value={samlPost.SAMLResponse} />}
{samlPost.RelayState && <input type="hidden" name="RelayState" value={samlPost.RelayState} />}
<noscript>
<p>{msg("saml.post-form.js-disabled")}</p>
<input type="submit" value={msgStr("doContinue")} />
</noscript>
</form>
</Template>
);
}

View File

@ -7,6 +7,9 @@ import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { typeGuard } from "tsafe/typeGuard";
export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext, { pageId: "webauthn-authenticate.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -21,10 +24,24 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
const createTimeout = Number(kcContext.createTimeout);
const isUserIdentified = kcContext.isUserIdentified == "true";
const formElementRef = useRef<HTMLFormElement>(null);
const webAuthnAuthenticate = useConstCallback(async () => {
if (!isUserIdentified) {
return;
}
const submitForm = async (): Promise<void> => {
const formElement = formElementRef.current;
if (formElement === null) {
await new Promise(resolve => setTimeout(resolve, 100));
return submitForm();
}
formElement.submit();
};
const allowCredentials = authenticators.authenticators.map(
authenticator =>
({
@ -57,30 +74,36 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
}
try {
const resultRaw = await navigator.credentials.get({ publicKey });
if (!resultRaw || resultRaw.type != "public-key") return;
const result = resultRaw as PublicKeyCredential;
if (!("authenticatorData" in result.response)) return;
const response = result.response as AuthenticatorAssertionResponse;
const result = await navigator.credentials.get({ publicKey });
if (!result || result.type != "public-key") {
return;
}
assert(is<PublicKeyCredential>(result));
if (!("authenticatorData" in result.response)) {
return;
}
const response = result.response;
const clientDataJSON = response.clientDataJSON;
assert(
typeGuard<AuthenticatorAssertionResponse>(response, "signature" in response && response.authenticatorData instanceof ArrayBuffer),
"response not an AuthenticatorAssertionResponse"
);
const authenticatorData = response.authenticatorData;
const signature = response.signature;
setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { pad: false }));
setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { pad: false }));
setSignature(base64url.stringify(new Uint8Array(signature), { pad: false }));
setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { "pad": false }));
setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { "pad": false }));
setSignature(base64url.stringify(new Uint8Array(signature), { "pad": false }));
setCredentialId(result.id);
setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { pad: false }));
submitForm();
setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { "pad": false }));
} catch (err) {
setError(String(err));
submitForm();
}
});
const webAuthForm = useRef<HTMLFormElement>(null);
const submitForm = useConstCallback(() => {
webAuthForm.current!.submit();
submitForm();
});
const [clientDataJSON, setClientDataJSON] = useState("");
@ -93,7 +116,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("webauthn-login-title")}>
<div id="kc-form-webauthn" className={getClassName("kcFormClass")}>
<form id="webauth" action={url.loginAction} ref={webAuthForm} method="post">
<form id="webauth" action={url.loginAction} ref={formElementRef} method="post">
<input type="hidden" id="clientDataJSON" name="clientDataJSON" value={clientDataJSON} />
<input type="hidden" id="authenticatorData" name="authenticatorData" value={authenticatorData} />
<input type="hidden" id="signature" name="signature" value={signature} />

View File

@ -12,7 +12,7 @@ export function headInsert(
type: "javascript";
src: string;
}
) {
): { remove: () => void; prLoaded: Promise<void> } {
const htmlElement = document.createElement(
(() => {
switch (params.type) {
@ -66,5 +66,8 @@ export function headInsert(
})()
](htmlElement);
return dLoaded.pr;
return {
"prLoaded": dLoaded.pr,
"remove": () => htmlElement.remove()
};
}

27
stories/account/KcApp.tsx Normal file
View File

@ -0,0 +1,27 @@
import React, { lazy, Suspense } from "react";
import Fallback from "../../dist/account";
import type { KcContext } from "./kcContext";
import { useI18n } from "./i18n";
const DefaultTemplate = lazy(() => import("../../dist/account/Template"));
export default function KcApp(props: { kcContext: KcContext }) {
const { kcContext } = props;
const i18n = useI18n({ kcContext });
if (i18n === null) {
return null;
}
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
default:
return <Fallback {...{ kcContext, i18n }} Template={DefaultTemplate} doUseDefaultCss={true} />;
}
})()}
</Suspense>
);
}

View File

@ -0,0 +1,19 @@
import React from "react";
import { getKcContext, type KcContext } from "./kcContext";
import KcApp from "./KcApp";
import type { DeepPartial } from "../../dist/tools/DeepPartial";
export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
const { pageId } = params;
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
const { kcContext } = getKcContext({
mockPageId: pageId,
storyPartialKcContext: params.kcContext
});
return <KcApp kcContext={kcContext} />;
}
return { PageStory };
}

5
stories/account/i18n.ts Normal file
View File

@ -0,0 +1,5 @@
import { createUseI18n } from "../../dist/account";
export const { useI18n } = createUseI18n({});
export type I18n = NonNullable<ReturnType<typeof useI18n>>;

View File

@ -0,0 +1,7 @@
import { createGetKcContext } from "../../dist/account";
export const { getKcContext } = createGetKcContext();
const { kcContext } = getKcContext();
export type KcContext = NonNullable<typeof kcContext>;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "account.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,31 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "password.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `account/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;
export const WithNoMessage = () => (
<PageStory
kcContext={{
message: undefined
}}
/>
);

9
stories/global.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
declare module "*.png" {
const _default: string;
export default _default;
}
declare module "*.md" {
const _default: string;
export default _default;
}

View File

@ -0,0 +1,61 @@
import React from "react";
import { memo, useState } from "react";
import { useConstCallback } from "powerhooks";
import { keyframes } from "tss-react";
import keycloakifyLogoHeroMovingPngUrl from "./keycloakify-logo-hero-moving.png";
import keycloakifyLogoHeroStillPngUrl from "./keycloakify-logo-hero-still.png";
import { makeStyles } from "./tss";
export type Props = {
style?: React.CSSProperties;
id?: string;
onLoad?: () => void;
};
export const KeycloakifyRotatingLogo = memo((props: Props) => {
const { id, style, onLoad: onLoadProp } = props;
const [isImageLoaded, setIsImageLoaded] = useState(false);
const onLoad = useConstCallback(() => {
setIsImageLoaded(true);
onLoadProp?.();
});
const { classes } = useStyles({
isImageLoaded
});
return (
<div id={id} className={classes.root} style={style}>
<img className={classes.rotatingImg} onLoad={onLoad} src={keycloakifyLogoHeroMovingPngUrl} alt={"Rotating react logo"} />
<img className={classes.stillImg} src={keycloakifyLogoHeroStillPngUrl} alt={"keyhole"} />
</div>
);
});
const useStyles = makeStyles<{ isImageLoaded: boolean }>({
"name": { KeycloakifyRotatingLogo }
})((_theme, { isImageLoaded }) => ({
"root": {
"position": "relative"
},
"rotatingImg": {
"animation": `${keyframes({
"from": {
"transform": "rotate(0deg)"
},
"to": {
"transform": "rotate(360deg)"
}
})} infinite 20s linear`,
"width": isImageLoaded ? "100%" : undefined,
"height": isImageLoaded ? "auto" : undefined
},
"stillImg": {
"position": "absolute",
"top": "0",
"left": "0",
"width": isImageLoaded ? "100%" : undefined,
"height": isImageLoaded ? "auto" : undefined
}
}));

View File

@ -0,0 +1,35 @@
import { Meta } from "@storybook/addon-docs";
import { KeycloakifyRotatingLogo } from "./KeycloakifyRotatingLogo";
<Meta
title="Introduction"
parameters={{
"viewMode": "docs",
"previewTabs": {
"canvas": { "hidden": true },
"zoom": { "hidden": true },
"storybook/background": { "hidden": true },
"storybook/viewport": { "hidden": true },
},
}}
/>
<div style={{ "margin": "0 auto", "maxWidth": "700px", "textAlign": "center" }}>
<div style={{ "display": "flex", "justifyContent": "center" }}>
<KeycloakifyRotatingLogo style={{ "width": 400 }} />
</div>
<h1><a href="#">Keycloakify </a> Storybook</h1>
<p>
This website showcases all the Keycloak user-facing pages that can be customized using Keycloakify.
The storybook serves as a comprehensive reference to help you determine which pages you would like to personalize.
Keep in mind that customizing the <a href="https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/login/Template.tsx"><code>Template</code></a> component alone will already cover 90% of your customization needs.
</p>
<p>
If you discover that a page you wish to customize is not currently supported by Keycloakify, don't worry.
Simply refer to <a href="https://docs.keycloakify.dev/limitations#i-have-established-that-a-page-that-i-need-isnt-supported-out-of-the-box-by-keycloakify-now-what">this documentation page</a> for further assistance.
</p>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

5
stories/intro/tss.ts Normal file
View File

@ -0,0 +1,5 @@
import { createMakeAndWithStyles } from "tss-react";
export const { makeStyles, useStyles } = createMakeAndWithStyles({
"useTheme": () => ({})
});

50
stories/login/KcApp.tsx Normal file
View File

@ -0,0 +1,50 @@
import React, { lazy, Suspense } from "react";
import Fallback from "../../dist/login";
import type { KcContext } from "./kcContext";
import { useI18n } from "./i18n";
import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms";
import tos_en_url from "./tos_en.md";
import tos_fr_url from "./tos_fr.md";
const DefaultTemplate = lazy(() => import("../../dist/login/Template"));
export default function KcApp(props: { kcContext: KcContext }) {
const { kcContext } = props;
const i18n = useI18n({ kcContext });
useDownloadTerms({
"kcContext": kcContext as any,
"downloadTermMarkdown": async ({ currentLanguageTag }) => {
const resource = (() => {
switch (currentLanguageTag) {
case "fr":
return tos_fr_url;
default:
return tos_en_url;
}
})();
// webpack5 (used via storybook) loads markdown as string, not url
if (resource.includes("\n")) return resource;
const response = await fetch(resource);
return response.text();
}
});
if (i18n === null) {
return null;
}
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
default:
return <Fallback {...{ kcContext, i18n }} Template={DefaultTemplate} doUseDefaultCss={true} />;
}
})()}
</Suspense>
);
}

View File

@ -0,0 +1,19 @@
import React from "react";
import { getKcContext, type KcContext } from "./kcContext";
import KcApp from "./KcApp";
import type { DeepPartial } from "../../dist/tools/DeepPartial";
export function createPageStory<PageId extends KcContext["pageId"]>(params: { pageId: PageId }) {
const { pageId } = params;
function PageStory(params: { kcContext?: DeepPartial<Extract<KcContext, { pageId: PageId }>> }) {
const { kcContext } = getKcContext({
mockPageId: pageId,
storyPartialKcContext: params.kcContext
});
return <KcApp kcContext={kcContext} />;
}
return { PageStory };
}

5
stories/login/i18n.ts Normal file
View File

@ -0,0 +1,5 @@
import { createUseI18n } from "../../dist/login";
export const { useI18n } = createUseI18n({});
export type I18n = NonNullable<ReturnType<typeof useI18n>>;

View File

@ -0,0 +1,7 @@
import { createGetKcContext } from "../../dist/login";
export const { getKcContext } = createGetKcContext();
const { kcContext } = getKcContext();
export type KcContext = NonNullable<typeof kcContext>;

View File

@ -0,0 +1,32 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "error.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
hidden: true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;
export const WithAnotherMessage = () => (
<PageStory
kcContext={{
message: { summary: "With another error message" }
}}
/>
);

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "idp-review-user-profile.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "info.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,105 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;
export const WithoutPasswordField = () => (
<PageStory
kcContext={{
realm: { password: false }
}}
/>
);
export const WithoutRegistration = () => (
<PageStory
kcContext={{
realm: { registrationAllowed: false }
}}
/>
);
export const WithoutRememberMe = () => (
<PageStory
kcContext={{
realm: { rememberMe: false }
}}
/>
);
export const WithoutPasswordReset = () => (
<PageStory
kcContext={{
realm: { resetPasswordAllowed: false }
}}
/>
);
export const WithEmailAsUsername = () => (
<PageStory
kcContext={{
realm: { loginWithEmailAllowed: false }
}}
/>
);
export const WithPresetUsername = () => (
<PageStory
kcContext={{
login: { username: "max.mustermann@mail.com" }
}}
/>
);
export const WithImmutablePresetUsername = () => (
<PageStory
kcContext={{
login: { username: "max.mustermann@mail.com" },
usernameEditDisabled: true
}}
/>
);
export const WithSocialProviders = () => (
<PageStory
kcContext={{
social: {
displayInfo: true,
providers: [
{ loginUrl: "google", alias: "google", providerId: "google", displayName: "Google" },
{ loginUrl: "microsoft", alias: "microsoft", providerId: "microsoft", displayName: "Microsoft" },
{ loginUrl: "facebook", alias: "facebook", providerId: "facebook", displayName: "Facebook" },
{ loginUrl: "instagram", alias: "instagram", providerId: "instagram", displayName: "Instagram" },
{ loginUrl: "twitter", alias: "twitter", providerId: "twitter", displayName: "Twitter" },
{ loginUrl: "linkedin", alias: "linkedin", providerId: "linkedin", displayName: "LinkedIn" },
{ loginUrl: "stackoverflow", alias: "stackoverflow", providerId: "stackoverflow", displayName: "Stackoverflow" },
{ loginUrl: "github", alias: "github", providerId: "github", displayName: "Github" },
{ loginUrl: "gitlab", alias: "gitlab", providerId: "gitlab", displayName: "Gitlab" },
{ loginUrl: "bitbucket", alias: "bitbucket", providerId: "bitbucket", displayName: "Bitbucket" },
{ loginUrl: "paypal", alias: "paypal", providerId: "paypal", displayName: "PayPal" },
{ loginUrl: "openshift", alias: "openshift", providerId: "openshift", displayName: "OpenShift" }
]
}
}}
/>
);

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-config-totp.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-idp-link-confirm.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-idp-link-email.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-otp.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-page-expired.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-password.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-reset-password.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-update-password.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-update-profile.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-username.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "login-verify-email.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

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