Compare commits

..

96 Commits

Author SHA1 Message Date
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
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
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
102 changed files with 12374 additions and 710 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,12 +20,15 @@
<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>
@ -36,14 +39,82 @@
<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://www.cloud-iam.com/) provides the following services:
- Perfectly configured and optimized Keycloak IAM, ready in seconds.
- Custom theme building for your brand using Keycloakify.
<p align="center">
<a href="https://www.cloud-iam.com/">
<img src="https://user-images.githubusercontent.com/6702424/232165752-17134e68-4a55-4d6e-8672-e9132ecac5d5.svg" alt="Cloud IAM Logo" width="200"/>
</a>
<br/>
<i>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://www.cloud-iam.com/), 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.7",
"version": "7.9.4",
"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

@ -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

@ -26,6 +26,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 +40,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 +74,6 @@ export declare namespace KcContext {
export type Account = Common & {
pageId: "account.ftl";
url: {
referrerURI: string;
accountUrl: string;
};
realm: {

View File

@ -0,0 +1,104 @@
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";
import { id } from "tsafe/id";
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
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 (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
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";
@ -9,8 +9,8 @@ const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"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(

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: "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({
@ -26,7 +26,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
<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 +38,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
value={account.username ?? ""}
autoComplete="username"
readOnly
style={{ "display": "none;" }}
style={{ "display": "none" }}
/>
{password.passwordSet && (

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

@ -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
})
) {
@ -172,49 +172,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"];

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

@ -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

@ -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

@ -333,7 +333,6 @@ export declare namespace KcContext {
totpSecretEncoded: string;
qrUrl: string;
policy: {
supportedApplications: string[];
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number;
lookAheadWindow: number;
@ -347,6 +346,7 @@ export declare namespace KcContext {
initialCounter: number;
}
);
supportedApplications: string[];
totpSecretQrCode: string;
manualUrl: string;
totpSecret: string;

View File

@ -0,0 +1,162 @@
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";
import { loginThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
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 (id<readonly string[]>(loginThemePageIds).indexOf(realKcContext.pageId) < 0 && !("login" in realKcContext)) {
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,6 +1,6 @@
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";
@ -104,8 +104,8 @@ export const kcContextCommonMock: KcContext.Common = {
"keycloakifyVersion": "0.0.0",
"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"
},
@ -453,8 +453,8 @@ export const kcContextMocks: KcContext[] = [
manualUrl: "#",
totpSecret: "G4nsI8lQagRMUchH8jEG",
otpCredentials: [],
supportedApplications: ["FreeOTP", "Google Authenticator"],
policy: {
supportedApplications: ["FreeOTP", "Google Authenticator"],
algorithm: "HmacSHA1",
digits: 6,
lookAheadWindow: 1,

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

@ -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 />;

View File

@ -0,0 +1,24 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "logout-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 = "register.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 = "register-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 = "select-authenticator.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 = "terms.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 = "update-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 = "update-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 = "webauthn-authenticate.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 />;

180
stories/login/tos_en.md Normal file
View File

@ -0,0 +1,180 @@
# Terms of Service
## Presentation / Features
[EC: continuation of today's meeting: this would deserve to differentiate the SSP Cloud from the Onyxia SSP Cloud instance]
The SSP Cloud is a service (hereinafter referred to as "the service") implemented by the National Institute for Statistics and Economic Studies (hereinafter referred to as "Insee").
The SSP Cloud is an implementation of free software [Onyxia] (https://github.com/InseeFrLab/onyxia) created and maintained by the innovation and technical instruction division of INSEE (information system management / innovation unit and information system strategy). The SSP Cloud is hosted by INSEE.
[EC: I will remove the "on open data", since the SSP Cloud can accommodate secure data under the appropriate conditions]
The SSP Cloud is a platform offering a "datalab" intended for _data science_ experiments on open data in which users can orchestrate services dedicated to the practice of _data science_ (development environments, databases, etc.). This service offering thus aims to familiarize users with new collaborative working methods using _open source_ statistical languages (R, python, Julia, etc.), _cloud computing_ type technologies, as well as to allow processing experiments. innovative statistics. The services offered are standard.
The SSP Cloud is aimed at officials of the official statistical system as well as teachers and students of the Group of National Schools of Economics and Statistics, allowing inter-service collaboration and cooperation with their ecosystem. Access can thus be granted on request and after decision of the governance bodies of the Cloud SSP to external collaborators and involved in the realization of experimental projects of the official statistical system. Projects involving non-open data are also subject to the decision of the governing bodies.
The SSP Cloud allows:
- the orchestration of _data science_ trainings
- access to _data science_ services
- secure data storage
- management of secrets, such as encryption keys
- access to a code management service
- orchestration of data processing flows
A user account is also used to connect to the service platform of the Inter-ministerial Mutualization Free Software community (<https://groupes.mim-libre.fr/>).
## Legal Notice
Functional administration of the Cloud SSP: Insee
This site is published by the National Institute for Statistics and Economic Studies (Insee).
INSEE
88 avenue Verdier
CS 70058
92541 Montrouge cedex
Director of publication: Mr. Jean-Luc Tavernier
Administrator: Frédéric Comte
Maintenance of the _open source_ Onyxia project: Insee
Hosting: Insee - Innovation and technical instruction division
## Terms of use of the Service
The SSP Cloud datalab can be accessed from any browser connected to
Internet. The use of a computer is recommended. Use of the datalab services is free.
The user community is accessible on:
- Tchap, salon [SSP Cloud] (https://www.tchap.gouv.fr/#/room/#SSPCloudXDpAw6v:agent.finances.tchap.gouv.fr)
- Rocket Chat at MIM Libre, [SSP Cloud] lounge (https://chat.mim-libre.fr/channel/sspcloud)
## Limits of use of the Service
Public data and data can be processed on the datalab
usual (working data without particular sensitivity). In the absence of specific authorization for a given experimental project, cannot be
"protected" or "sensitive" data processed on the datalab, with or without a
confidentiality intended to restrict distribution to a specific domain
(statistical, commercial, industrial secrecy, etc.).
[EC: seems too "weak" to me, refer to the opinion of the UAJC on this point: if an agent puts sensitive data on the datalab, under his responsibility, what is the responsibility of his employer? from INSEE? can be added "after he has taken a legal opinion on the character 'protected' or 'sensitive' and that he informed his hierarchy ??]
The "protected" or "sensitive" nature of the information stored or processed on the datalab
is subject to the discretion of the user under his own
responsibility.
## Roles, commitments and associated responsibilities
The service is made available by INSEE without other express guarantees or
tacit than those provided herein. The service is based on benchmark open source technologies. However, it is not guaranteed that it
is free from anomalies or errors. The service is therefore made available ** without
guaranteed availability and performance **. As such, INSEE cannot
be held responsible for loss and / or damage of any kind
be, who couldbe caused as a result of a malfunction or
unavailability of the service. Such situations will not give right to any
financial compensation.
Each user has a personal storage space. By default, all the information deposited in a user's storage space is accessible only to him. Each user has the possibility of making public files stored in their personal storage space. Each user is responsible for making their files available to the public.
[EC: take the opinion of the UAJC, I do not know if it is the user specifically who is responsible for the processing or the institution on which he depends]
Each user is responsible for processing all the experimental work he performs on the SSP Cloud.
He must, if necessary, declare the personal processing carried out using the SSP Cloud to the data protection officer of his structure and inform the members thereof. [not sure that it is only the DPD of his structure who must be aware, also the DPD Insee?]
[EC: in the case of a project involving several institutions, users must have previously established a data sharing / provision agreement.]
## Creating an account on the SSP Cloud
Access to the SSP Cloud requires prior registration and authentication.
## Experimental projects on sensitive data
** TODO **
Role of the project security manager
Enrollment of sensitive projects
Creation of collaborative spaces for sensitive projects
Creation and life cycle of spaces
## Processing of personal data
Data processing is based on the performance of the mission of providing a platform dedicated to experimentation and learning about data science for the benefit of the official statistical system.
The Service only collects the data strictly necessary for its implementation.
artwork.
The processing of personal data within the meaning of Articles 9 and 10 of
general data protection regulation (racial or ethnic origin,
political opinions, religious or philosophical beliefs, belonging
union, criminal convictions ...) is banned on the SSP Cloud.
[EC: same remark as above -> have the opinion of the Legal Unit]
Personal data processed as part of an experiment carried out by a user, when there is any, is the responsibility of the entity
administrative office from which the user originated. The
arrangements for their treatment must be communicated by
the user to the data protection officer of his entity
administrative unit.
Regarding the scope of the SSP Cloud service, the purpose of processing
concerns the management of the platform's accounts
(creation / conservation / deletion), operation of the platform (monitoring,
usage statistics) as well as the management of the services offered by the platform. Below is the list of
transverse personal data whose processing is under the
responsibility of INSEE.
** Suite to be managed with the DC POD **
> RL: @Fred, I put it a bit at random, I let you complete / amend
### Profile data
their first name, last name and email address (required);
freely:
- photo (see gitlab)
- ...
### Trace data
They are collected each time a user connects and, for example,
the use of a technical identifier, to trace connection operations and
modification of the objects of the service database.
They are used for technical support purposes. They can also do
subject to periodic review by the directors for control purposes and usage statistics.
### Cookie data
These cookies are only intended to allow the service to function and
to facilitate its use by users according to the constraints of each typology.
- Session cookie: mandatory, it identifies the session of
the user. The cookie is destroyed at the end of the session.
- Reauthentication cookie: optional, it allows you to re-authenticate
the user logged in for the duration of the cookie (one year maximum)
## Modification and evolution of the Service
INSEE reserves the right to develop, modify or suspend,
without notice, the Service for maintenance reasons or for any other
reason deemed necessary. The information is then communicated to users via Tchap.
The terms of these conditions of use may be modified or
completed at any time, without notice, depending on changes
made to the Service, changes in legislation or for any other reason
deemed necessary. These modifications and updates are binding on the user who
should therefore refer regularly to this section to verifythe
general conditions in force (accessible from the home page).
## Contact
For technical problems and / or
functionalities encountered on the platform, it is recommended, first of all
time to solicit communities of peers in collaborative spaces
provided for this purpose on Tchap and Rocket Chat-MIM Libre.
CNIL right of access for: <innovation@insee.fr>

180
stories/login/tos_fr.md Normal file
View File

@ -0,0 +1,180 @@
# Conditions générales d'utilisation
## Présentation / Fonctionnalités
[EC: suite de la réunion d'aujourdhui : cela mériterait de différencier le SSP Cloud de l'instance d'Onyxia SSP Cloud]
Le SSP Cloud est un service (ci après désigné par "le service") mis en œuvre par l'Institut national de la statistique et des études économiques (ci-après dénommé "l'Insee").
Le SSP Cloud est une implémentation du logiciel libre [Onyxia](https://github.com/InseeFrLab/onyxia) créé et maintenu par la division innovation et instruction technique de l'Insee (direction du système d'information/unité innovation et stratégie du système d'information). Lhébergement du SSP Cloud est assuré par l'Insee.
[EC: j'enlèverai le "sur données ouvertes", puisque le SSP Cloud peut accueillir dans les donditions idoines des données sécurisées]
Le SSP Cloud est une plateforme proposant un "datalab" destiné aux expérimentations de _data science_ sur données ouvertes dans lequel les utilisateurs peuvent orchestrer des services dédiés à la pratique de la _data science_ (environnements de développement, bases de données...). Cette offre de services vise ainsi à familiariser les utilisateurs avec de nouvelles méthodes de travail collaboratif mobilisant des langages statistiques _open source_ (R, python, Julia...), des technologies de type _cloud computing_ ainsi qu'à permettre d'expérimenter des traitements statistiques innovants. Les services proposés sont standards.
Le SSP Cloud sadresse aux agents du système statistique public ainsi qu'aux enseignants et étudiants du Groupe des écoles nationales d'économie et de statistique, permettant une collaboration interservices et la coopération avec leur écosystème. Des accès peuvent ainsi être accordés sur demande et après décision des organes de gouvernance du SSP Cloud à des collaborateurs extérieurs et impliqués dans la réalisation de projets expérimentaux du système statistique public. Les projets mobilisant des données non ouvertes sont aussi soumis à la décision des organes de gouvernance.
Le SSP Cloud permet :
- l'orchestration de formations de _data science_
- l'accès à des services de _data science_
- le stockage sécurisé de données
- la gestion de secrets, tels que des clés de chiffrement
- l'accès à un service de gestion de code
- l'orchestration de flux de traitement de données
Un compte utilisateur permet également de se connecter à la plateforme de services de la communauté Mutualisation Inter-ministérielle Logiciels Libres (<https://groupes.mim-libre.fr/>).
## Mentions légales
Administration fonctionnelle du SSP Cloud : Insee
Ce site est édité par l'Institut national de la statistique et des études économiques (Insee).
Insee
88 avenue Verdier
CS 70058
92541 Montrouge cedex
Directeur de la publication : Monsieur Jean-Luc Tavernier
Administrateur : Frédéric Comte
Maintenance du projet _open source_ Onyxia : Insee
Hébergement : Insee - Division innovation et instruction technique
## Modalités dutilisation du Service
Le datalab SSP Cloud est accessible depuis nimporte quel navigateur connecté à
Internet. L'utilisation d'un ordinateur est recommandée. Lutilisation des services du datalab est gratuite.
La communauté d'utilisateurs est accessible sur :
- Tchap, salon [SSP Cloud](https://www.tchap.gouv.fr/#/room/#SSPCloudXDpAw6v:agent.finances.tchap.gouv.fr)
- Rocket Chat du MIM Libre, salon [SSP Cloud](https://chat.mim-libre.fr/channel/sspcloud)
## Limites dutilisation du Service
Peuvent être traitées sur le datalab les données publiques et données
usuelles (données de travail sans sensibilité particulière). En l'absence d'autorisation spécifique pour un projet d'expérimentation donné, ne peuvent être
traitées sur le datalab les données protégées ou sensibles, avec ou sans marque de
confidentialité destinée à restreindre la diffusion à un domaine spécifique
(secret statistique, commercial, industriel..).
[EC: me semble trop "faible", se référer à l'avis de l'UAJC sur ce point : si un agent met des données sensibles sur le datalab, sous sa responsabilité, quelle est la responsabilité de son employeur? de l'Insee ? peut être ajouter "après qu'il ait pris un avis juridique sur le caractère 'protégé' ou 'sensible' et qu'il en ait informé sa hiérarchie??]
Le caractère protégé ou sensible des informations stockées ou traitées sur le datalab
est soumis à lappréciation de lutilisateur sous sa propre
responsabilité.
## Les rôles, engagements et responsabilités associées
Le service est mis à disposition par l'Insee sans autres garanties expresses ou
tacites que celles qui sont prévues par les présentes. Le service sappuie sur des technologies open source de référence. Toutefois, il nest pas garanti quil
soit exempt danomalies ou erreurs. Le service est donc mis à disposition **sans
garantie sur sa disponibilité et ses performances**. A ce titre, l'Insee ne peut
être tenue responsable des pertes et/ou préjudices, de quelque nature quils
soient, qui pourraient être causés à la suite dun dysfonctionnement ou une
indisponibilité du service. De telles situations n'ouvriront droit à aucune
compensation financière.
Chaque utilisateur dispose d'un espace de stockage personnel. Par défaut, toutes les informations déposées dans un espace de stockage d'un utilisateur ne sont accessibles qu'à lui seul. Chaque utilisateur a la possibilité de rendre publics des fichiers stockés dans son espace de stockage personnel. Chaque utilisateur est responsable de la mise à disposition publique de ses fichiers.
[EC : prendre l'avis de l'UAJC, je ne sais pas si c'est l'utilisateur nommément qui est responsable du traitement ou bien l'institution dont il dépend]
Chaque utilisateur est responsable de traitement pour lensemble des travaux d'expérimentation qu'il réalise sur le SSP Cloud.
Il doit, le cas échant, déclarer les traitements à caractère personnel réalisés à l'aide du SSP Cloud au délégué à la protection des données de sa structure et en informer les membres. [pas sur que ce soit uniquement le DPD de sa structure qui doit être au courant, aussi le DPD Insee?]
[EC : dans le cas d'un projet faisant intervenir plusieurs institutions, les utilisateurs doivent avoir au préalable établi un conventionnement de partage/ mise à disposition des données.]
## La création de compte sur le SSP Cloud
L'accès au SSP Cloud nécessite une inscription préalable et une authentification.
## Les projets d'expérimentation sur données sensibles
**TODO**
Rôle du responsable de sécurité du projet
Enrôlement des projets sensibles
Création d'espaces collaboratifs pour les projets sensibles
Création et cycle de vie des espaces
## Traitement des données à caractère personnel
Le traitement des données se fonde sur lexécution de la mission que constitue la mise à disposition d'une plateforme dédiée à l'expérimentation et à l'apprentissage de la datascience au bénéfice du système statistique public.
Le Service ne collecte que les données strictement nécessaires à sa mise en
œuvre.
Le traitement de données à caractère personnel au sens des articles 9 et 10 du
règlement général sur la protection des données (origine raciale ou ethnique,
opinions politiques, convictions religieuses ou philosophiques, appartenance
syndicale, condamnations pénales...) est proscrit sur le SSP Cloud.
[EC: meme remarque que ci-dessus --> avoir l'avis de l'Unité juridique]
Les données à caractère personnel traitées dans le cadre d'une expérimentation réalisée par un utilisateur, quand il y en a, relèvent de la responsabilité de lentité
administrative dont est issu lutilisateur. Les
dispositions relatives à leur traitement doivent être communiquées par
l'utilisateur au délégué à la protection des données de son entité
administrative de rattachement.
Pour ce qui est du périmètre du service SSP Cloud, la finalité de traitement
concerne la gestion des comptes de la plateforme
(création/conservation/suppression), lexploitation de la plateforme (suivi,
statistiques dusages) ainsi que la gestion des services offerts par la plateforme. Ci-dessous la liste des
données à caractère personnel transverses dont le traitement est sous la
responsabilité de l'Insee.
**Suite à gérer avec le DC POD**
> RL : @Fred, je mets un peu au hasard, je te laisse compléter/amender
### Données relatives au profil
ses prénom, nom et adresse mail (obligatoire) ;
de façon libre :
- photo (cf. gitlab)
- ...
### Données de trace
Elles sont collectées à chaque connexion d'un utilisateur et permettent, par
lutilisation dun identifiant technique, de tracer les opérations de connexion et
de modification des objets de la base de données du service.
Elles servent à des fins de support technique. Elles peuvent également faire
l'objet d'une revue périodique de la part des administrateurs à des fins de contrôle et de statistiques d'usage.
### Les données de cookies
Ces cookies nont pour objet que de permettre le fonctionnement du service et
de faciliter son usage par les utilisateurs selon les contraintes chaque typologie.
- Cookie de session : obligatoire , il permet d'identifier la session de
l'utilisateur. Le cookie est détruit à la fin de la session.
- Cookie de réauthentification : optionnel, il permet de ré-authentifier
l'utilisateur connecté pendant la durée du cookie (un an maximum)
## Modification et évolution du Service
L'Insee se réserve la liberté de faire évoluer, de modifier ou de suspendre,
sans préavis, le Service pour des raisons de maintenance ou pour tout autre
motif jugé nécessaire. L'information est alors communiquée aux utilisateurs via Tchap.
Les termes des présentes conditions dutilisation peuvent être modifiés ou
complétés à tout moment, sans préavis, en fonction des modifications
apportées au Service, de lévolution de la législation ou pour tout autre motif
jugé nécessaire. Ces modifications et mises à jour simposent à lutilisateur qui
doit, en conséquence, se référer régulièrement à cette rubrique pour vérifier les
conditions générales en vigueur (accessible depuis la page daccueil).
## Contact
Pour les problèmes techniques et/ou
fonctionnels rencontrés sur la plateforme, il est conseillé, dans un premier
temps de solliciter les communautés de pairs dans les espaces collaboratifs
prévus à cet effet sur Tchap et Rocket Chat-MIM Libre.
Droit daccès CNIL pour : <innovation@insee.fr>

View File

@ -53,8 +53,7 @@ describe("Sample Project", () => {
const destDirPath = pathJoin(
readBuildOptions({
"isExternalAssetsCliParamProvided": false,
"isSilent": true,
"processArgv": ["--silent"],
"projectDirPath": process.cwd()
}).keycloakifyBuildDirPath,
"src",
@ -62,7 +61,7 @@ describe("Sample Project", () => {
"resources",
"theme"
);
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", isSilent: false });
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", "isSilent": false });
},
{ timeout: 90000 }
);
@ -80,8 +79,7 @@ describe("Sample Project", () => {
const destDirPath = pathJoin(
readBuildOptions({
"isExternalAssetsCliParamProvided": false,
"isSilent": true,
"processArgv": ["--silent"],
"projectDirPath": process.cwd()
}).keycloakifyBuildDirPath,
"src",
@ -89,7 +87,7 @@ describe("Sample Project", () => {
"resources",
"theme"
);
await downloadBuiltinKeycloakTheme({ destDirPath, keycloakVersion: "11.0.3", isSilent: false });
await downloadBuiltinKeycloakTheme({ destDirPath, "keycloakVersion": "11.0.3", "isSilent": false });
},
{ timeout: 90000 }
);

View File

@ -0,0 +1,273 @@
import { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
import type { ExtendKcContext } from "keycloakify/login/kcContext/getKcContextFromWindow";
import type { KcContext } from "keycloakify/login/kcContext";
import { same } from "evt/tools/inDepth";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { kcContextMocks, kcContextCommonMock } from "keycloakify/login/kcContext/kcContextMocks";
import { deepClone } from "keycloakify/tools/deepClone";
import { expect, it, describe } from "vitest";
describe("createGetKcContext", () => {
const authorizedMailDomains = ["example.com", "another-example.com", "*.yet-another-example.com", "*.example.com", "hello-world.com"];
const displayName = "this is an overwritten common value";
const aNonStandardValue1 = "a non standard value 1";
const aNonStandardValue2 = "a non standard value 2";
type KcContextExtension =
| {
pageId: "register.ftl";
authorizedMailDomains: string[];
}
| {
pageId: "info.ftl";
aNonStandardValue1: string;
}
| {
pageId: "my-extra-page-1.ftl";
}
| {
pageId: "my-extra-page-2.ftl";
aNonStandardValue2: string;
};
const getKcContextProxy = (params: { mockPageId: ExtendKcContext<KcContextExtension>["pageId"] }) => {
const { mockPageId } = params;
const { getKcContext } = createGetKcContext<KcContextExtension>({
"mockData": [
{
"pageId": "login.ftl",
"realm": { displayName }
},
{
"pageId": "info.ftl",
aNonStandardValue1
},
{
"pageId": "register.ftl",
authorizedMailDomains
},
{
"pageId": "my-extra-page-2.ftl",
aNonStandardValue2
}
]
});
const { kcContext } = getKcContext({
mockPageId
});
return { kcContext };
};
it("has proper API for login.ftl", () => {
const pageId = "login.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContext.Login>>();
expect(
same(
//NOTE: deepClone for printIfExists or other functions...
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
mock.realm.displayName = displayName;
return mock;
})()
)
).toBe(true);
});
it("has a proper API for info.ftl", () => {
const pageId = "info.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<
Equals<
typeof kcContext,
KcContext.Info & {
pageId: typeof pageId;
aNonStandardValue1: string;
}
>
>();
expect(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { aNonStandardValue1 });
return mock;
})()
)
).toBe(true);
});
it("has a proper API for register.ftl", () => {
const pageId = "register.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
//NOTE: I don't understand the need to add: pageId: typeof pageId; ...
assert<
Equals<
typeof kcContext,
KcContext.Register & {
pageId: typeof pageId;
authorizedMailDomains: string[];
}
>
>();
expect(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!);
Object.assign(mock, { authorizedMailDomains });
return mock;
})()
)
).toBe(true);
});
it("has a proper API for my-extra-page-2.ftl", () => {
const pageId = "my-extra-page-2.ftl";
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<
Equals<
typeof kcContext,
KcContext.Common & {
pageId: typeof pageId;
aNonStandardValue2: string;
}
>
>();
kcContext.aNonStandardValue2;
expect(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId, aNonStandardValue2 });
return mock;
})()
)
).toBe(true);
});
it("has a proper API for my-extra-page-1.ftl", () => {
const pageId = "my-extra-page-1.ftl";
console.log("We expect a warning here =>");
const { kcContext } = getKcContextProxy({ "mockPageId": pageId });
assert(kcContext?.pageId === pageId);
assert<Equals<typeof kcContext, KcContext.Common & { pageId: typeof pageId }>>();
expect(
same(
deepClone(kcContext),
(() => {
const mock = deepClone(kcContextCommonMock);
Object.assign(mock, { pageId });
return mock;
})()
)
).toBe(true);
});
it("returns the proper mock for login.ftl", () => {
const pageId = "login.ftl";
const { getKcContext } = createGetKcContext();
const { kcContext } = getKcContext({
"mockPageId": pageId
});
assert<Equals<typeof kcContext, KcContext.Login>>();
assert(same(deepClone(kcContext), deepClone(kcContextMocks.find(({ pageId: pageId_i }) => pageId_i === pageId)!)));
});
it("returns undefined when no mock is specified", () => {
const { getKcContext } = createGetKcContext();
const { kcContext } = getKcContext();
assert<Equals<typeof kcContext, KcContext | undefined>>();
assert(kcContext === undefined);
});
it("mock are properly overwritten", () => {
const { getKcContext } = createGetKcContext();
const displayName = "myDisplayName";
const { kcContext } = getKcContext({
"mockPageId": "login.ftl",
"storyPartialKcContext": {
"realm": {
displayName
}
}
});
assert<Equals<typeof kcContext, KcContext.Login>>();
assert(kcContext?.realm.displayName === displayName);
});
it("mockPageId doesn't have to be a singleton", () => {
const { getKcContext } = createGetKcContext();
const mockPageId: "login.ftl" | "register.ftl" = "login.ftl" as any;
const { kcContext } = getKcContext({
mockPageId
});
assert<Equals<typeof kcContext, KcContext.Login | KcContext.Register>>();
});
it("no undefined as long as we provide a mock pageId", () => {
const { getKcContext } = createGetKcContext();
const mockPageId: KcContext["pageId"] = "login.ftl" as any;
const { kcContext } = getKcContext({
mockPageId
});
assert<Equals<typeof kcContext, KcContext>>();
});
});

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