Compare commits
71 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
5580248bcd | |||
c9c10b8fba | |||
ed254922e9 | |||
4b7d1e2cec | |||
775ae57258 | |||
96e4cd79ee | |||
bb70f7df4f | |||
602de2e407 | |||
225ced989c | |||
ab53698f34 | |||
02f2124126 | |||
66623e3324 | |||
4cc886fd04 | |||
a10b490245 | |||
b947b8a00d | |||
60fa240a4d | |||
e05cd87b7c | |||
8e41c905ed | |||
e21f607ab0 | |||
34af5abb82 | |||
fc1cdb5dc9 | |||
069a0cc980 | |||
78363727e1 | |||
23b16746f6 | |||
6edf9c3d15 | |||
2e371d2078 | |||
b70b478e25 | |||
97ad132086 | |||
2c5c54bf46 | |||
c0ca078b43 | |||
53e94d04f6 | |||
dd198f9f06 | |||
43f455f4d0 | |||
d9132ea5a5 | |||
d5c7e2547b | |||
13b87de06c | |||
83bdbb7a7e | |||
89320b8d51 | |||
5fa9c3879c | |||
c0cd76d40e | |||
01f60f8013 | |||
91ad0712af | |||
2cb1b36725 | |||
67ce66765f | |||
c8cc453942 | |||
3f835f152f | |||
35e8a853e0 | |||
d084a4bf4a | |||
2a6b79e097 | |||
5d786c922f | |||
26bd5dd534 | |||
b4df0ce52c | |||
386a8d7cd7 | |||
5221fb3479 | |||
2871f63f25 | |||
4c282d0559 | |||
4ac14dc074 | |||
fcdbb04ea6 | |||
14f283cf49 | |||
efc459663a | |||
d459aaf943 | |||
921c7d5441 | |||
7d7e648968 | |||
96fc779ec8 | |||
9605e17e96 | |||
111c1675f9 | |||
d547ec3126 | |||
0ce6a7be7f | |||
1e5eae69e9 | |||
89d9208f44 | |||
1638577d98 |
@ -231,6 +231,15 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"code"
|
"code"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "madmadson",
|
||||||
|
"name": "Tobias Matt",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/798831?v=4",
|
||||||
|
"profile": "https://github.com/madmadson",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
storybook:
|
storybook:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
#if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
needs: test
|
needs: test
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
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
|
|
||||||
});
|
|
33
.storybook/customTheme.ts
Normal file
33
.storybook/customTheme.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
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 = {
|
||||||
|
base: "dark",
|
||||||
|
appBg: "#1E1E1E",
|
||||||
|
appContentBg: "#161616",
|
||||||
|
barBg: "#161616",
|
||||||
|
colorSecondary: "#8585F6",
|
||||||
|
textColor: "#FFFFFF",
|
||||||
|
brandImage,
|
||||||
|
brandTitle,
|
||||||
|
brandUrl,
|
||||||
|
fontBase,
|
||||||
|
fontCode
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lightTheme: typeof darkTheme = {
|
||||||
|
base: "light",
|
||||||
|
appBg: "#F6F6F6",
|
||||||
|
appContentBg: "#FFFFFF",
|
||||||
|
barBg: "#FFFFFF",
|
||||||
|
colorSecondary: "#000091",
|
||||||
|
textColor: "#212121",
|
||||||
|
brandImage,
|
||||||
|
brandTitle,
|
||||||
|
brandUrl,
|
||||||
|
fontBase,
|
||||||
|
fontCode
|
||||||
|
};
|
@ -1,15 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
"stories": [
|
stories: [
|
||||||
"../stories/**/*.stories.@(ts|tsx|mdx)"
|
"../stories/**/*.stories.tsx"
|
||||||
],
|
],
|
||||||
"addons": [
|
addons: [
|
||||||
"@storybook/addon-links",
|
|
||||||
"@storybook/addon-essentials",
|
|
||||||
"storybook-dark-mode",
|
"storybook-dark-mode",
|
||||||
"@storybook/addon-a11y"
|
"@storybook/addon-a11y"
|
||||||
],
|
],
|
||||||
"core": {
|
core: {
|
||||||
"builder": "webpack5"
|
builder: "webpack5"
|
||||||
},
|
},
|
||||||
"staticDirs": ["./static"]
|
staticDirs: ["./static"]
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { addons } from '@storybook/addons';
|
import { addons } from '@storybook/addons';
|
||||||
|
|
||||||
addons.setConfig({
|
addons.setConfig({
|
||||||
"selectedPanel": 'storybook/a11y/panel',
|
selectedPanel: 'storybook/a11y/panel',
|
||||||
"showPanel": false,
|
showPanel: false
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,9 @@
|
|||||||
|
<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">
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body.sb-show-main.sb-main-padded {
|
body.sb-show-main.sb-main-padded {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@ -1,116 +1,105 @@
|
|||||||
import { darkTheme, lightTheme } from "./customTheme";
|
import { darkTheme, lightTheme } from "./customTheme";
|
||||||
import { DocsContainer, CanvasContainer } from "./Containers";
|
import { create as createTheme } from "@storybook/theming";
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
"actions": { "argTypesRegex": "^on[A-Z].*" },
|
actions: { argTypesRegex: "^on[A-Z].*" },
|
||||||
"controls": {
|
controls: {
|
||||||
"matchers": {
|
matchers: {
|
||||||
"color": /(background|color)$/i,
|
color: /(background|color)$/i,
|
||||||
"date": /Date$/,
|
date: /Date$/,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"backgrounds": { "disable": true },
|
backgrounds: { disable: true },
|
||||||
"darkMode": {
|
darkMode: {
|
||||||
"light": lightTheme,
|
light: createTheme(lightTheme),
|
||||||
"dark": darkTheme,
|
dark: createTheme(darkTheme),
|
||||||
},
|
},
|
||||||
"docs": {
|
controls: {
|
||||||
"container": DocsContainer
|
disable: true,
|
||||||
},
|
},
|
||||||
"controls": {
|
actions: {
|
||||||
"disable": true,
|
disable: true
|
||||||
},
|
},
|
||||||
"actions": {
|
viewport: {
|
||||||
"disable": true
|
viewports: {
|
||||||
},
|
|
||||||
"viewport": {
|
|
||||||
"viewports": {
|
|
||||||
"1440p": {
|
"1440p": {
|
||||||
"name": "1440p",
|
name: "1440p",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "2560px",
|
width: "2560px",
|
||||||
"height": "1440px",
|
height: "1440px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"fullHD": {
|
fullHD: {
|
||||||
"name": "Full HD",
|
name: "Full HD",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "1920px",
|
width: "1920px",
|
||||||
"height": "1080px",
|
height: "1080px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"macBookProBig": {
|
macBookProBig: {
|
||||||
"name": "MacBook Pro Big",
|
name: "MacBook Pro Big",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "1024px",
|
width: "1024px",
|
||||||
"height": "640px",
|
height: "640px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"macBookProMedium": {
|
macBookProMedium: {
|
||||||
"name": "MacBook Pro Medium",
|
name: "MacBook Pro Medium",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "1440px",
|
width: "1440px",
|
||||||
"height": "900px",
|
height: "900px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"macBookProSmall": {
|
macBookProSmall: {
|
||||||
"name": "MacBook Pro Small",
|
name: "MacBook Pro Small",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "1680px",
|
width: "1680px",
|
||||||
"height": "1050px",
|
height: "1050px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"pcAgent": {
|
pcAgent: {
|
||||||
"name": "PC Agent",
|
name: "PC Agent",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "960px",
|
width: "960px",
|
||||||
"height": "540px",
|
height: "540px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"iphone12Pro": {
|
iphone12Pro: {
|
||||||
"name": "Iphone 12 pro",
|
name: "Iphone 12 pro",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "390px",
|
width: "390px",
|
||||||
"height": "844px",
|
height: "844px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"iphone5se": {
|
iphone5se: {
|
||||||
"name": "Iphone 5/SE",
|
name: "Iphone 5/SE",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "320px",
|
width: "320px",
|
||||||
"height": "568px",
|
height: "568px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"ipadPro": {
|
ipadPro: {
|
||||||
"name": "Ipad pro",
|
name: "Ipad pro",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "1240px",
|
width: "1240px",
|
||||||
"height": "1366px",
|
height: "1366px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"Galaxy s9+": {
|
"Galaxy s9+": {
|
||||||
"name": "Galaxy S9+",
|
name: "Galaxy S9+",
|
||||||
"styles": {
|
styles: {
|
||||||
"width": "320px",
|
width: "320px",
|
||||||
"height": "658px",
|
height: "658px",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"options": {
|
options: {
|
||||||
"storySort": (a, b) =>
|
storySort: (a, b) =>
|
||||||
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
|
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const decorators = [
|
|
||||||
(Story) => (
|
|
||||||
<CanvasContainer>
|
|
||||||
<Story />
|
|
||||||
</CanvasContainer>
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const { getHardCodedWeight } = (() => {
|
const { getHardCodedWeight } = (() => {
|
||||||
|
|
||||||
const orderedPagesPrefix = [
|
const orderedPagesPrefix = [
|
||||||
|
272
README.md
272
README.md
@ -2,7 +2,7 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
<i>🔏 Keycloak Theming for the Modern Web 🔏</i>
|
||||||
<br>
|
<br>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/garronej/keycloakify/actions">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
@ -38,22 +38,39 @@
|
|||||||
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
|
<i>This build tool generates a Keycloak theme <a href="https://www.keycloakify.dev">Learn more</a></i>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
|
<img width="400" src="https://github.com/user-attachments/assets/6bf3bef9-00b0-4460-97b9-0d2da8500798">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||||
|
|
||||||
> NOTE: Keycloakify 10 is still in realase-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
> NOTE: Keycloakify 10, while still being tagged as release candidate is the version you should use if you are starting today.
|
||||||
|
> Use `yarn add keycloakify@next` or pin [the latest version candidate](https://www.npmjs.com/package/keycloakify?activeTab=versions).
|
||||||
|
|
||||||
## Sponsor
|
## Sponsors
|
||||||
|
|
||||||
We are exclusively sponsored by [Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), a French company offering Keycloak as a service.
|
Friends for the project, we trust and recommend their services.
|
||||||
Their dedicated support helps us continue the development and maintenance of this project.
|
|
||||||
|
|
||||||
[Cloud IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github) provides the following services:
|
<br/>
|
||||||
|
|
||||||
- Simplify and secure your Keycloak Identity and Access Management. Keycloak as a Service.
|
<div align="center">
|
||||||
- Custom theme building for your brand using Keycloakify.
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.zone2.tech/services/keycloak-consulting">
|
||||||
|
<i><strong>Keycloak Consulting Services</strong> - Your partner in Keycloak deployment, configuration, and extension development for optimized identity management solutions.</i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
@ -68,13 +85,11 @@ Their dedicated support helps us continue the development and maintenance of thi
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>Checkout <a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github">Cloud-IAM</a> and use promo code <code>keycloakify5</code></i>
|
<a href="https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github"><strong>Managed Keycloak Provider</strong> - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. </a>
|
||||||
<br/>
|
<br/>
|
||||||
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
|
Use code <code>keycloakify5</code> at checkout for a 5% discount.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Thank you, [Cloud-IAM](https://cloud-iam.com/?mtm_campaign=keycloakify-deal&mtm_source=keycloakify-github), for your support!
|
|
||||||
|
|
||||||
## Contributors ✨
|
## Contributors ✨
|
||||||
|
|
||||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||||
@ -116,7 +131,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://m-siemens.de/"><img src="https://avatars.githubusercontent.com/u/1873922?v=4?s=100" width="100px;" alt="Markus Siemens"/><br /><sub><b>Markus Siemens</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=msiemens" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/law108000"><img src="https://avatars.githubusercontent.com/u/8112024?v=4?s=100" width="100px;" alt="Rlok"/><br /><sub><b>Rlok</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=law108000" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Moulyy"><img src="https://avatars.githubusercontent.com/u/115405804?v=4?s=100" width="100px;" alt="Moulyy"/><br /><sub><b>Moulyy</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Moulyy" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/giorgoslytos"><img src="https://avatars.githubusercontent.com/u/50946162?v=4?s=100" width="100px;" alt="giorgoslytos"/><br /><sub><b>giorgoslytos</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=giorgoslytos" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/madmadson"><img src="https://avatars.githubusercontent.com/u/798831?v=4?s=100" width="100px;" alt="Tobias Matt"/><br /><sub><b>Tobias Matt</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=madmadson" title="Code">💻</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@ -125,230 +140,3 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<!-- prettier-ignore-end -->
|
<!-- prettier-ignore-end -->
|
||||||
|
|
||||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||||
|
|
||||||
# Changelog highlights
|
|
||||||
|
|
||||||
## 9.5
|
|
||||||
|
|
||||||
- Post build hook: You can now apply custom transformation to your theme files. [Learn more](https://docs.keycloakify.dev/build-options#postbuild-hook).
|
|
||||||
- You can now specify your option in the Keycloakify's Vite plugin instead in the package.json. [See example](https://docs.keycloakify.dev/build-options#themename).
|
|
||||||
|
|
||||||
## 9.4
|
|
||||||
|
|
||||||
**Vite Support! 🎉**
|
|
||||||
|
|
||||||
- [The starter is now a Vite project](https://github.com/keycloakify/keycloakify-starter).
|
|
||||||
The Webpack based starter is accessible [here](https://github.com/keycloakify/keycloakify-starter-cra).
|
|
||||||
- CRA (Webpack) remains supported for the forseable future.
|
|
||||||
- If you have a CRA Keycloakify theme that you wish to migrate to Vite checkout [this migration guide](https://docs.keycloakify.dev/migration-guides/cra-greater-than-vite).
|
|
||||||
|
|
||||||
## 9.0
|
|
||||||
|
|
||||||
Bring back support for account themes in Keycloak v23 and up! [See issue](https://github.com/keycloakify/keycloakify/issues/389).
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
Very few. Check them out [here](https://docs.keycloakify.dev/migration-guides/v8-greater-than-v9).
|
|
||||||
|
|
||||||
## 8.0
|
|
||||||
|
|
||||||
- Much smaller .jar size. 70.2 MB -> 7.8 MB.
|
|
||||||
Keycloakify now detects which of the static resources from the default theme are actually used by your theme and only include those in the .jar.
|
|
||||||
- Build time: The first build is slowed but the subsequent build are faster. [Update your CI so that the cache is persisted across CI build](https://github.com/keycloakify/keycloakify-starter/commit/bc378d5afb67e796f520afbc348185f3e319d9d0).
|
|
||||||
|
|
||||||
### Breaking changes
|
|
||||||
|
|
||||||
There are very few breaking changes in this major version. [Check them out](https://docs.keycloakify.dev/migration-guides/v7-greater-than-v8).
|
|
||||||
|
|
||||||
## 7.15
|
|
||||||
|
|
||||||
- The i18n messages you defines in your theme are now also maid available to Keycloak.
|
|
||||||
In practice this mean that you can now customize the `kcContext.message.summary` that
|
|
||||||
display a general alert and the values returned by `kcContext.messagesPerField.get()` that
|
|
||||||
are used to display specific error on some field of the form.
|
|
||||||
[See video](https://youtu.be/D6tZcemReTI)
|
|
||||||
|
|
||||||
## 7.14
|
|
||||||
|
|
||||||
- Deprecate the `extraPages` build option. Keycloakify is now able to analyze your code to detect extra pages.
|
|
||||||
|
|
||||||
## 7.13
|
|
||||||
|
|
||||||
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
|
|
||||||
It's now mandatory to [adopt the new directory structure](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
|
|
||||||
|
|
||||||
## 7.12
|
|
||||||
|
|
||||||
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.
|
|
||||||
There is now an idiomatic way of achieving the same result. [Learn more](https://docs.keycloakify.dev/build-options#keycloakify.themeVariantNames).
|
|
||||||
|
|
||||||
## 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 🚀
|
|
||||||
- It's much easier to customize pages at the CSS level, you can now see in the browser dev tool the customizable classes.
|
|
||||||
- New interactive CLI tool `npx eject-keycloak-page`, that enables to select the page you want to customize at the component level.
|
|
||||||
- There is [a Storybook](https://storybook.keycloakify.dev)
|
|
||||||
- [Remember me is fixed](https://github.com/keycloakify/keycloakify/pull/272)
|
|
||||||
|
|
||||||
## 6.13
|
|
||||||
|
|
||||||
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
|
|
||||||
|
|
||||||
## 6.12
|
|
||||||
|
|
||||||
Massive improvement in the developer experience:
|
|
||||||
|
|
||||||
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
|
|
||||||
- A lot of comments have been added in the code of the starter to make it easier to get started.
|
|
||||||
- The doc has been updated: https://docs.keycloakify.dev
|
|
||||||
- A lot of improvements in the type system.
|
|
||||||
|
|
||||||
## 6.11.4
|
|
||||||
|
|
||||||
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/239).
|
|
||||||
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
|
|
||||||
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
|
|
||||||
|
|
||||||
## 6.10.0
|
|
||||||
|
|
||||||
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
|
|
||||||
|
|
||||||
## 6.8.4
|
|
||||||
|
|
||||||
- `@emotion/react` is no longer a peer dependency of Keycloakify.
|
|
||||||
|
|
||||||
## 6.8.0
|
|
||||||
|
|
||||||
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
|
|
||||||
individual page (`<Login />`, `<RegisterUserProfile />`, ...) it enables to customize only the header and footer for
|
|
||||||
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/keycloakify/issues/191).
|
|
||||||
|
|
||||||
## 6.7.0
|
|
||||||
|
|
||||||
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/185).
|
|
||||||
|
|
||||||
## 6.6.0
|
|
||||||
|
|
||||||
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/184).
|
|
||||||
|
|
||||||
## 6.5.0
|
|
||||||
|
|
||||||
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/183).
|
|
||||||
|
|
||||||
## 6.4.0
|
|
||||||
|
|
||||||
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
|
|
||||||
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
|
|
||||||
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
|
|
||||||
|
|
||||||
## 6.0.0
|
|
||||||
|
|
||||||
- Bundle size drastically reduced, locals and component dynamically loaded.
|
|
||||||
- First print much quicker, use of React.lazy() everywhere.
|
|
||||||
- Real i18n API.
|
|
||||||
- Actual documentation for build options.
|
|
||||||
|
|
||||||
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
|
|
||||||
|
|
||||||
## 5.8.0
|
|
||||||
|
|
||||||
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
|
|
||||||
|
|
||||||
## 5.7.0
|
|
||||||
|
|
||||||
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
|
|
||||||
|
|
||||||
## 5.6.4
|
|
||||||
|
|
||||||
Fix `login-verify-email.ftl` page. [Before](https://user-images.githubusercontent.com/6702424/177436014-0bad22c4-5bfb-45bb-8fc9-dad65143cd0c.png) - [After](https://user-images.githubusercontent.com/6702424/177435797-ec5d7db3-84cf-49cb-8efc-3427a81f744e.png)
|
|
||||||
|
|
||||||
## 5.6.0
|
|
||||||
|
|
||||||
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
|
|
||||||
|
|
||||||
## 5.3.0
|
|
||||||
|
|
||||||
Rename `keycloak_theme_email` to `keycloak_email`.
|
|
||||||
If you already had a `keycloak_theme_email` you should rename it `keycloak_email`.
|
|
||||||
|
|
||||||
## 5.0.0
|
|
||||||
|
|
||||||
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
|
|
||||||
New i18n system.
|
|
||||||
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
|
|
||||||
|
|
||||||
## 4.10.0
|
|
||||||
|
|
||||||
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
|
|
||||||
|
|
||||||
## 4.8.0
|
|
||||||
|
|
||||||
[Email template customization.](#email-template-customization)
|
|
||||||
|
|
||||||
## 4.7.4
|
|
||||||
|
|
||||||
**M1 Mac** support (for testing locally with a dockerized Keycloak).
|
|
||||||
|
|
||||||
## 4.7.2
|
|
||||||
|
|
||||||
> WARNING: This is broken.
|
|
||||||
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
|
|
||||||
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
|
||||||
|
|
||||||
## 4.7.0
|
|
||||||
|
|
||||||
Register with user profile enabled: Out of the box `options` validator support.
|
|
||||||
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
|
|
||||||
|
|
||||||
## 4.6.0
|
|
||||||
|
|
||||||
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
|
|
||||||
After updating Keycloakify you can remove `tss-react` and `powerhooks` from your dependencies if you don't use them explicitly.
|
|
||||||
|
|
||||||
## 4.5.3
|
|
||||||
|
|
||||||
There is a new recommended way to setup highly customized theme. See [here](https://github.com/garronej/keycloakify-demo-app/blob/look_and_feel/src/KcApp/KcApp.tsx).
|
|
||||||
Unlike with [the previous recommended method](https://github.com/garronej/keycloakify-demo-app/blob/a51660578bea15fb3e506b8a2b78e1056c6d68bb/src/KcApp/KcApp.tsx),
|
|
||||||
with this new method your theme wont break on minor Keycloakify update.
|
|
||||||
|
|
||||||
## 4.3.0
|
|
||||||
|
|
||||||
Feature [`login-update-password.ftl`](https://user-images.githubusercontent.com/6702424/147517600-6191cf72-93dd-437b-a35c-47180142063e.png).
|
|
||||||
Every time a page is added it's a breaking change for non CSS-only theme.
|
|
||||||
Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L17) and [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77ce3c53ac7df0622d94d04e76d3f9f/src/KcApp/KcApp.tsx#L37) to update.
|
|
||||||
|
|
||||||
## 4
|
|
||||||
|
|
||||||
- Out of the box [frontend form validation](#user-profile-and-frontend-form-validation) 🥳
|
|
||||||
- Improvements (and breaking changes in `import { useKcMessage } from "keycloakify"`.
|
|
||||||
|
|
||||||
## 3
|
|
||||||
|
|
||||||
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
|
||||||
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
|
||||||
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
|
|
||||||
|
|
||||||
## 2.5
|
|
||||||
|
|
||||||
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
|
||||||
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
|
||||||
- Test container now uses Keycloak version `15.0.2`.
|
|
||||||
|
|
||||||
## 2
|
|
||||||
|
|
||||||
- It's now possible to implement custom `.ftl` pages.
|
|
||||||
- Support for Keycloak plugins that introduce non standard ftl values.
|
|
||||||
(Like for example [this plugin](https://github.com/micedre/keycloak-mail-whitelisting) that define `authorizedMailDomains` in `register.ftl`).
|
|
||||||
|
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "10.0.0-rc.119",
|
"version": "10.0.0-rc.142",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -72,14 +72,10 @@
|
|||||||
"@emotion/react": "^11.11.4",
|
"@emotion/react": "^11.11.4",
|
||||||
"@octokit/rest": "^20.1.1",
|
"@octokit/rest": "^20.1.1",
|
||||||
"@storybook/addon-a11y": "^6.5.16",
|
"@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/builder-webpack5": "^6.5.13",
|
||||||
"@storybook/manager-webpack5": "^6.5.13",
|
"@storybook/manager-webpack5": "^6.5.13",
|
||||||
"@storybook/react": "^6.5.13",
|
"@storybook/react": "^6.5.13",
|
||||||
"@storybook/testing-library": "^0.0.13",
|
"eslint-plugin-storybook": "^0.6.7",
|
||||||
"@types/babel__generator": "^7.6.4",
|
"@types/babel__generator": "^7.6.4",
|
||||||
"@types/make-fetch-happen": "^10.0.1",
|
"@types/make-fetch-happen": "^10.0.1",
|
||||||
"@types/minimist": "^1.2.2",
|
"@types/minimist": "^1.2.2",
|
||||||
@ -89,10 +85,9 @@
|
|||||||
"@types/yauzl": "^2.10.3",
|
"@types/yauzl": "^2.10.3",
|
||||||
"@vercel/ncc": "^0.38.1",
|
"@vercel/ncc": "^0.38.1",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"cheerio": "^1.0.0-rc.12",
|
"cheerio": "1.0.0-rc.12",
|
||||||
"chokidar-cli": "^3.0.0",
|
"chokidar-cli": "^3.0.0",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
"eslint-plugin-storybook": "^0.6.7",
|
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"magic-string": "^0.30.7",
|
"magic-string": "^0.30.7",
|
||||||
@ -108,7 +103,7 @@
|
|||||||
"termost": "^v0.12.1",
|
"termost": "^v0.12.1",
|
||||||
"tsc-alias": "^1.8.10",
|
"tsc-alias": "^1.8.10",
|
||||||
"tss-react": "^4.9.10",
|
"tss-react": "^4.9.10",
|
||||||
"typescript": "^4.9.1-beta",
|
"typescript": "^4.9.4",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0",
|
||||||
"yauzl": "^2.10.0",
|
"yauzl": "^2.10.0",
|
||||||
|
@ -1,16 +1,13 @@
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { join } from "path";
|
import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
|
||||||
|
|
||||||
run("yarn build");
|
(async () => {
|
||||||
|
run("yarn build");
|
||||||
|
|
||||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
await copyKeycloakResourcesToStorybookStaticDir();
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
run("npx build-storybook");
|
run("npx build-storybook");
|
||||||
|
})();
|
||||||
|
|
||||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||||
console.log(`$ ${command}`);
|
console.log(`$ ${command}`);
|
||||||
|
18
scripts/copyKeycloakResourcesToStorybookStaticDir.ts
Normal file
18
scripts/copyKeycloakResourcesToStorybookStaticDir.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
||||||
|
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||||
|
import { LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
||||||
|
|
||||||
|
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
||||||
|
await copyKeycloakResourcesToPublic({
|
||||||
|
buildContext: {
|
||||||
|
cacheDirPath: pathJoin(__dirname, "..", "node_modules", ".cache", "scripts"),
|
||||||
|
fetchOptions: getProxyFetchOptions({
|
||||||
|
npmConfigGetCwd: pathJoin(__dirname, "..")
|
||||||
|
}),
|
||||||
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
|
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||||
|
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -2,8 +2,33 @@ import * as child_process from "child_process";
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||||
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
|
|
||||||
|
{
|
||||||
|
const dirPath = "node_modules";
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
||||||
|
} catch {
|
||||||
|
// NOTE: This is a workaround for windows
|
||||||
|
// we can't remove locked executables.
|
||||||
|
|
||||||
|
crawl({
|
||||||
|
dirPath,
|
||||||
|
returnedPathsType: "absolute"
|
||||||
|
}).forEach(filePath => {
|
||||||
|
try {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
} catch (error) {
|
||||||
|
if (filePath.endsWith(".exe")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fs.rmSync("node_modules", { recursive: true, force: true });
|
|
||||||
fs.rmSync("dist", { recursive: true, force: true });
|
fs.rmSync("dist", { recursive: true, force: true });
|
||||||
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||||
|
|
||||||
|
@ -1,30 +1,26 @@
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import * as fs from "fs";
|
|
||||||
import { join } from "path";
|
|
||||||
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
|
||||||
|
import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir";
|
||||||
|
|
||||||
run("yarn build");
|
(async () => {
|
||||||
|
run("yarn build");
|
||||||
|
|
||||||
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
|
await copyKeycloakResourcesToStorybookStaticDir();
|
||||||
env: {
|
|
||||||
...process.env,
|
{
|
||||||
PUBLIC_DIR_PATH: join(".storybook", "static")
|
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
|
||||||
|
shell: true
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stdout.on("data", data => process.stdout.write(data));
|
||||||
|
|
||||||
|
child.stderr.on("data", data => process.stderr.write(data));
|
||||||
|
|
||||||
|
child.on("exit", process.exit.bind(process));
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
{
|
startRebuildOnSrcChange();
|
||||||
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], {
|
})();
|
||||||
shell: true
|
|
||||||
});
|
|
||||||
|
|
||||||
child.stdout.on("data", data => process.stdout.write(data));
|
|
||||||
|
|
||||||
child.stderr.on("data", data => process.stderr.write(data));
|
|
||||||
|
|
||||||
child.on("exit", process.exit.bind(process));
|
|
||||||
}
|
|
||||||
|
|
||||||
startRebuildOnSrcChange();
|
|
||||||
|
|
||||||
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
|
||||||
console.log(`$ ${command}`);
|
console.log(`$ ${command}`);
|
||||||
|
@ -8,7 +8,7 @@ export default function FederatedIdentity(props: PageProps<Extract<KcContext, {
|
|||||||
const { url, federatedIdentity, stateChecker } = kcContext;
|
const { url, federatedIdentity, stateChecker } = kcContext;
|
||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
return (
|
return (
|
||||||
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="federatedIdentity">
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="social">
|
||||||
<div className="main-layout social">
|
<div className="main-layout social">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-10">
|
<div className="col-md-10">
|
||||||
|
@ -26,11 +26,43 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
console.log(chalk.cyan("Theme type:"));
|
console.log(chalk.cyan("Theme type:"));
|
||||||
|
|
||||||
const { value: themeType } = await cliSelect<ThemeType>({
|
const themeType = await (async () => {
|
||||||
values: [...THEME_TYPES]
|
const values = THEME_TYPES.filter(themeType => {
|
||||||
}).catch(() => {
|
switch (themeType) {
|
||||||
process.exit(-1);
|
case "account":
|
||||||
});
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
|
case "login":
|
||||||
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(values.length > 0, "No theme is implemented in this project");
|
||||||
|
|
||||||
|
if (values.length === 1) {
|
||||||
|
return values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value } = await cliSelect<ThemeType>({
|
||||||
|
values
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (
|
||||||
|
themeType === "account" &&
|
||||||
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
`${chalk.red("✗")} Sorry, there is no Storybook support for Single-Page Account themes.`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
|
|
||||||
|
@ -12,7 +12,12 @@ import {
|
|||||||
} from "./shared/constants";
|
} from "./shared/constants";
|
||||||
import { capitalize } from "tsafe/capitalize";
|
import { capitalize } from "tsafe/capitalize";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
import {
|
||||||
|
join as pathJoin,
|
||||||
|
relative as pathRelative,
|
||||||
|
dirname as pathDirname,
|
||||||
|
basename as pathBasename
|
||||||
|
} from "path";
|
||||||
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
import { assert, Equals } from "tsafe/assert";
|
import { assert, Equals } from "tsafe/assert";
|
||||||
import type { CliCommandOptions } from "./main";
|
import type { CliCommandOptions } from "./main";
|
||||||
@ -28,11 +33,114 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
console.log(chalk.cyan("Theme type:"));
|
console.log(chalk.cyan("Theme type:"));
|
||||||
|
|
||||||
const { value: themeType } = await cliSelect<ThemeType>({
|
const themeType = await (async () => {
|
||||||
values: [...THEME_TYPES]
|
const values = THEME_TYPES.filter(themeType => {
|
||||||
}).catch(() => {
|
switch (themeType) {
|
||||||
process.exit(-1);
|
case "account":
|
||||||
});
|
return buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
|
case "login":
|
||||||
|
return buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert(values.length > 0, "No theme is implemented in this project");
|
||||||
|
|
||||||
|
if (values.length === 1) {
|
||||||
|
return values[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { value } = await cliSelect<ThemeType>({
|
||||||
|
values
|
||||||
|
}).catch(() => {
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (
|
||||||
|
themeType === "account" &&
|
||||||
|
(assert(buildContext.implementedThemeTypes.account.isImplemented),
|
||||||
|
buildContext.implementedThemeTypes.account.type === "Single-Page")
|
||||||
|
) {
|
||||||
|
const srcDirPath = pathJoin(
|
||||||
|
pathDirname(buildContext.packageJsonFilePath),
|
||||||
|
"node_modules",
|
||||||
|
"@keycloakify",
|
||||||
|
"keycloak-account-ui",
|
||||||
|
"src"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`There isn't an interactive CLI to eject components of the Single-Page Account theme.`,
|
||||||
|
`You can however copy paste into your codebase the any file or directory from the following source directory:`,
|
||||||
|
``,
|
||||||
|
`${chalk.bold(pathJoin(pathRelative(process.cwd(), srcDirPath)))}`,
|
||||||
|
``
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
eject_entrypoint: {
|
||||||
|
const kcAccountUiTsxFileRelativePath = "KcAccountUi.tsx";
|
||||||
|
|
||||||
|
const accountThemeSrcDirPath = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"account"
|
||||||
|
);
|
||||||
|
|
||||||
|
const targetFilePath = pathJoin(
|
||||||
|
accountThemeSrcDirPath,
|
||||||
|
kcAccountUiTsxFileRelativePath
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fs.existsSync(targetFilePath)) {
|
||||||
|
break eject_entrypoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.cpSync(
|
||||||
|
pathJoin(srcDirPath, kcAccountUiTsxFileRelativePath),
|
||||||
|
targetFilePath
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
const kcPageTsxFilePath = pathJoin(accountThemeSrcDirPath, "KcPage.tsx");
|
||||||
|
|
||||||
|
const kcPageTsxCode = fs.readFileSync(kcPageTsxFilePath).toString("utf8");
|
||||||
|
|
||||||
|
const componentName = pathBasename(
|
||||||
|
kcAccountUiTsxFileRelativePath
|
||||||
|
).replace(/.tsx$/, "");
|
||||||
|
|
||||||
|
const modifiedKcPageTsxCode = kcPageTsxCode.replace(
|
||||||
|
`@keycloakify/keycloak-account-ui/${componentName}`,
|
||||||
|
`./${componentName}`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
kcPageTsxFilePath,
|
||||||
|
Buffer.from(modifiedKcPageTsxCode, "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const routesTsxFilePath = pathRelative(
|
||||||
|
process.cwd(),
|
||||||
|
pathJoin(srcDirPath, "routes.tsx")
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`To help you get started ${chalk.bold(pathRelative(process.cwd(), targetFilePath))} has been copied into your project.`,
|
||||||
|
`The next step is usually to eject ${chalk.bold(routesTsxFilePath)}`,
|
||||||
|
`with \`cp ${routesTsxFilePath} ${pathRelative(process.cwd(), accountThemeSrcDirPath)}\``,
|
||||||
|
`then update the import of routes in ${kcAccountUiTsxFileRelativePath}.`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.log(`→ ${themeType}`);
|
console.log(`→ ${themeType}`);
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import chalk from "chalk";
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
|
||||||
|
import { generateKcGenTs } from "../shared/generateKcGenTs";
|
||||||
|
|
||||||
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
export async function command(params: { cliCommandOptions: CliCommandOptions }) {
|
||||||
const { cliCommandOptions } = params;
|
const { cliCommandOptions } = params;
|
||||||
@ -14,7 +15,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
|
||||||
|
|
||||||
if (fs.existsSync(accountThemeSrcDirPath)) {
|
if (
|
||||||
|
fs.existsSync(accountThemeSrcDirPath) &&
|
||||||
|
fs.readdirSync(accountThemeSrcDirPath).length > 0
|
||||||
|
) {
|
||||||
console.warn(
|
console.warn(
|
||||||
chalk.red(
|
chalk.red(
|
||||||
`There is already a ${pathRelative(
|
`There is already a ${pathRelative(
|
||||||
@ -92,4 +96,17 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
|
||||||
|
|
||||||
|
await generateKcGenTs({
|
||||||
|
buildContext: {
|
||||||
|
...buildContext,
|
||||||
|
implementedThemeTypes: {
|
||||||
|
...buildContext.implementedThemeTypes,
|
||||||
|
account: {
|
||||||
|
isImplemented: true,
|
||||||
|
type: accountThemeType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,10 @@ import { join as pathJoin, relative as pathRelative, dirname as pathDirname } fr
|
|||||||
import type { BuildContext } from "../shared/buildContext";
|
import type { BuildContext } from "../shared/buildContext";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { getLatestsSemVersionedTag } from "../shared/getLatestsSemVersionedTag";
|
import {
|
||||||
|
getLatestsSemVersionedTag,
|
||||||
|
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||||
|
} from "../shared/getLatestsSemVersionedTag";
|
||||||
import fetch from "make-fetch-happen";
|
import fetch from "make-fetch-happen";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { assert, type Equals } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
@ -12,8 +15,7 @@ import { npmInstall } from "../tools/npmInstall";
|
|||||||
import { copyBoilerplate } from "./copyBoilerplate";
|
import { copyBoilerplate } from "./copyBoilerplate";
|
||||||
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
|
||||||
|
|
||||||
type BuildContextLike = {
|
type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {
|
||||||
cacheDirPath: string;
|
|
||||||
fetchOptions: BuildContext["fetchOptions"];
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
packageJsonFilePath: string;
|
packageJsonFilePath: string;
|
||||||
};
|
};
|
||||||
@ -30,11 +32,11 @@ export async function initializeAccountTheme_singlePage(params: {
|
|||||||
const REPO = "keycloak-account-ui";
|
const REPO = "keycloak-account-ui";
|
||||||
|
|
||||||
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
const [semVersionedTag] = await getLatestsSemVersionedTag({
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
|
||||||
owner: OWNER,
|
owner: OWNER,
|
||||||
repo: REPO,
|
repo: REPO,
|
||||||
count: 1,
|
count: 1,
|
||||||
doIgnoreReleaseCandidates: false
|
doIgnoreReleaseCandidates: false,
|
||||||
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const dependencies = await fetch(
|
const dependencies = await fetch(
|
||||||
|
@ -30,7 +30,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
// NOTE: This is arbitrary
|
// NOTE: This is arbitrary
|
||||||
startingFromMajor: 17,
|
startingFromMajor: 17,
|
||||||
excludeMajorVersions: [],
|
excludeMajorVersions: [],
|
||||||
cacheDirPath: buildContext.cacheDirPath
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import {
|
import {
|
||||||
replaceImportsInJsCode,
|
replaceImportsInJsCode,
|
||||||
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
||||||
@ -77,7 +77,8 @@ export function generateFtlFilesCodeFactory(params: {
|
|||||||
(
|
(
|
||||||
[
|
[
|
||||||
["link", "href"],
|
["link", "href"],
|
||||||
["script", "src"]
|
["script", "src"],
|
||||||
|
["script", "data-src"]
|
||||||
] as const
|
] as const
|
||||||
).forEach(([selector, attrName]) =>
|
).forEach(([selector, attrName]) =>
|
||||||
$(selector).each((...[, element]) => {
|
$(selector).each((...[, element]) => {
|
||||||
|
@ -208,6 +208,18 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
) || (
|
) || (
|
||||||
key == "attributes" &&
|
key == "attributes" &&
|
||||||
areSamePath(path, ["realm"])
|
areSamePath(path, ["realm"])
|
||||||
|
) || (
|
||||||
|
xKeycloakify.pageId == "index.ftl" &&
|
||||||
|
xKeycloakify.themeType == "account" &&
|
||||||
|
areSamePath(path, ["realm"]) &&
|
||||||
|
![
|
||||||
|
"name",
|
||||||
|
"registrationEmailAsUsername",
|
||||||
|
"editUsernameAllowed",
|
||||||
|
"isInternationalizationEnabled",
|
||||||
|
"identityFederationEnabled",
|
||||||
|
"userManagedAccessAllowed"
|
||||||
|
]?seq_contains(key)
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
|
||||||
|
@ -314,7 +314,7 @@ export async function generateResourcesForMainTheme(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
|
urlOrPath: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
fetchOptions: buildContext.fetchOptions,
|
fetchOptions: buildContext.fetchOptions,
|
||||||
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",
|
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",
|
||||||
|
@ -78,7 +78,7 @@ program
|
|||||||
|
|
||||||
program
|
program
|
||||||
.command<{
|
.command<{
|
||||||
port: number;
|
port: number | undefined;
|
||||||
keycloakVersion: string | undefined;
|
keycloakVersion: string | undefined;
|
||||||
realmJsonFilePath: string | undefined;
|
realmJsonFilePath: string | undefined;
|
||||||
}>({
|
}>({
|
||||||
@ -96,7 +96,7 @@ program
|
|||||||
return name;
|
return name;
|
||||||
})(),
|
})(),
|
||||||
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
|
||||||
defaultValue: 8080
|
defaultValue: undefined
|
||||||
})
|
})
|
||||||
.option({
|
.option({
|
||||||
key: "keycloakVersion",
|
key: "keycloakVersion",
|
||||||
|
@ -14,7 +14,8 @@ import { assert, type Equals } from "tsafe/assert";
|
|||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import {
|
import {
|
||||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||||
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME
|
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
|
||||||
|
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||||
import { exclude } from "tsafe";
|
import { exclude } from "tsafe";
|
||||||
@ -60,6 +61,19 @@ export type BuildContext = {
|
|||||||
keycloakVersionRange: KeycloakVersionRange;
|
keycloakVersionRange: KeycloakVersionRange;
|
||||||
jarFileBasename: string;
|
jarFileBasename: string;
|
||||||
}[];
|
}[];
|
||||||
|
startKeycloakOptions: {
|
||||||
|
dockerImage:
|
||||||
|
| {
|
||||||
|
reference: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
| undefined;
|
||||||
|
dockerExtraArgs: string[];
|
||||||
|
keycloakExtraArgs: string[];
|
||||||
|
extensionJars: ({ type: "path"; path: string } | { type: "url"; url: string })[];
|
||||||
|
realmJsonFilePath: string | undefined;
|
||||||
|
port: number | undefined;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
|
assert<Equals<keyof BuildContext["implementedThemeTypes"], ThemeType | "email">>();
|
||||||
@ -74,6 +88,14 @@ export type BuildOptions = {
|
|||||||
loginThemeResourcesFromKeycloakVersion?: string;
|
loginThemeResourcesFromKeycloakVersion?: string;
|
||||||
keycloakifyBuildDirPath?: string;
|
keycloakifyBuildDirPath?: string;
|
||||||
kcContextExclusionsFtl?: string;
|
kcContextExclusionsFtl?: string;
|
||||||
|
startKeycloakOptions?: {
|
||||||
|
dockerImage?: string;
|
||||||
|
dockerExtraArgs?: string[];
|
||||||
|
keycloakExtraArgs?: string[];
|
||||||
|
extensionJars?: string[];
|
||||||
|
realmJsonFilePath?: string;
|
||||||
|
port?: number;
|
||||||
|
};
|
||||||
} & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
|
} & BuildOptions.AccountThemeImplAndKeycloakVersionTargets;
|
||||||
|
|
||||||
export namespace BuildOptions {
|
export namespace BuildOptions {
|
||||||
@ -300,6 +322,23 @@ export function getBuildContext(params: {
|
|||||||
return id<z.ZodType<TargetType>>(zTargetType);
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
const zStartKeycloakOptions = (() => {
|
||||||
|
type TargetType = NonNullable<BuildOptions["startKeycloakOptions"]>;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
dockerImage: z.string().optional(),
|
||||||
|
extensionJars: z.array(z.string()).optional(),
|
||||||
|
realmJsonFilePath: z.string().optional(),
|
||||||
|
dockerExtraArgs: z.array(z.string()).optional(),
|
||||||
|
keycloakExtraArgs: z.array(z.string()).optional(),
|
||||||
|
port: z.number().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
const zBuildOptions = (() => {
|
const zBuildOptions = (() => {
|
||||||
type TargetType = BuildOptions;
|
type TargetType = BuildOptions;
|
||||||
|
|
||||||
@ -320,7 +359,8 @@ export function getBuildContext(params: {
|
|||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
loginThemeResourcesFromKeycloakVersion: z.string().optional(),
|
||||||
keycloakifyBuildDirPath: z.string().optional(),
|
keycloakifyBuildDirPath: z.string().optional(),
|
||||||
kcContextExclusionsFtl: z.string().optional()
|
kcContextExclusionsFtl: z.string().optional(),
|
||||||
|
startKeycloakOptions: zStartKeycloakOptions.optional()
|
||||||
}),
|
}),
|
||||||
zAccountThemeImplAndKeycloakVersionTargets
|
zAccountThemeImplAndKeycloakVersionTargets
|
||||||
);
|
);
|
||||||
@ -506,7 +546,8 @@ export function getBuildContext(params: {
|
|||||||
buildOptions.artifactId ??
|
buildOptions.artifactId ??
|
||||||
`${themeNames[0]}-keycloak-theme`,
|
`${themeNames[0]}-keycloak-theme`,
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
buildOptions.loginThemeResourcesFromKeycloakVersion ?? "24.0.4",
|
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
||||||
|
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||||
projectDirPath,
|
projectDirPath,
|
||||||
projectBuildDirPath,
|
projectBuildDirPath,
|
||||||
keycloakifyBuildDirPath: (() => {
|
keycloakifyBuildDirPath: (() => {
|
||||||
@ -554,27 +595,23 @@ export function getBuildContext(params: {
|
|||||||
|
|
||||||
return pathJoin(projectDirPath, resolvedViteConfig.publicDir);
|
return pathJoin(projectDirPath, resolvedViteConfig.publicDir);
|
||||||
})(),
|
})(),
|
||||||
cacheDirPath: (() => {
|
cacheDirPath: pathJoin(
|
||||||
const cacheDirPath = pathJoin(
|
(() => {
|
||||||
(() => {
|
if (process.env.XDG_CACHE_HOME !== undefined) {
|
||||||
if (process.env.XDG_CACHE_HOME !== undefined) {
|
return getAbsoluteAndInOsFormatPath({
|
||||||
return getAbsoluteAndInOsFormatPath({
|
pathIsh: process.env.XDG_CACHE_HOME,
|
||||||
pathIsh: process.env.XDG_CACHE_HOME,
|
cwd: process.cwd()
|
||||||
cwd: process.cwd()
|
});
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return pathJoin(
|
return pathJoin(
|
||||||
pathDirname(packageJsonFilePath),
|
pathDirname(packageJsonFilePath),
|
||||||
"node_modules",
|
"node_modules",
|
||||||
".cache"
|
".cache"
|
||||||
);
|
);
|
||||||
})(),
|
})(),
|
||||||
"keycloakify"
|
"keycloakify"
|
||||||
);
|
),
|
||||||
|
|
||||||
return cacheDirPath;
|
|
||||||
})(),
|
|
||||||
urlPathname: (() => {
|
urlPathname: (() => {
|
||||||
webpack: {
|
webpack: {
|
||||||
if (bundler !== "webpack") {
|
if (bundler !== "webpack") {
|
||||||
@ -893,6 +930,48 @@ export function getBuildContext(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return jarTargets;
|
return jarTargets;
|
||||||
})()
|
})(),
|
||||||
|
startKeycloakOptions: {
|
||||||
|
dockerImage: (() => {
|
||||||
|
if (buildOptions.startKeycloakOptions?.dockerImage === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [reference, tag, ...rest] =
|
||||||
|
buildOptions.startKeycloakOptions.dockerImage.split(":");
|
||||||
|
|
||||||
|
assert(
|
||||||
|
reference !== undefined && tag !== undefined && rest.length === 0,
|
||||||
|
`Invalid docker image: ${buildOptions.startKeycloakOptions.dockerImage}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { reference, tag };
|
||||||
|
})(),
|
||||||
|
dockerExtraArgs: buildOptions.startKeycloakOptions?.dockerExtraArgs ?? [],
|
||||||
|
keycloakExtraArgs: buildOptions.startKeycloakOptions?.keycloakExtraArgs ?? [],
|
||||||
|
extensionJars: (buildOptions.startKeycloakOptions?.extensionJars ?? []).map(
|
||||||
|
urlOrPath => {
|
||||||
|
if (/^https?:\/\//.test(urlOrPath)) {
|
||||||
|
return { type: "url", url: urlOrPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "path",
|
||||||
|
path: getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: urlOrPath,
|
||||||
|
cwd: projectDirPath
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
realmJsonFilePath:
|
||||||
|
buildOptions.startKeycloakOptions?.realmJsonFilePath === undefined
|
||||||
|
? undefined
|
||||||
|
: getAbsoluteAndInOsFormatPath({
|
||||||
|
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
|
||||||
|
cwd: projectDirPath
|
||||||
|
}),
|
||||||
|
port: buildOptions.startKeycloakOptions?.port
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -69,3 +69,5 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
|
|||||||
export const CONTAINER_NAME = "keycloak-keycloakify";
|
export const CONTAINER_NAME = "keycloak-keycloakify";
|
||||||
|
|
||||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||||
|
|
||||||
|
export const LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
||||||
|
@ -21,7 +21,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
urlOrPath: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
fetchOptions: buildContext.fetchOptions,
|
fetchOptions: buildContext.fetchOptions,
|
||||||
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",
|
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
import type { BuildContext } from "./buildContext";
|
import type { BuildContext } from "./buildContext";
|
||||||
import * as fs from "fs/promises";
|
import * as fs from "fs/promises";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
projectDirPath: string;
|
projectDirPath: string;
|
||||||
themeNames: string[];
|
themeNames: string[];
|
||||||
environmentVariables: { name: string; default: string }[];
|
environmentVariables: { name: string; default: string }[];
|
||||||
themeSrcDirPath: string;
|
themeSrcDirPath: string;
|
||||||
|
implementedThemeTypes: Pick<
|
||||||
|
BuildContext["implementedThemeTypes"],
|
||||||
|
"login" | "account"
|
||||||
|
>;
|
||||||
|
packageJsonFilePath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
@ -18,12 +25,53 @@ export async function generateKcGenTs(params: {
|
|||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const filePath = pathJoin(buildContext.themeSrcDirPath, "kc.gen.ts");
|
const isReactProject: boolean = await (async () => {
|
||||||
|
const parsedPackageJson = await (async () => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
dependencies?: Record<string, string>;
|
||||||
|
devDependencies?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
dependencies: z.record(z.string()).optional(),
|
||||||
|
devDependencies: z.record(z.string()).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>();
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return zParsedPackageJson.parse(
|
||||||
|
JSON.parse(
|
||||||
|
(await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
...parsedPackageJson.dependencies,
|
||||||
|
...parsedPackageJson.devDependencies
|
||||||
|
}.react !== undefined
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const filePath = pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
`kc.gen.ts${isReactProject ? "x" : ""}`
|
||||||
|
);
|
||||||
|
|
||||||
const currentContent = (await existsAsync(filePath))
|
const currentContent = (await existsAsync(filePath))
|
||||||
? await fs.readFile(filePath)
|
? await fs.readFile(filePath)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
|
||||||
|
const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
|
||||||
|
|
||||||
const newContent = Buffer.from(
|
const newContent = Buffer.from(
|
||||||
[
|
[
|
||||||
`/* prettier-ignore-start */`,
|
`/* prettier-ignore-start */`,
|
||||||
@ -36,6 +84,8 @@ export async function generateKcGenTs(params: {
|
|||||||
``,
|
``,
|
||||||
`// This file is auto-generated by Keycloakify`,
|
`// This file is auto-generated by Keycloakify`,
|
||||||
``,
|
``,
|
||||||
|
isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
|
||||||
|
``,
|
||||||
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
|
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
|
||||||
``,
|
``,
|
||||||
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
|
`export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
|
||||||
@ -54,9 +104,52 @@ export async function generateKcGenTs(params: {
|
|||||||
2
|
2
|
||||||
)};`,
|
)};`,
|
||||||
``,
|
``,
|
||||||
|
`export type KcContext =`,
|
||||||
|
hasLoginTheme && ` | import("./login/KcContext").KcContext`,
|
||||||
|
hasAccountTheme && ` | import("./account/KcContext").KcContext`,
|
||||||
|
` ;`,
|
||||||
|
``,
|
||||||
|
`declare global {`,
|
||||||
|
` interface Window {`,
|
||||||
|
` kcContext?: KcContext;`,
|
||||||
|
` }`,
|
||||||
|
`}`,
|
||||||
|
``,
|
||||||
|
...(!isReactProject
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
hasLoginTheme &&
|
||||||
|
`export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
|
||||||
|
hasAccountTheme &&
|
||||||
|
`export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
|
||||||
|
``,
|
||||||
|
`export function KcPage(`,
|
||||||
|
` props: {`,
|
||||||
|
` kcContext: KcContext;`,
|
||||||
|
` fallback?: ReactNode;`,
|
||||||
|
` }`,
|
||||||
|
`) {`,
|
||||||
|
` const { kcContext, fallback } = props;`,
|
||||||
|
` return (`,
|
||||||
|
` <Suspense fallback={fallback}>`,
|
||||||
|
` {(() => {`,
|
||||||
|
` switch (kcContext.themeType) {`,
|
||||||
|
hasLoginTheme &&
|
||||||
|
` case "login": return <KcLoginPage kcContext={kcContext} />;`,
|
||||||
|
hasAccountTheme &&
|
||||||
|
` case "account": return <KcAccountPage kcContext={kcContext} />;`,
|
||||||
|
` }`,
|
||||||
|
` })()}`,
|
||||||
|
` </Suspense>`,
|
||||||
|
` );`,
|
||||||
|
`}`
|
||||||
|
]),
|
||||||
|
``,
|
||||||
`/* prettier-ignore-end */`,
|
`/* prettier-ignore-end */`,
|
||||||
``
|
``
|
||||||
].join("\n"),
|
]
|
||||||
|
.filter(item => typeof item === "string")
|
||||||
|
.join("\n"),
|
||||||
"utf8"
|
"utf8"
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -65,4 +158,18 @@ export async function generateKcGenTs(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(filePath, newContent);
|
await fs.writeFile(filePath, newContent);
|
||||||
|
|
||||||
|
delete_legacy_file: {
|
||||||
|
if (!isReactProject) {
|
||||||
|
break delete_legacy_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyFilePath = filePath.replace(/tsx$/, "ts");
|
||||||
|
|
||||||
|
if (!(await existsAsync(legacyFilePath))) {
|
||||||
|
break delete_legacy_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.unlink(legacyFilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import { assert, type Equals } from "tsafe/assert";
|
|||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
import type { SemVer } from "../tools/SemVer";
|
import type { SemVer } from "../tools/SemVer";
|
||||||
import { same } from "evt/tools/inDepth/same";
|
import { same } from "evt/tools/inDepth/same";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
|
||||||
type GetLatestsSemVersionedTag = ReturnType<
|
type GetLatestsSemVersionedTag = ReturnType<
|
||||||
typeof getLatestsSemVersionedTagFactory
|
typeof getLatestsSemVersionedTagFactory
|
||||||
@ -31,11 +33,23 @@ type Cache = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BuildContextLike = {
|
||||||
|
cacheDirPath: string;
|
||||||
|
fetchOptions: BuildContext["fetchOptions"];
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function getLatestsSemVersionedTag({
|
export async function getLatestsSemVersionedTag({
|
||||||
cacheDirPath,
|
buildContext,
|
||||||
...params
|
...params
|
||||||
}: Params & { cacheDirPath: string }): Promise<R> {
|
}: Params & {
|
||||||
const cacheFilePath = pathJoin(cacheDirPath, "latest-sem-versioned-tags.json");
|
buildContext: BuildContextLike;
|
||||||
|
}): Promise<R> {
|
||||||
|
const cacheFilePath = pathJoin(
|
||||||
|
buildContext.cacheDirPath,
|
||||||
|
"latest-sem-versioned-tags.json"
|
||||||
|
);
|
||||||
|
|
||||||
const cacheLookupResult = (() => {
|
const cacheLookupResult = (() => {
|
||||||
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
|
const getResult_currentCache = (currentCacheEntries: Cache["entries"]) => ({
|
||||||
@ -144,9 +158,16 @@ export async function getLatestsSemVersionedTag({
|
|||||||
const octokit = (() => {
|
const octokit = (() => {
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
const octokit = new Octokit(
|
const octokit = new Octokit({
|
||||||
githubToken === undefined ? undefined : { auth: githubToken }
|
...(githubToken === undefined ? {} : { auth: githubToken }),
|
||||||
);
|
request: {
|
||||||
|
fetch: (url: string, options?: any) =>
|
||||||
|
fetch(url, {
|
||||||
|
...options,
|
||||||
|
...buildContext.fetchOptions
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return octokit;
|
return octokit;
|
||||||
})();
|
})();
|
||||||
|
@ -1,22 +1,31 @@
|
|||||||
import { getLatestsSemVersionedTag } from "./getLatestsSemVersionedTag";
|
import {
|
||||||
|
getLatestsSemVersionedTag,
|
||||||
|
type BuildContextLike as BuildContextLike_getLatestsSemVersionedTag
|
||||||
|
} from "./getLatestsSemVersionedTag";
|
||||||
import cliSelect from "cli-select";
|
import cliSelect from "cli-select";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
|
import type { BuildContext } from "./buildContext";
|
||||||
|
|
||||||
|
export type BuildContextLike = BuildContextLike_getLatestsSemVersionedTag & {};
|
||||||
|
|
||||||
|
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||||
|
|
||||||
export async function promptKeycloakVersion(params: {
|
export async function promptKeycloakVersion(params: {
|
||||||
startingFromMajor: number | undefined;
|
startingFromMajor: number | undefined;
|
||||||
excludeMajorVersions: number[];
|
excludeMajorVersions: number[];
|
||||||
cacheDirPath: string;
|
buildContext: BuildContextLike;
|
||||||
}) {
|
}) {
|
||||||
const { startingFromMajor, excludeMajorVersions, cacheDirPath } = params;
|
const { startingFromMajor, excludeMajorVersions, buildContext } = params;
|
||||||
|
|
||||||
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
const semVersionedTagByMajor = new Map<number, { tag: string; version: SemVer }>();
|
||||||
|
|
||||||
const semVersionedTags = await getLatestsSemVersionedTag({
|
const semVersionedTags = await getLatestsSemVersionedTag({
|
||||||
cacheDirPath,
|
|
||||||
count: 50,
|
count: 50,
|
||||||
owner: "keycloak",
|
owner: "keycloak",
|
||||||
repo: "keycloak",
|
repo: "keycloak",
|
||||||
doIgnoreReleaseCandidates: true
|
doIgnoreReleaseCandidates: true,
|
||||||
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
semVersionedTags.forEach(semVersionedTag => {
|
semVersionedTags.forEach(semVersionedTag => {
|
||||||
|
@ -40,7 +40,7 @@ async function appBuild_vite(params: {
|
|||||||
|
|
||||||
const dIsSuccess = new Deferred<boolean>();
|
const dIsSuccess = new Deferred<boolean>();
|
||||||
|
|
||||||
console.log(chalk.blue("Running: 'npx vite build'"));
|
console.log(chalk.blue("$ npx vite build"));
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["vite", "build"], {
|
const child = child_process.spawn("npx", ["vite", "build"], {
|
||||||
cwd: buildContext.projectDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
@ -145,7 +145,7 @@ async function appBuild_webpack(params: {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(chalk.blue(`Running: '${subCommand}'`));
|
console.log(chalk.blue(`$ ${subCommand}`));
|
||||||
|
|
||||||
const child = child_process.spawn(command, args, {
|
const child = child_process.spawn(command, args, {
|
||||||
cwd: commandCwd,
|
cwd: commandCwd,
|
||||||
|
@ -20,7 +20,7 @@ export async function keycloakifyBuild(params: {
|
|||||||
|
|
||||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||||
|
|
||||||
console.log(chalk.blue("Running: 'npx keycloakify build'"));
|
console.log(chalk.blue("$ npx keycloakify build"));
|
||||||
|
|
||||||
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
||||||
cwd: buildContext.projectDirPath,
|
cwd: buildContext.projectDirPath,
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["view-profile", "manage-account"]
|
"account": ["delete-account", "view-profile", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -611,8 +611,8 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2099,7 +2099,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["view-profile", "manage-account"]
|
"account": ["delete-account", "view-profile", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -618,8 +618,8 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2130,7 +2130,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["view-profile", "manage-account"]
|
"account": ["delete-account", "view-profile", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -628,8 +628,8 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2140,7 +2140,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -73,7 +73,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["view-profile", "manage-account"]
|
"account": ["delete-account", "view-profile", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -632,8 +632,8 @@
|
|||||||
"name": "",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2144,7 +2144,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["view-profile", "manage-account"]
|
"account": ["delete-account", "view-profile", "manage-account"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -644,7 +644,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "https://my-theme.keycloakify.dev",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2088,7 +2088,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["manage-account", "view-profile"]
|
"account": ["delete-account", "manage-account", "view-profile"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -449,7 +449,7 @@
|
|||||||
"gender": ["prefer_not_to_say"],
|
"gender": ["prefer_not_to_say"],
|
||||||
"bio": ["Hello I'm Test User and I do not exist."],
|
"bio": ["Hello I'm Test User and I do not exist."],
|
||||||
"phone_number": ["1111111111"],
|
"phone_number": ["1111111111"],
|
||||||
"locale": ["fr"],
|
"locale": ["en"],
|
||||||
"favorite_media": ["movies", "series"]
|
"favorite_media": ["movies", "series"]
|
||||||
},
|
},
|
||||||
"createdTimestamp": 1716183898408,
|
"createdTimestamp": 1716183898408,
|
||||||
@ -653,7 +653,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "https://my-theme.keycloakify.dev",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2235,7 +2235,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
"composites": {
|
"composites": {
|
||||||
"realm": ["offline_access", "uma_authorization"],
|
"realm": ["offline_access", "uma_authorization"],
|
||||||
"client": {
|
"client": {
|
||||||
"account": ["manage-account", "view-profile"]
|
"account": ["delete-account", "manage-account", "view-profile"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"clientRole": false,
|
"clientRole": false,
|
||||||
@ -543,7 +543,7 @@
|
|||||||
"favourite_pet": ["cat"],
|
"favourite_pet": ["cat"],
|
||||||
"bio": ["Hello I'm Test User and I do not exist."],
|
"bio": ["Hello I'm Test User and I do not exist."],
|
||||||
"phone_number": ["1111111111"],
|
"phone_number": ["1111111111"],
|
||||||
"locale": ["fr"],
|
"locale": ["en"],
|
||||||
"favorite_media": ["movies", "series"]
|
"favorite_media": ["movies", "series"]
|
||||||
},
|
},
|
||||||
"createdTimestamp": 1716183898408,
|
"createdTimestamp": 1716183898408,
|
||||||
@ -767,7 +767,7 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"rootUrl": "https://my-theme.keycloakify.dev",
|
"rootUrl": "https://my-theme.keycloakify.dev",
|
||||||
"adminUrl": "https://my-theme.keycloakify.dev",
|
"adminUrl": "https://my-theme.keycloakify.dev",
|
||||||
"baseUrl": "",
|
"baseUrl": "https://my-theme.keycloakify.dev",
|
||||||
"surrogateAuthRequired": false,
|
"surrogateAuthRequired": false,
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"alwaysDisplayInConsole": false,
|
"alwaysDisplayInConsole": false,
|
||||||
@ -2315,7 +2315,7 @@
|
|||||||
"alias": "delete_account",
|
"alias": "delete_account",
|
||||||
"name": "Delete Account",
|
"name": "Delete Account",
|
||||||
"providerId": "delete_account",
|
"providerId": "delete_account",
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"defaultAction": false,
|
"defaultAction": false,
|
||||||
"priority": 60,
|
"priority": 60,
|
||||||
"config": {}
|
"config": {}
|
||||||
|
@ -4,7 +4,7 @@ import type { CliCommandOptions as CliCommandOptions_common } from "../main";
|
|||||||
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
|
||||||
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
|
import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants";
|
||||||
import { SemVer } from "../tools/SemVer";
|
import { SemVer } from "../tools/SemVer";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import {
|
import {
|
||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
@ -26,9 +26,10 @@ import { keycloakifyBuild } from "./keycloakifyBuild";
|
|||||||
import { isInside } from "../tools/isInside";
|
import { isInside } from "../tools/isInside";
|
||||||
import { existsAsync } from "../tools/fs.existsAsync";
|
import { existsAsync } from "../tools/fs.existsAsync";
|
||||||
import { rm } from "../tools/fs.rm";
|
import { rm } from "../tools/fs.rm";
|
||||||
|
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||||
|
|
||||||
export type CliCommandOptions = CliCommandOptions_common & {
|
export type CliCommandOptions = CliCommandOptions_common & {
|
||||||
port: number;
|
port: number | undefined;
|
||||||
keycloakVersion: string | undefined;
|
keycloakVersion: string | undefined;
|
||||||
realmJsonFilePath: string | undefined;
|
realmJsonFilePath: string | undefined;
|
||||||
};
|
};
|
||||||
@ -88,30 +89,65 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
const buildContext = getBuildContext({ cliCommandOptions });
|
const buildContext = getBuildContext({ cliCommandOptions });
|
||||||
|
|
||||||
const { keycloakVersion } = await (async () => {
|
const { dockerImageTag } = await (async () => {
|
||||||
if (cliCommandOptions.keycloakVersion !== undefined) {
|
if (cliCommandOptions.keycloakVersion !== undefined) {
|
||||||
|
return { dockerImageTag: cliCommandOptions.keycloakVersion };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.dockerImage !== undefined) {
|
||||||
return {
|
return {
|
||||||
keycloakVersion: cliCommandOptions.keycloakVersion,
|
dockerImageTag: buildContext.startKeycloakOptions.dockerImage.tag
|
||||||
keycloakMajorNumber: SemVer.parse(cliCommandOptions.keycloakVersion).major
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
chalk.cyan("On which version of Keycloak do you want to test your theme?")
|
[
|
||||||
|
chalk.cyan(
|
||||||
|
"On which version of Keycloak do you want to test your theme?"
|
||||||
|
),
|
||||||
|
chalk.gray(
|
||||||
|
"You can also explicitly provide the version with `npx keycloakify start-keycloak --keycloak-version 25.0.2` (or any other version)"
|
||||||
|
)
|
||||||
|
].join("\n")
|
||||||
);
|
);
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion({
|
const { keycloakVersion } = await promptKeycloakVersion({
|
||||||
startingFromMajor: 18,
|
startingFromMajor: 18,
|
||||||
excludeMajorVersions: [22],
|
excludeMajorVersions: [22],
|
||||||
cacheDirPath: buildContext.cacheDirPath
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`→ ${keycloakVersion}`);
|
console.log(`→ ${keycloakVersion}`);
|
||||||
|
|
||||||
return { keycloakVersion };
|
return { dockerImageTag: keycloakVersion };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const keycloakMajorVersionNumber = SemVer.parse(keycloakVersion).major;
|
const keycloakMajorVersionNumber = (() => {
|
||||||
|
if (buildContext.startKeycloakOptions.dockerImage === undefined) {
|
||||||
|
return SemVer.parse(dockerImageTag).major;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tag } = buildContext.startKeycloakOptions.dockerImage;
|
||||||
|
|
||||||
|
const [wrap] = [18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
|
||||||
|
.map(majorVersionNumber => ({
|
||||||
|
majorVersionNumber,
|
||||||
|
index: tag.indexOf(`${majorVersionNumber}`)
|
||||||
|
}))
|
||||||
|
.filter(({ index }) => index !== -1)
|
||||||
|
.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
if (wrap === undefined) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
`Could not determine the major Keycloak version number from the docker image tag ${tag}. Assuming 25`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return 25;
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrap.majorVersionNumber;
|
||||||
|
})();
|
||||||
|
|
||||||
{
|
{
|
||||||
const { isAppBuildSuccess } = await appBuild({
|
const { isAppBuildSuccess } = await appBuild({
|
||||||
@ -150,26 +186,50 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
|
|
||||||
assert(jarFilePath !== undefined);
|
assert(jarFilePath !== undefined);
|
||||||
|
|
||||||
console.log(`Using ${chalk.bold(pathBasename(jarFilePath))}`);
|
const extensionJarFilePaths = await Promise.all(
|
||||||
|
buildContext.startKeycloakOptions.extensionJars.map(async extensionJar => {
|
||||||
|
switch (extensionJar.type) {
|
||||||
|
case "path": {
|
||||||
|
assert(
|
||||||
|
await existsAsync(extensionJar.path),
|
||||||
|
`${extensionJar.path} does not exist`
|
||||||
|
);
|
||||||
|
return extensionJar.path;
|
||||||
|
}
|
||||||
|
case "url": {
|
||||||
|
const { archiveFilePath } = await downloadAndExtractArchive({
|
||||||
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
|
fetchOptions: buildContext.fetchOptions,
|
||||||
|
urlOrPath: extensionJar.url,
|
||||||
|
uniqueIdOfOnArchiveFile: "no extraction",
|
||||||
|
onArchiveFile: async () => {}
|
||||||
|
});
|
||||||
|
return archiveFilePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert<Equals<typeof extensionJar, never>>(false);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const realmJsonFilePath = await (async () => {
|
const realmJsonFilePath = await (async () => {
|
||||||
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
if (cliCommandOptions.realmJsonFilePath !== undefined) {
|
||||||
if (cliCommandOptions.realmJsonFilePath === "none") {
|
if (cliCommandOptions.realmJsonFilePath === "none") {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
|
||||||
chalk.green(
|
|
||||||
`Using realm json file: ${cliCommandOptions.realmJsonFilePath}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return getAbsoluteAndInOsFormatPath({
|
return getAbsoluteAndInOsFormatPath({
|
||||||
pathIsh: cliCommandOptions.realmJsonFilePath,
|
pathIsh: cliCommandOptions.realmJsonFilePath,
|
||||||
cwd: process.cwd()
|
cwd: process.cwd()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (buildContext.startKeycloakOptions.realmJsonFilePath !== undefined) {
|
||||||
|
assert(
|
||||||
|
await existsAsync(buildContext.startKeycloakOptions.realmJsonFilePath),
|
||||||
|
`${pathRelative(process.cwd(), buildContext.startKeycloakOptions.realmJsonFilePath)} does not exist`
|
||||||
|
);
|
||||||
|
return buildContext.startKeycloakOptions.realmJsonFilePath;
|
||||||
|
}
|
||||||
|
|
||||||
const internalFilePath = await (async () => {
|
const internalFilePath = await (async () => {
|
||||||
const dirPath = pathJoin(
|
const dirPath = pathJoin(
|
||||||
getThisCodebaseRootDirPath(),
|
getThisCodebaseRootDirPath(),
|
||||||
@ -274,77 +334,93 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const spawnArgs = [
|
const DEFAULT_PORT = 8080;
|
||||||
"docker",
|
const port =
|
||||||
[
|
cliCommandOptions.port ?? buildContext.startKeycloakOptions.port ?? DEFAULT_PORT;
|
||||||
"run",
|
|
||||||
...["-p", `${cliCommandOptions.port}:8080`],
|
|
||||||
...["--name", CONTAINER_NAME],
|
|
||||||
...["-e", "KEYCLOAK_ADMIN=admin"],
|
|
||||||
...["-e", "KEYCLOAK_ADMIN_PASSWORD=admin"],
|
|
||||||
...(realmJsonFilePath === undefined
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
"-v",
|
|
||||||
`${realmJsonFilePath}:/opt/keycloak/data/import/myrealm-realm.json`
|
|
||||||
]),
|
|
||||||
...[
|
|
||||||
"-v",
|
|
||||||
`${jarFilePath_cacheDir}:/opt/keycloak/providers/keycloak-theme.jar`
|
|
||||||
],
|
|
||||||
...(keycloakMajorVersionNumber <= 20
|
|
||||||
? ["-e", "JAVA_OPTS=-Dkeycloak.profile=preview"]
|
|
||||||
: []),
|
|
||||||
...[
|
|
||||||
...buildContext.themeNames,
|
|
||||||
...(fs.existsSync(
|
|
||||||
pathJoin(
|
|
||||||
buildContext.keycloakifyBuildDirPath,
|
|
||||||
"theme",
|
|
||||||
ACCOUNT_V1_THEME_NAME
|
|
||||||
)
|
|
||||||
)
|
|
||||||
? [ACCOUNT_V1_THEME_NAME]
|
|
||||||
: [])
|
|
||||||
]
|
|
||||||
.map(themeName => ({
|
|
||||||
localDirPath: pathJoin(
|
|
||||||
buildContext.keycloakifyBuildDirPath,
|
|
||||||
"theme",
|
|
||||||
themeName
|
|
||||||
),
|
|
||||||
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
|
||||||
}))
|
|
||||||
.map(({ localDirPath, containerDirPath }) => [
|
|
||||||
"-v",
|
|
||||||
`${localDirPath}:${containerDirPath}:rw`
|
|
||||||
])
|
|
||||||
.flat(),
|
|
||||||
...buildContext.environmentVariables
|
|
||||||
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
|
||||||
.map(({ name, envValue }) =>
|
|
||||||
envValue === undefined ? undefined : { name, envValue }
|
|
||||||
)
|
|
||||||
.filter(exclude(undefined))
|
|
||||||
.map(({ name, envValue }) => [
|
|
||||||
"--env",
|
|
||||||
`${name}='${envValue.replace(/'/g, "'\\''")}'`
|
|
||||||
])
|
|
||||||
.flat(),
|
|
||||||
`quay.io/keycloak/keycloak:${keycloakVersion}`,
|
|
||||||
"start-dev",
|
|
||||||
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
|
||||||
? ["--features=declarative-user-profile"]
|
|
||||||
: []),
|
|
||||||
...(realmJsonFilePath === undefined ? [] : ["--import-realm"])
|
|
||||||
],
|
|
||||||
{
|
|
||||||
cwd: buildContext.keycloakifyBuildDirPath,
|
|
||||||
shell: true
|
|
||||||
}
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
const child = child_process.spawn(...spawnArgs);
|
const SPACE_PLACEHOLDER = "SPACE_PLACEHOLDER_xKLmdPd";
|
||||||
|
|
||||||
|
const dockerRunArgs: string[] = [
|
||||||
|
`-p${SPACE_PLACEHOLDER}${port}:8080`,
|
||||||
|
`--name${SPACE_PLACEHOLDER}${CONTAINER_NAME}`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN=admin`,
|
||||||
|
`-e${SPACE_PLACEHOLDER}KEYCLOAK_ADMIN_PASSWORD=admin`,
|
||||||
|
buildContext.startKeycloakOptions.dockerExtraArgs.join(SPACE_PLACEHOLDER),
|
||||||
|
...(realmJsonFilePath === undefined
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
|
||||||
|
]),
|
||||||
|
`-v${SPACE_PLACEHOLDER}"./${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
|
||||||
|
...extensionJarFilePaths.map(
|
||||||
|
jarFilePath =>
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
|
||||||
|
),
|
||||||
|
...(keycloakMajorVersionNumber <= 20
|
||||||
|
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
|
||||||
|
: []),
|
||||||
|
...[
|
||||||
|
...buildContext.themeNames,
|
||||||
|
...(fs.existsSync(
|
||||||
|
pathJoin(
|
||||||
|
buildContext.keycloakifyBuildDirPath,
|
||||||
|
"theme",
|
||||||
|
ACCOUNT_V1_THEME_NAME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
? [ACCOUNT_V1_THEME_NAME]
|
||||||
|
: [])
|
||||||
|
]
|
||||||
|
.map(themeName => ({
|
||||||
|
localDirPath: pathJoin(
|
||||||
|
buildContext.keycloakifyBuildDirPath,
|
||||||
|
"theme",
|
||||||
|
themeName
|
||||||
|
),
|
||||||
|
containerDirPath: `/opt/keycloak/themes/${themeName}`
|
||||||
|
}))
|
||||||
|
.map(
|
||||||
|
({ localDirPath, containerDirPath }) =>
|
||||||
|
`-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
|
||||||
|
),
|
||||||
|
...buildContext.environmentVariables
|
||||||
|
.map(({ name }) => ({ name, envValue: process.env[name] }))
|
||||||
|
.map(({ name, envValue }) =>
|
||||||
|
envValue === undefined ? undefined : { name, envValue }
|
||||||
|
)
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.map(
|
||||||
|
({ name, envValue }) =>
|
||||||
|
`--env${SPACE_PLACEHOLDER}${name}='${envValue.replace(/'/g, "'\\''")}'`
|
||||||
|
),
|
||||||
|
`${buildContext.startKeycloakOptions.dockerImage?.reference ?? "quay.io/keycloak/keycloak"}:${dockerImageTag}`,
|
||||||
|
"start-dev",
|
||||||
|
...(21 <= keycloakMajorVersionNumber && keycloakMajorVersionNumber < 24
|
||||||
|
? ["--features=declarative-user-profile"]
|
||||||
|
: []),
|
||||||
|
...(realmJsonFilePath === undefined ? [] : ["--import-realm"]),
|
||||||
|
buildContext.startKeycloakOptions.keycloakExtraArgs.join(SPACE_PLACEHOLDER)
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
chalk.blue(
|
||||||
|
[
|
||||||
|
`$ docker run \\`,
|
||||||
|
...dockerRunArgs
|
||||||
|
.map(arg => arg.replace(new RegExp(SPACE_PLACEHOLDER, "g"), " "))
|
||||||
|
.map(
|
||||||
|
(line, i, arr) =>
|
||||||
|
` ${line}${arr.length - 1 === i ? "" : " \\"}`
|
||||||
|
)
|
||||||
|
].join("\n")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const child = child_process.spawn(
|
||||||
|
"docker",
|
||||||
|
["run", ...dockerRunArgs.map(line => line.split(SPACE_PLACEHOLDER)).flat()],
|
||||||
|
{ shell: true }
|
||||||
|
);
|
||||||
|
|
||||||
child.stdout.on("data", data => process.stdout.write(data));
|
child.stdout.on("data", data => process.stdout.write(data));
|
||||||
|
|
||||||
@ -372,7 +448,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
)} are mounted in the Keycloak container.`,
|
)} are mounted in the Keycloak container.`,
|
||||||
"",
|
"",
|
||||||
`Keycloak Admin console: ${chalk.cyan.bold(
|
`Keycloak Admin console: ${chalk.cyan.bold(
|
||||||
`http://localhost:${cliCommandOptions.port}`
|
`http://localhost:${port}`
|
||||||
)}`,
|
)}`,
|
||||||
`- user: ${chalk.cyan.bold("admin")}`,
|
`- user: ${chalk.cyan.bold("admin")}`,
|
||||||
`- password: ${chalk.cyan.bold("admin")}`,
|
`- password: ${chalk.cyan.bold("admin")}`,
|
||||||
@ -380,7 +456,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
|||||||
"",
|
"",
|
||||||
`${chalk.green("Your theme is accessible at:")}`,
|
`${chalk.green("Your theme is accessible at:")}`,
|
||||||
`${chalk.green("➜")} ${chalk.cyan.bold(
|
`${chalk.green("➜")} ${chalk.cyan.bold(
|
||||||
`https://my-theme.keycloakify.dev${cliCommandOptions.port === 8080 ? "" : `?port=${cliCommandOptions.port}`}`
|
`https://my-theme.keycloakify.dev${port === DEFAULT_PORT ? "" : `?port=${port}`}`
|
||||||
)}`,
|
)}`,
|
||||||
"",
|
"",
|
||||||
"You can login with the following credentials:",
|
"You can login with the following credentials:",
|
||||||
|
@ -5,9 +5,11 @@ export function assertNoPnpmDlx() {
|
|||||||
if (__dirname.includes(`${pathSep}pnpm${pathSep}dlx${pathSep}`)) {
|
if (__dirname.includes(`${pathSep}pnpm${pathSep}dlx${pathSep}`)) {
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
chalk.red("Please don't use `pnpm dlx keycloakify`"),
|
chalk.red(
|
||||||
|
"Please don't use `pnpm dlx keycloakify` (download and execute)"
|
||||||
|
),
|
||||||
"\nUse `npx keycloakify` or `pnpm exec keycloakify` instead since you want to use the keycloakify",
|
"\nUse `npx keycloakify` or `pnpm exec keycloakify` instead since you want to use the keycloakify",
|
||||||
"version that is installed in your project and not the latest version on NPM."
|
"version that is installed in your project and not download and run the latest NPM version of keycloakify."
|
||||||
].join(" ")
|
].join(" ")
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||||
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
|
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
|
||||||
import { dirname as pathDirname, join as pathJoin } from "path";
|
import { dirname as pathDirname, join as pathJoin, basename as pathBasename } from "path";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { extractArchive } from "./extractArchive";
|
import { extractArchive } from "./extractArchive";
|
||||||
import { existsAsync } from "./fs.existsAsync";
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
import { rm } from "./fs.rm";
|
import { rm } from "./fs.rm";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
|
||||||
export async function downloadAndExtractArchive(params: {
|
export async function downloadAndExtractArchive(params: {
|
||||||
url: string;
|
urlOrPath: string;
|
||||||
uniqueIdOfOnArchiveFile: string;
|
uniqueIdOfOnArchiveFile: string;
|
||||||
onArchiveFile: (params: {
|
onArchiveFile: (params: {
|
||||||
fileRelativePath: string;
|
fileRelativePath: string;
|
||||||
@ -21,15 +21,34 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
}) => Promise<void>;
|
}) => Promise<void>;
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
fetchOptions: FetchOptions | undefined;
|
fetchOptions: FetchOptions | undefined;
|
||||||
}): Promise<{ extractedDirPath: string }> {
|
}): Promise<{ extractedDirPath: string; archiveFilePath: string; }> {
|
||||||
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
const {
|
||||||
params;
|
urlOrPath,
|
||||||
|
uniqueIdOfOnArchiveFile,
|
||||||
|
onArchiveFile,
|
||||||
|
cacheDirPath,
|
||||||
|
fetchOptions
|
||||||
|
} = params;
|
||||||
|
|
||||||
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
|
const isUrl = /^https?:\/\//.test(urlOrPath);
|
||||||
|
|
||||||
|
const archiveFileBasename = isUrl
|
||||||
|
? urlOrPath.split("?")[0].split("/").reverse()[0]
|
||||||
|
: pathBasename(urlOrPath);
|
||||||
|
|
||||||
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
|
const archiveFilePath = pathJoin(cacheDirPath, archiveFileBasename);
|
||||||
|
|
||||||
download: {
|
download: {
|
||||||
|
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
||||||
|
|
||||||
|
if (!isUrl) {
|
||||||
|
await fsPr.copyFile(urlOrPath, archiveFilePath);
|
||||||
|
|
||||||
|
break download;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = urlOrPath;
|
||||||
|
|
||||||
if (await existsAsync(archiveFilePath)) {
|
if (await existsAsync(archiveFilePath)) {
|
||||||
const isDownloaded = await SuccessTracker.getIsDownloaded({
|
const isDownloaded = await SuccessTracker.getIsDownloaded({
|
||||||
cacheDirPath,
|
cacheDirPath,
|
||||||
@ -48,8 +67,6 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
|
||||||
|
|
||||||
const response = await fetch(url, fetchOptions);
|
const response = await fetch(url, fetchOptions);
|
||||||
|
|
||||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||||
@ -136,7 +153,7 @@ export async function downloadAndExtractArchive(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { extractedDirPath };
|
return { extractedDirPath, archiveFilePath };
|
||||||
}
|
}
|
||||||
|
|
||||||
type SuccessTracker = {
|
type SuccessTracker = {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { type FetchOptions } from "make-fetch-happen";
|
import { type FetchOptions } from "make-fetch-happen";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
export type ProxyFetchOptions = Pick<
|
export type ProxyFetchOptions = Pick<
|
||||||
FetchOptions,
|
FetchOptions,
|
||||||
@ -23,12 +24,32 @@ export function getProxyFetchOptions(params: {
|
|||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(line => !line.startsWith(";"))
|
.filter(line => !line.startsWith(";"))
|
||||||
.map(line => line.trim())
|
.map(line => line.trim())
|
||||||
.map(line => line.split("=", 2) as [string, string])
|
.map(line => {
|
||||||
|
const [key, value] = line.split("=");
|
||||||
|
if (key === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return [key.trim(), value.trim()] as const;
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))
|
||||||
|
.filter(([key]) => key !== "")
|
||||||
|
.map(([key, value]) => {
|
||||||
|
if (value.startsWith('"') && value.endsWith('"')) {
|
||||||
|
return [key, value.slice(1, -1)] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "true" || value === "false") {
|
||||||
|
return [key, value] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))
|
||||||
.reduce(
|
.reduce(
|
||||||
(
|
(cfg: Record<string, string | string[]>, [key, value]) =>
|
||||||
cfg: Record<string, string | string[]>,
|
|
||||||
[key, value]: [string, string]
|
|
||||||
) =>
|
|
||||||
key in cfg
|
key in cfg
|
||||||
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
||||||
: { ...cfg, [key]: value },
|
: { ...cfg, [key]: value },
|
||||||
@ -37,18 +58,18 @@ export function getProxyFetchOptions(params: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||||
|
|
||||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||||
|
|
||||||
function maybeBoolean(arg0: string | undefined) {
|
const strictSSL = ensureSingleOrNone(cfg["strict-ssl"]) === "true";
|
||||||
return typeof arg0 === "undefined" ? undefined : Boolean(arg0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const strictSSL = maybeBoolean(ensureSingleOrNone(cfg["strict-ssl"]));
|
|
||||||
const cert = cfg["cert"];
|
const cert = cfg["cert"];
|
||||||
|
|
||||||
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
const ca = ensureArray(cfg["ca"] ?? cfg["ca[]"]);
|
||||||
|
|
||||||
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
const cafile = ensureSingleOrNone(cfg["cafile"]);
|
||||||
|
|
||||||
if (typeof cafile !== "undefined" && cafile !== "null") {
|
if (cafile !== undefined) {
|
||||||
ca.push(
|
ca.push(
|
||||||
...(() => {
|
...(() => {
|
||||||
const cafileContent = fs.readFileSync(cafile).toString("utf8");
|
const cafileContent = fs.readFileSync(cafile).toString("utf8");
|
||||||
@ -82,7 +103,7 @@ export function getProxyFetchOptions(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureArray<T>(arg0: T | T[]) {
|
function ensureArray<T>(arg0: T | T[]) {
|
||||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
return Array.isArray(arg0) ? arg0 : arg0 === undefined ? [] : [arg0];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||||
|
@ -79,8 +79,8 @@ export declare namespace KcContext {
|
|||||||
};
|
};
|
||||||
realm: {
|
realm: {
|
||||||
name: string;
|
name: string;
|
||||||
displayName?: string;
|
displayName: string;
|
||||||
displayNameHtml?: string;
|
displayNameHtml: string;
|
||||||
internationalizationEnabled: boolean;
|
internationalizationEnabled: boolean;
|
||||||
registrationEmailAsUsername: boolean;
|
registrationEmailAsUsername: boolean;
|
||||||
};
|
};
|
||||||
|
@ -224,19 +224,17 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
{auth !== undefined && auth.showTryAnotherWayLink && (
|
{auth !== undefined && auth.showTryAnotherWayLink && (
|
||||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post">
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
<a
|
||||||
<a
|
href="#"
|
||||||
href="#"
|
id="try-another-way"
|
||||||
id="try-another-way"
|
onClick={() => {
|
||||||
onClick={() => {
|
document.forms["kc-select-try-another-way-form" as never].submit();
|
||||||
document.forms["kc-select-try-another-way-form" as never].submit();
|
return false;
|
||||||
return false;
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{msg("doTryAnotherWay")}
|
||||||
{msg("doTryAnotherWay")}
|
</a>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
@ -217,7 +217,13 @@ function createI18nTranslationFunctionsFactory<MessageKey_themeDefined extends s
|
|||||||
|
|
||||||
messageWithArgsInjected = messageWithArgsInjected.replace(
|
messageWithArgsInjected = messageWithArgsInjected.replace(
|
||||||
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
new RegExp(`\\{${i + startIndex}\\}`, "g"),
|
||||||
arg.replace(/</g, "<").replace(/>/g, ">")
|
(() => {
|
||||||
|
if (key === "loginTitleHtml") {
|
||||||
|
return arg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return arg.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
})()
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -59,6 +59,8 @@ export type FormAction =
|
|||||||
action: "update";
|
action: "update";
|
||||||
name: string;
|
name: string;
|
||||||
valueOrValues: string | string[];
|
valueOrValues: string | string[];
|
||||||
|
/** Default false */
|
||||||
|
displayErrorsImmediately?: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
action: "focus lost";
|
action: "focus lost";
|
||||||
@ -413,6 +415,24 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType
|
|||||||
formFieldStates: state.formFieldStates
|
formFieldStates: state.formFieldStates
|
||||||
});
|
});
|
||||||
|
|
||||||
|
simulate_focus_lost: {
|
||||||
|
const { displayErrorsImmediately = false } = formAction;
|
||||||
|
|
||||||
|
if (!displayErrorsImmediately) {
|
||||||
|
break simulate_focus_lost;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fieldIndex of formAction.valueOrValues instanceof Array
|
||||||
|
? formAction.valueOrValues.map((...[, index]) => index)
|
||||||
|
: [undefined]) {
|
||||||
|
state = reducer(state, {
|
||||||
|
action: "focus lost",
|
||||||
|
name: formAction.name,
|
||||||
|
fieldIndex
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
update_password_confirm: {
|
update_password_confirm: {
|
||||||
if (doMakeUserConfirmPassword) {
|
if (doMakeUserConfirmPassword) {
|
||||||
break update_password_confirm;
|
break update_password_confirm;
|
||||||
@ -425,7 +445,8 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType
|
|||||||
state = reducer(state, {
|
state = reducer(state, {
|
||||||
action: "update",
|
action: "update",
|
||||||
name: "password-confirm",
|
name: "password-confirm",
|
||||||
valueOrValues: formAction.valueOrValues
|
valueOrValues: formAction.valueOrValues,
|
||||||
|
displayErrorsImmediately: formAction.displayErrorsImmediately
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +468,8 @@ export function useUserProfileForm(params: UseUserProfileFormParams): ReturnType
|
|||||||
assert(formFieldState !== undefined);
|
assert(formFieldState !== undefined);
|
||||||
|
|
||||||
return formFieldState.valueOrValues;
|
return formFieldState.valueOrValues;
|
||||||
})()
|
})(),
|
||||||
|
displayErrorsImmediately: formAction.displayErrorsImmediately
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,30 +32,30 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
|
|||||||
<label htmlFor="password-new" className={kcClsx("kcLabelClass")}>
|
<label htmlFor="password-new" className={kcClsx("kcLabelClass")}>
|
||||||
{msg("passwordNew")}
|
{msg("passwordNew")}
|
||||||
</label>
|
</label>
|
||||||
<div className={kcClsx("kcInputWrapperClass")}>
|
</div>
|
||||||
<PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password-new">
|
<div className={kcClsx("kcInputWrapperClass")}>
|
||||||
<input
|
<PasswordWrapper kcClsx={kcClsx} i18n={i18n} passwordInputId="password-new">
|
||||||
type="password"
|
<input
|
||||||
id="password-new"
|
type="password"
|
||||||
name="password-new"
|
id="password-new"
|
||||||
className={kcClsx("kcInputClass")}
|
name="password-new"
|
||||||
autoFocus
|
className={kcClsx("kcInputClass")}
|
||||||
autoComplete="new-password"
|
autoFocus
|
||||||
aria-invalid={messagesPerField.existsError("password", "password-confirm")}
|
autoComplete="new-password"
|
||||||
/>
|
aria-invalid={messagesPerField.existsError("password", "password-confirm")}
|
||||||
</PasswordWrapper>
|
/>
|
||||||
|
</PasswordWrapper>
|
||||||
|
|
||||||
{messagesPerField.existsError("password") && (
|
{messagesPerField.existsError("password") && (
|
||||||
<span
|
<span
|
||||||
id="input-error-password"
|
id="input-error-password"
|
||||||
className={kcClsx("kcInputErrorMessageClass")}
|
className={kcClsx("kcInputErrorMessageClass")}
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: messagesPerField.get("password")
|
__html: messagesPerField.get("password")
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -89,32 +89,30 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className={kcClsx("kcFormGroupClass")}>
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
<LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} />
|
<LogoutOtherSessions kcClsx={kcClsx} i18n={i18n} />
|
||||||
|
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||||
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
<input
|
||||||
<input
|
className={kcClsx(
|
||||||
className={kcClsx(
|
"kcButtonClass",
|
||||||
"kcButtonClass",
|
"kcButtonPrimaryClass",
|
||||||
"kcButtonPrimaryClass",
|
!isAppInitiatedAction && "kcButtonBlockClass",
|
||||||
isAppInitiatedAction && "kcButtonBlockClass",
|
"kcButtonLargeClass"
|
||||||
"kcButtonLargeClass"
|
|
||||||
)}
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
{isAppInitiatedAction && (
|
|
||||||
<button
|
|
||||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
|
||||||
type="submit"
|
|
||||||
name="cancel-aia"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
{msg("doCancel")}
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
{isAppInitiatedAction && (
|
||||||
|
<button
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonLargeClass")}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -22,7 +22,15 @@ export function useInsertLinkTags(params: {
|
|||||||
alreadyMountedComponentOrHookNames.has(componentOrHookName);
|
alreadyMountedComponentOrHookNames.has(componentOrHookName);
|
||||||
|
|
||||||
if (isAlreadyMounted) {
|
if (isAlreadyMounted) {
|
||||||
window.location.reload();
|
reload: {
|
||||||
|
if (
|
||||||
|
new URL(window.location.href).searchParams.get("viewMode") === "docs"
|
||||||
|
) {
|
||||||
|
// NOTE: Special case for Storybook, we want to avoid infinite reload loop.
|
||||||
|
break reload;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,15 @@ export function useInsertScriptTags(params: {
|
|||||||
alreadyMountedComponentOrHookNames.has(componentOrHookName);
|
alreadyMountedComponentOrHookNames.has(componentOrHookName);
|
||||||
|
|
||||||
if (isAlreadyMounted) {
|
if (isAlreadyMounted) {
|
||||||
window.location.reload();
|
reload: {
|
||||||
|
if (
|
||||||
|
new URL(window.location.href).searchParams.get("viewMode") === "docs"
|
||||||
|
) {
|
||||||
|
// NOTE: Special case for Storybook, we want to avoid infinite reload loop.
|
||||||
|
break reload;
|
||||||
|
}
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
94
stories/account-spa/index.stories.tsx
Normal file
94
stories/account-spa/index.stories.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { useInsertLinkTags } from "../../dist/tools/useInsertLinkTags";
|
||||||
|
import { tss } from "../tss";
|
||||||
|
// @ts-expect-error
|
||||||
|
import screenshotPngUrl from "./screenshot.png";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Account SPA/index.ftl"
|
||||||
|
} satisfies Meta<any>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const NotInStorybookButSupported: Story = {
|
||||||
|
render: () => <AccountSpa />
|
||||||
|
};
|
||||||
|
|
||||||
|
function AccountSpa() {
|
||||||
|
console.log(window.location.href);
|
||||||
|
|
||||||
|
useInsertLinkTags({
|
||||||
|
componentOrHookName: "Template",
|
||||||
|
hrefs: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { classes, theme } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<p>
|
||||||
|
Keycloakify offers two option for creating an account theme:{" "}
|
||||||
|
<a href="https://docs.keycloakify.dev/account-theme#multi-page" target="_blank">
|
||||||
|
Multi Page
|
||||||
|
</a>{" "}
|
||||||
|
or{" "}
|
||||||
|
<a href="https://docs.keycloakify.dev/account-theme#single-page" target="_blank">
|
||||||
|
Single Page
|
||||||
|
</a>
|
||||||
|
. Since the account Single Page does not support Storybook, here is a screenshot of it's default look:
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<img className={classes.screenshot} alt="image" src={screenshotPngUrl} />
|
||||||
|
<br />
|
||||||
|
<a href="https://docs.keycloakify.dev/account-theme" target="_blank">
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = tss.withName({ AccountSpa }).create(({ isDark, theme }) => ({
|
||||||
|
root: {
|
||||||
|
height: "100vh",
|
||||||
|
color: isDark ? "white" : "black",
|
||||||
|
backgroundColor: theme.appContentBg,
|
||||||
|
fontFamily: "'Work Sans'",
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
WebkitFontSmoothing: "antialiased",
|
||||||
|
"& a": {
|
||||||
|
color: theme.colorSecondary,
|
||||||
|
textDecoration: "none",
|
||||||
|
"&:hover": {
|
||||||
|
textDecoration: "underline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"& h1": {
|
||||||
|
fontSize: "32px",
|
||||||
|
marginBottom: 35
|
||||||
|
},
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
maxWidth: 750,
|
||||||
|
textAlign: "center",
|
||||||
|
marginTop: 100
|
||||||
|
},
|
||||||
|
keycloakifyLogoWrapper: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
keycloakifyLogo: {
|
||||||
|
width: 400
|
||||||
|
},
|
||||||
|
screenshot: {
|
||||||
|
maxWidth: "100%"
|
||||||
|
}
|
||||||
|
}));
|
BIN
stories/account-spa/screenshot.png
Normal file
BIN
stories/account-spa/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 269 KiB |
@ -15,6 +15,7 @@ export default meta;
|
|||||||
|
|
||||||
type Story = StoryObj<typeof meta>;
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
// NOTE: Enable in your Keycloak realm with: https://github.com/user-attachments/assets/5fc5e49e-a172-4cb0-897a-49baac284b47
|
||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
render: () => (
|
render: () => (
|
||||||
<KcPageStory
|
<KcPageStory
|
||||||
|
@ -2,18 +2,20 @@ import React from "react";
|
|||||||
import { memo, useState } from "react";
|
import { memo, useState } from "react";
|
||||||
import { useConstCallback } from "powerhooks";
|
import { useConstCallback } from "powerhooks";
|
||||||
import { keyframes } from "tss-react";
|
import { keyframes } from "tss-react";
|
||||||
|
// @ts-expect-error
|
||||||
import keycloakifyLogoHeroMovingPngUrl from "./keycloakify-logo-hero-moving.png";
|
import keycloakifyLogoHeroMovingPngUrl from "./keycloakify-logo-hero-moving.png";
|
||||||
|
// @ts-expect-error
|
||||||
import keycloakifyLogoHeroStillPngUrl from "./keycloakify-logo-hero-still.png";
|
import keycloakifyLogoHeroStillPngUrl from "./keycloakify-logo-hero-still.png";
|
||||||
import { makeStyles } from "./tss";
|
import { tss } from "../tss";
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
style?: React.CSSProperties;
|
className?: string;
|
||||||
id?: string;
|
id?: string;
|
||||||
onLoad?: () => void;
|
onLoad?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KeycloakifyRotatingLogo = memo((props: Props) => {
|
export const KeycloakifyRotatingLogo = memo((props: Props) => {
|
||||||
const { id, style, onLoad: onLoadProp } = props;
|
const { id, className, onLoad: onLoadProp } = props;
|
||||||
|
|
||||||
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
const [isImageLoaded, setIsImageLoaded] = useState(false);
|
||||||
|
|
||||||
@ -22,40 +24,41 @@ export const KeycloakifyRotatingLogo = memo((props: Props) => {
|
|||||||
onLoadProp?.();
|
onLoadProp?.();
|
||||||
});
|
});
|
||||||
|
|
||||||
const { classes } = useStyles({
|
const { cx, classes } = useStyles({
|
||||||
isImageLoaded
|
isImageLoaded
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div id={id} className={classes.root} style={style}>
|
<div id={id} className={cx(classes.root, className)}>
|
||||||
<img className={classes.rotatingImg} onLoad={onLoad} src={keycloakifyLogoHeroMovingPngUrl} alt={"Rotating react logo"} />
|
<img className={classes.rotatingImg} onLoad={onLoad} src={keycloakifyLogoHeroMovingPngUrl} alt={"Rotating react logo"} />
|
||||||
<img className={classes.stillImg} src={keycloakifyLogoHeroStillPngUrl} alt={"keyhole"} />
|
<img className={classes.stillImg} src={keycloakifyLogoHeroStillPngUrl} alt={"keyhole"} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const useStyles = makeStyles<{ isImageLoaded: boolean }>({
|
const useStyles = tss
|
||||||
name: { KeycloakifyRotatingLogo }
|
.withParams<{ isImageLoaded: boolean }>()
|
||||||
})((_theme, { isImageLoaded }) => ({
|
.withName({ KeycloakifyRotatingLogo })
|
||||||
root: {
|
.create(({ isImageLoaded }) => ({
|
||||||
position: "relative"
|
root: {
|
||||||
},
|
position: "relative"
|
||||||
rotatingImg: {
|
},
|
||||||
animation: `${keyframes({
|
rotatingImg: {
|
||||||
from: {
|
animation: `${keyframes({
|
||||||
transform: "rotate(0deg)"
|
from: {
|
||||||
},
|
transform: "rotate(0deg)"
|
||||||
to: {
|
},
|
||||||
transform: "rotate(360deg)"
|
to: {
|
||||||
}
|
transform: "rotate(360deg)"
|
||||||
})} infinite 20s linear`,
|
}
|
||||||
width: isImageLoaded ? "100%" : undefined,
|
})} infinite 20s linear`,
|
||||||
height: isImageLoaded ? "auto" : undefined
|
width: isImageLoaded ? "100%" : undefined,
|
||||||
},
|
height: isImageLoaded ? "auto" : undefined
|
||||||
stillImg: {
|
},
|
||||||
position: "absolute",
|
stillImg: {
|
||||||
top: "0",
|
position: "absolute",
|
||||||
left: "0",
|
top: "0",
|
||||||
width: isImageLoaded ? "100%" : undefined,
|
left: "0",
|
||||||
height: isImageLoaded ? "auto" : undefined
|
width: isImageLoaded ? "100%" : undefined,
|
||||||
}
|
height: isImageLoaded ? "auto" : undefined
|
||||||
}));
|
}
|
||||||
|
}));
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
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 of the login and account theme.
|
|
||||||
The storybook serves as a reference to help you determine which pages you would like to personalize.
|
|
||||||
These pages are a direct React adaptation of the [built-in FreeMarker Keycloak pages](https://github.com/keycloak/keycloak/tree/24.0.4/themes/src/main/resources/theme/base).
|
|
||||||
You may notice some visual bugs on certain pages; these issues were not introduced by Keycloakiy and are also present in the default Keycloak 24 theme.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
90
stories/intro/intro.stories.tsx
Normal file
90
stories/intro/intro.stories.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { KeycloakifyRotatingLogo } from "./KeycloakifyRotatingLogo";
|
||||||
|
import { useInsertLinkTags } from "../../dist/tools/useInsertLinkTags";
|
||||||
|
import { useOnFistMount } from "../../dist/tools/useOnFirstMount";
|
||||||
|
import { tss } from "../tss";
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "Introduction"
|
||||||
|
} satisfies Meta<any>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const WhatIsThisWebsite: Story = {
|
||||||
|
render: () => <Introduction />
|
||||||
|
};
|
||||||
|
|
||||||
|
function Introduction() {
|
||||||
|
console.log(window.location.href);
|
||||||
|
|
||||||
|
useInsertLinkTags({
|
||||||
|
componentOrHookName: "Template",
|
||||||
|
hrefs: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const { classes, theme } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<div className={classes.content}>
|
||||||
|
<div className={classes.keycloakifyLogoWrapper}>
|
||||||
|
<KeycloakifyRotatingLogo className={classes.keycloakifyLogo} />
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
<a href={theme.brandUrl}>Keycloakify </a> Storybook
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This website showcases all the Keycloak user-facing pages of the Login and{" "}
|
||||||
|
<a href="https://docs.keycloakify.dev/account-theme#multi-page">Account Multi-Page theme</a>.<br />
|
||||||
|
The storybook serves as a reference to help you determine which pages you would like to personalize.
|
||||||
|
<br />
|
||||||
|
These pages are a direct React adaptation of the{" "}
|
||||||
|
<a href="https://github.com/keycloak/keycloak/tree/24.0.4/themes/src/main/resources/theme/base" target="_blank">
|
||||||
|
built-in FreeMarker Keycloak pages
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = tss.withName({ Introduction }).create(({ isDark, theme }) => ({
|
||||||
|
root: {
|
||||||
|
height: "100vh",
|
||||||
|
color: isDark ? "white" : "black",
|
||||||
|
backgroundColor: theme.appContentBg,
|
||||||
|
fontFamily: "'Work Sans'",
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "24px",
|
||||||
|
WebkitFontSmoothing: "antialiased",
|
||||||
|
"& a": {
|
||||||
|
color: theme.colorSecondary,
|
||||||
|
textDecoration: "none",
|
||||||
|
"&:hover": {
|
||||||
|
textDecoration: "underline"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"& h1": {
|
||||||
|
fontSize: "32px",
|
||||||
|
marginBottom: 35
|
||||||
|
},
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
maxWidth: 750,
|
||||||
|
textAlign: "center"
|
||||||
|
},
|
||||||
|
keycloakifyLogoWrapper: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center"
|
||||||
|
},
|
||||||
|
keycloakifyLogo: {
|
||||||
|
width: 400
|
||||||
|
}
|
||||||
|
}));
|
@ -1,5 +0,0 @@
|
|||||||
import { createMakeAndWithStyles } from "tss-react";
|
|
||||||
|
|
||||||
export const { makeStyles, useStyles } = createMakeAndWithStyles({
|
|
||||||
useTheme: () => ({})
|
|
||||||
});
|
|
@ -122,73 +122,85 @@ export const WithSocialProviders: Story = {
|
|||||||
loginUrl: "google",
|
loginUrl: "google",
|
||||||
alias: "google",
|
alias: "google",
|
||||||
providerId: "google",
|
providerId: "google",
|
||||||
displayName: "Google"
|
displayName: "Google",
|
||||||
|
iconClasses: "fa fa-google"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "microsoft",
|
loginUrl: "microsoft",
|
||||||
alias: "microsoft",
|
alias: "microsoft",
|
||||||
providerId: "microsoft",
|
providerId: "microsoft",
|
||||||
displayName: "Microsoft"
|
displayName: "Microsoft",
|
||||||
|
iconClasses: "fa fa-windows"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "facebook",
|
loginUrl: "facebook",
|
||||||
alias: "facebook",
|
alias: "facebook",
|
||||||
providerId: "facebook",
|
providerId: "facebook",
|
||||||
displayName: "Facebook"
|
displayName: "Facebook",
|
||||||
|
iconClasses: "fa fa-facebook"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "instagram",
|
loginUrl: "instagram",
|
||||||
alias: "instagram",
|
alias: "instagram",
|
||||||
providerId: "instagram",
|
providerId: "instagram",
|
||||||
displayName: "Instagram"
|
displayName: "Instagram",
|
||||||
|
iconClasses: "fa fa-instagram"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "twitter",
|
loginUrl: "twitter",
|
||||||
alias: "twitter",
|
alias: "twitter",
|
||||||
providerId: "twitter",
|
providerId: "twitter",
|
||||||
displayName: "Twitter"
|
displayName: "Twitter",
|
||||||
|
iconClasses: "fa fa-twitter"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "linkedin",
|
loginUrl: "linkedin",
|
||||||
alias: "linkedin",
|
alias: "linkedin",
|
||||||
providerId: "linkedin",
|
providerId: "linkedin",
|
||||||
displayName: "LinkedIn"
|
displayName: "LinkedIn",
|
||||||
|
iconClasses: "fa fa-linkedin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "stackoverflow",
|
loginUrl: "stackoverflow",
|
||||||
alias: "stackoverflow",
|
alias: "stackoverflow",
|
||||||
providerId: "stackoverflow",
|
providerId: "stackoverflow",
|
||||||
displayName: "Stackoverflow"
|
displayName: "Stackoverflow",
|
||||||
|
iconClasses: "fa fa-stack-overflow"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "github",
|
loginUrl: "github",
|
||||||
alias: "github",
|
alias: "github",
|
||||||
providerId: "github",
|
providerId: "github",
|
||||||
displayName: "Github"
|
displayName: "Github",
|
||||||
|
iconClasses: "fa fa-github"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "gitlab",
|
loginUrl: "gitlab",
|
||||||
alias: "gitlab",
|
alias: "gitlab",
|
||||||
providerId: "gitlab",
|
providerId: "gitlab",
|
||||||
displayName: "Gitlab"
|
displayName: "Gitlab",
|
||||||
|
iconClasses: "fa fa-gitlab"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "bitbucket",
|
loginUrl: "bitbucket",
|
||||||
alias: "bitbucket",
|
alias: "bitbucket",
|
||||||
providerId: "bitbucket",
|
providerId: "bitbucket",
|
||||||
displayName: "Bitbucket"
|
displayName: "Bitbucket",
|
||||||
|
iconClasses: "fa fa-bitbucket"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "paypal",
|
loginUrl: "paypal",
|
||||||
alias: "paypal",
|
alias: "paypal",
|
||||||
providerId: "paypal",
|
providerId: "paypal",
|
||||||
displayName: "PayPal"
|
displayName: "PayPal",
|
||||||
|
iconClasses: "fa fa-paypal"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loginUrl: "openshift",
|
loginUrl: "openshift",
|
||||||
alias: "openshift",
|
alias: "openshift",
|
||||||
providerId: "openshift",
|
providerId: "openshift",
|
||||||
displayName: "OpenShift"
|
displayName: "OpenShift",
|
||||||
|
iconClasses: "fa fa-cloud"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
13
stories/tss.ts
Normal file
13
stories/tss.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { createTss } from "tss-react";
|
||||||
|
import { useDarkMode } from "storybook-dark-mode";
|
||||||
|
import { darkTheme, lightTheme } from "../.storybook/customTheme";
|
||||||
|
|
||||||
|
function useContext() {
|
||||||
|
const isDark = useDarkMode();
|
||||||
|
|
||||||
|
return { isDark, theme: isDark ? darkTheme : lightTheme };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const { tss } = createTss({
|
||||||
|
useContext
|
||||||
|
});
|
Reference in New Issue
Block a user