Merge branch 'main' into fix-download-cache
This commit is contained in:
commit
0ba2f37004
50
README.md
50
README.md
@ -14,7 +14,7 @@
|
|||||||
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
||||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/InseeFrLab/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||||
@ -25,6 +25,8 @@
|
|||||||
-
|
-
|
||||||
<a href="https://docs.keycloakify.dev">Documentation</a>
|
<a href="https://docs.keycloakify.dev">Documentation</a>
|
||||||
-
|
-
|
||||||
|
<a href="https://storybook.keycloakify.dev/storybook">Storybook</a>
|
||||||
|
-
|
||||||
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||||
</p>
|
</p>
|
||||||
</p>
|
</p>
|
||||||
@ -34,15 +36,25 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
> 🗣 V6 have been released 🎉
|
The more ⭐️ the project gets, the more time I spend improving and maintaining it. Thank you for your support 😊
|
||||||
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
|
|
||||||
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
|
> 🗣 V7 have been released 🎉
|
||||||
|
> [It features major improvements](https://github.com/keycloakify/keycloakify#70-).
|
||||||
|
> Checkout [the migration guide](https://docs.keycloakify.dev/migration-guides/v6-greater-than-v7).
|
||||||
|
|
||||||
# Changelog highlights
|
# Changelog highlights
|
||||||
|
|
||||||
|
## 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
|
## 6.13
|
||||||
|
|
||||||
- Build work behind corporate proxies, [see issue](https://github.com/InseeFrLab/keycloakify/issues/257).
|
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
|
||||||
|
|
||||||
## 6.12
|
## 6.12
|
||||||
|
|
||||||
@ -55,13 +67,13 @@ Massive improvement in the developer experience:
|
|||||||
|
|
||||||
## 6.11.4
|
## 6.11.4
|
||||||
|
|
||||||
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/239).
|
- 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).
|
- 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.
|
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
|
## 6.10.0
|
||||||
|
|
||||||
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/226)). WSL is no longer required 🎉
|
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
|
||||||
|
|
||||||
## 6.8.4
|
## 6.8.4
|
||||||
|
|
||||||
@ -71,19 +83,19 @@ Massive improvement in the developer experience:
|
|||||||
|
|
||||||
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
|
- 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
|
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/InseeFrLab/keycloakify/issues/191).
|
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/keycloakify/issues/191).
|
||||||
|
|
||||||
## 6.7.0
|
## 6.7.0
|
||||||
|
|
||||||
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/185).
|
- 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
|
## 6.6.0
|
||||||
|
|
||||||
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/184).
|
- 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
|
## 6.5.0
|
||||||
|
|
||||||
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/183).
|
- 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
|
## 6.4.0
|
||||||
|
|
||||||
@ -102,11 +114,11 @@ Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
|
|||||||
|
|
||||||
## 5.8.0
|
## 5.8.0
|
||||||
|
|
||||||
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/InseeFrLab/keycloakify/issues/141)
|
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
|
||||||
|
|
||||||
## 5.7.0
|
## 5.7.0
|
||||||
|
|
||||||
- Feat `logout-confirm.ftl`. [PR](https://github.com/InseeFrLab/keycloakify/pull/120)
|
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
|
||||||
|
|
||||||
## 5.6.4
|
## 5.6.4
|
||||||
|
|
||||||
@ -114,7 +126,7 @@ Fix `login-verify-email.ftl` page. [Before](https://user-images.githubuserconten
|
|||||||
|
|
||||||
## v5.6.0
|
## v5.6.0
|
||||||
|
|
||||||
Add support for `login-config-totp.ftl` page [#127](https://github.com/InseeFrLab/keycloakify/pull/127).
|
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
|
||||||
|
|
||||||
## v5.3.0
|
## v5.3.0
|
||||||
|
|
||||||
@ -129,7 +141,7 @@ Import of terms and services have changed. [See example](https://github.com/garr
|
|||||||
|
|
||||||
## v4.10.0
|
## v4.10.0
|
||||||
|
|
||||||
Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycloakify/pull/92).
|
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
|
||||||
|
|
||||||
## v4.8.0
|
## v4.8.0
|
||||||
|
|
||||||
@ -142,7 +154,7 @@ Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycl
|
|||||||
## v4.7.2
|
## v4.7.2
|
||||||
|
|
||||||
> WARNING: This is broken.
|
> WARNING: This is broken.
|
||||||
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
|
> 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.
|
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||||
|
|
||||||
## v4.7.0
|
## v4.7.0
|
||||||
@ -176,12 +188,12 @@ Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77c
|
|||||||
|
|
||||||
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.
|
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
|
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/InseeFrLab/keycloakify#implement-context-persistence-optional).
|
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
|
||||||
|
|
||||||
## v2.5
|
## v2.5
|
||||||
|
|
||||||
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||||
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
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`.
|
- Test container now uses Keycloak version `15.0.2`.
|
||||||
|
|
||||||
## v2
|
## v2
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "7.0.0-rc.10",
|
"version": "7.3.0",
|
||||||
"description": "Create Keycloak themes using React",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/inseefrlab/keycloakify.git"
|
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||||
},
|
},
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
|
@ -43,6 +43,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
|||||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||||
<li>
|
<li>
|
||||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
<a href="#" id="kc-current-locale-link">
|
<a href="#" id="kc-current-locale-link">
|
||||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||||
</a>
|
</a>
|
||||||
|
@ -64,6 +64,7 @@ export declare namespace KcContext {
|
|||||||
password: {
|
password: {
|
||||||
passwordSet: boolean;
|
passwordSet: boolean;
|
||||||
};
|
};
|
||||||
|
stateChecker: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Account = Common & {
|
export type Account = Common & {
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
|
||||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
@ -7,6 +6,7 @@ import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
|||||||
import { pathBasename } from "keycloakify/tools/pathBasename";
|
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||||
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
import { symToStr } from "tsafe/symToStr";
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||||
|
|
||||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||||
@ -62,6 +62,10 @@ export function getKcContext<KcContextExtension extends { pageId: string } = nev
|
|||||||
return { "kcContext": undefined };
|
return { "kcContext": undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!("account" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const { url } = realKcContext;
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { KcContext } from "./KcContext";
|
|
||||||
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||||
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||||
? KcContext
|
? KcContext
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
import type { KcContext } from "./KcContext";
|
|
||||||
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
import { id } from "tsafe/id";
|
import { id } from "tsafe/id";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
@ -154,7 +154,8 @@ export const kcContextMocks: KcContext[] = [
|
|||||||
"pageId": "password.ftl",
|
"pageId": "password.ftl",
|
||||||
"password": {
|
"password": {
|
||||||
"passwordSet": true
|
"passwordSet": true
|
||||||
}
|
},
|
||||||
|
"stateChecker": "state checker"
|
||||||
}),
|
}),
|
||||||
id<KcContext.Account>({
|
id<KcContext.Account>({
|
||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
|
@ -15,7 +15,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const { url, password, account } = kcContext;
|
const { url, password, account, stateChecker } = kcContext;
|
||||||
|
|
||||||
const { msg } = i18n;
|
const { msg } = i18n;
|
||||||
|
|
||||||
@ -55,7 +55,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}" />
|
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||||
|
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="col-sm-2 col-md-2">
|
<div className="col-sm-2 col-md-2">
|
||||||
|
@ -10,15 +10,17 @@ import { getLogger } from "./tools/logger";
|
|||||||
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||||
const { keycloakVersion, destDirPath, isSilent } = params;
|
const { keycloakVersion, destDirPath, isSilent } = params;
|
||||||
|
|
||||||
for (const ext of ["", "-community"]) {
|
await Promise.all(
|
||||||
await downloadAndUnzip({
|
["", "-community"].map(ext =>
|
||||||
"destDirPath": destDirPath,
|
downloadAndUnzip({
|
||||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
"destDirPath": destDirPath,
|
||||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||||
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
||||||
isSilent
|
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
||||||
});
|
isSilent
|
||||||
}
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
|
@ -16,6 +16,7 @@ import { existsSync } from "fs";
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } 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 { getThemeSrcDirPath } from "./getThemeSrcDirPath";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const projectRootDir = getProjectRoot();
|
const projectRootDir = getProjectRoot();
|
||||||
@ -50,7 +51,13 @@ import { assert, Equals } from "tsafe/assert";
|
|||||||
|
|
||||||
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
||||||
|
|
||||||
const targetFilePath = pathJoin(process.cwd(), "src", "keycloak-theme", themeType, "pages", pageBasename);
|
const { themeSrcDirPath } = getThemeSrcDirPath();
|
||||||
|
|
||||||
|
if (themeSrcDirPath === undefined) {
|
||||||
|
throw new Error("Couldn't locate your theme sources");
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
|
||||||
|
|
||||||
if (existsSync(targetFilePath)) {
|
if (existsSync(targetFilePath)) {
|
||||||
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||||
|
33
src/bin/getThemeSrcDirPath.ts
Normal file
33
src/bin/getThemeSrcDirPath.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { crawl } from "./tools/crawl";
|
||||||
|
import { exclude } from "tsafe/exclude";
|
||||||
|
|
||||||
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
|
const themeSrcDirBasename = "keycloak-theme";
|
||||||
|
|
||||||
|
export function getThemeSrcDirPath() {
|
||||||
|
const srcDirPath = pathJoin(reactProjectDirPath, "src");
|
||||||
|
|
||||||
|
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
|
||||||
|
.map(fileRelativePath => {
|
||||||
|
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||||
|
|
||||||
|
if (split.length !== 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||||
|
})
|
||||||
|
.filter(exclude(undefined))[0];
|
||||||
|
|
||||||
|
if (themeSrcDirPath === undefined) {
|
||||||
|
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
|
||||||
|
return { "themeSrcDirPath": srcDirPath };
|
||||||
|
}
|
||||||
|
return { "themeSrcDirPath": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { themeSrcDirPath };
|
||||||
|
}
|
@ -1,27 +1,43 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||||
import { keycloakThemeEmailDirPath } from "./keycloakify";
|
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { getCliOptions } from "./tools/cliOptions";
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
import { getLogger } from "./tools/logger";
|
import { getLogger } from "./tools/logger";
|
||||||
|
import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
|
||||||
|
|
||||||
(async () => {
|
export function getEmailThemeSrcDirPath() {
|
||||||
|
const { themeSrcDirPath } = getThemeSrcDirPath();
|
||||||
|
|
||||||
|
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
|
||||||
|
|
||||||
|
return { emailThemeSrcDirPath };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
const { isSilent } = getCliOptions(process.argv.slice(2));
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
const logger = getLogger({ isSilent });
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
|
||||||
logger.warn(`There is already a ${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
|
|
||||||
|
if (emailThemeSrcDirPath === undefined) {
|
||||||
|
logger.warn("Couldn't locate your theme source directory");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||||
|
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
|
||||||
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion();
|
const { keycloakVersion } = await promptKeycloakVersion();
|
||||||
|
|
||||||
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||||
|
|
||||||
await downloadBuiltinKeycloakTheme({
|
await downloadBuiltinKeycloakTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
@ -31,18 +47,20 @@ import { getLogger } from "./tools/logger";
|
|||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||||
"destDirPath": keycloakThemeEmailDirPath
|
"destDirPath": emailThemeSrcDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
{
|
||||||
const themePropertyFilePath = pathJoin(keycloakThemeEmailDirPath, "theme.properties");
|
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||||
|
|
||||||
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log(
|
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
|
||||||
`${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} ready to be customized, feel free to remove every file you do not customize`
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main();
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ type ParsedPackageJson = {
|
|||||||
artifactId?: string;
|
artifactId?: string;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
bundler?: Bundler;
|
bundler?: Bundler;
|
||||||
|
keycloakVersionDefaultAssets?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -38,7 +39,8 @@ const zParsedPackageJson = z.object({
|
|||||||
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
||||||
"artifactId": z.string().optional(),
|
"artifactId": z.string().optional(),
|
||||||
"groupId": z.string().optional(),
|
"groupId": z.string().optional(),
|
||||||
"bundler": z.enum(bundlers).optional()
|
"bundler": z.enum(bundlers).optional(),
|
||||||
|
"keycloakVersionDefaultAssets": z.string().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
@ -59,6 +61,7 @@ export namespace BuildOptions {
|
|||||||
groupId: string;
|
groupId: string;
|
||||||
artifactId: string;
|
artifactId: string;
|
||||||
bundler: Bundler;
|
bundler: Bundler;
|
||||||
|
keycloakVersionDefaultAssets: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Standalone = Common & {
|
export type Standalone = Common & {
|
||||||
@ -125,7 +128,8 @@ export function readBuildOptions(params: {
|
|||||||
const common: BuildOptions.Common = (() => {
|
const common: BuildOptions.Common = (() => {
|
||||||
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {};
|
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } =
|
||||||
|
keycloakify ?? {};
|
||||||
|
|
||||||
const themeName = name
|
const themeName = name
|
||||||
.replace(/^@(.*)/, "$1")
|
.replace(/^@(.*)/, "$1")
|
||||||
@ -167,7 +171,8 @@ export function readBuildOptions(params: {
|
|||||||
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
|
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
|
||||||
extraAccountPages,
|
extraAccountPages,
|
||||||
extraThemeProperties,
|
extraThemeProperties,
|
||||||
isSilent
|
isSilent,
|
||||||
|
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3"
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -13,7 +13,8 @@
|
|||||||
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
|
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
|
||||||
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
|
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
|
||||||
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
|
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
|
||||||
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
|
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel",
|
||||||
|
"location", "occupation"
|
||||||
]>
|
]>
|
||||||
|
|
||||||
<#attempt>
|
<#attempt>
|
||||||
@ -110,14 +111,16 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
out["pageId"] = "${pageId}";
|
<#if account??>
|
||||||
|
out["url"]["getLogoutUrl"] = function () {
|
||||||
|
<#attempt>
|
||||||
|
return "${url.getLogoutUrl()}";
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
};
|
||||||
|
</#if>
|
||||||
|
|
||||||
out["url"]["getLogoutUrl"] = function () {
|
out["pageId"] = "${pageId}";
|
||||||
<#attempt>
|
|
||||||
return "${url.getLogoutUrl()}";
|
|
||||||
<#recover>
|
|
||||||
</#attempt>
|
|
||||||
};
|
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
|
|
||||||
@ -162,9 +165,9 @@
|
|||||||
key == "updateProfileCtx" &&
|
key == "updateProfileCtx" &&
|
||||||
are_same_path(path, [])
|
are_same_path(path, [])
|
||||||
) || (
|
) || (
|
||||||
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||||
<#-- https://github.com/InseeFrLab/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||||
<#-- https://github.com/InseeFrLab/keycloakify/issues/109#issuecomment-1134610163 -->
|
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||||
key == "loginAction" &&
|
key == "loginAction" &&
|
||||||
are_same_path(path, ["url"]) &&
|
are_same_path(path, ["url"]) &&
|
||||||
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
|
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
|
||||||
|
@ -35,7 +35,8 @@ export const loginThemePageIds = [
|
|||||||
"login-config-totp.ftl",
|
"login-config-totp.ftl",
|
||||||
"logout-confirm.ftl",
|
"logout-confirm.ftl",
|
||||||
"update-user-profile.ftl",
|
"update-user-profile.ftl",
|
||||||
"idp-review-user-profile.ftl"
|
"idp-review-user-profile.ftl",
|
||||||
|
"update-email.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
||||||
|
@ -10,7 +10,6 @@ import { isInside } from "../tools/isInside";
|
|||||||
import type { BuildOptions } from "./BuildOptions";
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { Reflect } from "tsafe/Reflect";
|
import { Reflect } from "tsafe/Reflect";
|
||||||
import { getLogger } from "../tools/logger";
|
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
@ -56,13 +55,11 @@ export namespace BuildOptionsLike {
|
|||||||
export async function generateKeycloakThemeResources(params: {
|
export async function generateKeycloakThemeResources(params: {
|
||||||
reactAppBuildDirPath: string;
|
reactAppBuildDirPath: string;
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
keycloakThemeEmailDirPath: string;
|
emailThemeSrcDirPath: string | undefined;
|
||||||
keycloakVersion: string;
|
keycloakVersion: string;
|
||||||
buildOptions: BuildOptionsLike;
|
buildOptions: BuildOptionsLike;
|
||||||
}): Promise<{ doBundlesEmailTemplate: boolean }> {
|
}): Promise<{ doBundlesEmailTemplate: boolean }> {
|
||||||
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, keycloakVersion, buildOptions } = params;
|
||||||
|
|
||||||
const logger = getLogger({ isSilent: buildOptions.isSilent });
|
|
||||||
|
|
||||||
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
||||||
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
||||||
@ -228,13 +225,7 @@ export async function generateKeycloakThemeResources(params: {
|
|||||||
let doBundlesEmailTemplate: boolean;
|
let doBundlesEmailTemplate: boolean;
|
||||||
|
|
||||||
email: {
|
email: {
|
||||||
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
if (emailThemeSrcDirPath === undefined) {
|
||||||
logger.log(
|
|
||||||
[
|
|
||||||
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
|
|
||||||
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
|
|
||||||
].join("\n")
|
|
||||||
);
|
|
||||||
doBundlesEmailTemplate = false;
|
doBundlesEmailTemplate = false;
|
||||||
break email;
|
break email;
|
||||||
}
|
}
|
||||||
@ -242,7 +233,7 @@ export async function generateKeycloakThemeResources(params: {
|
|||||||
doBundlesEmailTemplate = true;
|
doBundlesEmailTemplate = true;
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
"srcDirPath": keycloakThemeEmailDirPath,
|
"srcDirPath": emailThemeSrcDirPath,
|
||||||
"destDirPath": getThemeDirPath("email")
|
"destDirPath": getThemeDirPath("email")
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||||
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
|
||||||
import * as child_process from "child_process";
|
import * as child_process from "child_process";
|
||||||
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
@ -9,12 +9,12 @@ import { getLogger } from "../tools/logger";
|
|||||||
import { getCliOptions } from "../tools/cliOptions";
|
import { getCliOptions } from "../tools/cliOptions";
|
||||||
import jar from "../tools/jar";
|
import jar from "../tools/jar";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { Equals } from "tsafe";
|
import { Equals } from "tsafe";
|
||||||
|
import { getEmailThemeSrcDirPath } from "../initialize-email-theme";
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||||
export const keycloakThemeEmailDirPath = pathJoin(reactProjectDirPath, "src", "keycloak-theme", "email");
|
|
||||||
|
|
||||||
export async function main() {
|
export async function main() {
|
||||||
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
||||||
@ -38,13 +38,18 @@ export async function main() {
|
|||||||
|
|
||||||
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
|
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
keycloakThemeEmailDirPath,
|
"emailThemeSrcDirPath": (() => {
|
||||||
|
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
|
||||||
|
|
||||||
|
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return emailThemeSrcDirPath;
|
||||||
|
})(),
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||||
buildOptions,
|
buildOptions,
|
||||||
//We have to leave it at that otherwise we break our default theme.
|
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets
|
||||||
//Problem is that we can't guarantee that the the old resources
|
|
||||||
//will still be available on the newer keycloak version.
|
|
||||||
"keycloakVersion": "11.0.3"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
@ -121,19 +126,31 @@ export async function main() {
|
|||||||
"",
|
"",
|
||||||
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
|
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
|
||||||
"",
|
"",
|
||||||
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename))} 👈`,
|
`👉 $ .${pathSep}${pathRelative(
|
||||||
|
reactProjectDirPath,
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename)
|
||||||
|
)} 👈`,
|
||||||
"",
|
"",
|
||||||
"Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags",
|
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
|
||||||
"",
|
``,
|
||||||
"Once your container is up and running: ",
|
`Once your container is up and running: `,
|
||||||
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||||
'- Create a realm named "myrealm"',
|
`- Create a realm: myrealm`,
|
||||||
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
|
||||||
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
|
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
|
||||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
`- Create a client id myclient`,
|
||||||
"",
|
` Root URL: https://www.keycloak.org/app/`,
|
||||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
|
||||||
""
|
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
|
||||||
|
` Web origins: *`,
|
||||||
|
` Login Theme: ${buildOptions.themeName}`,
|
||||||
|
` Save (button at the bottom of the page)`,
|
||||||
|
``,
|
||||||
|
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||||
|
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,
|
||||||
|
``,
|
||||||
|
`Video tutorial: https://youtu.be/WMyGZNHQkjU`,
|
||||||
|
``
|
||||||
].join("\n")
|
].join("\n")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ export default function tee(input: Readable) {
|
|||||||
let aFull = false;
|
let aFull = false;
|
||||||
let bFull = false;
|
let bFull = false;
|
||||||
|
|
||||||
|
a.setMaxListeners(Infinity);
|
||||||
|
|
||||||
a.on("drain", () => {
|
a.on("drain", () => {
|
||||||
aFull = false;
|
aFull = false;
|
||||||
if (!aFull && !bFull) input.resume();
|
if (!aFull && !bFull) input.resume();
|
||||||
|
@ -25,6 +25,7 @@ const LoginConfigTotp = lazy(() => import("keycloakify/login/pages/LoginConfigTo
|
|||||||
const LogoutConfirm = lazy(() => import("keycloakify/login/pages/LogoutConfirm"));
|
const LogoutConfirm = lazy(() => import("keycloakify/login/pages/LogoutConfirm"));
|
||||||
const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile"));
|
const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile"));
|
||||||
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
|
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
|
||||||
|
const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
|
||||||
|
|
||||||
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||||
const { kcContext, ...rest } = props;
|
const { kcContext, ...rest } = props;
|
||||||
@ -75,6 +76,8 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
|
|||||||
return <UpdateUserProfile kcContext={kcContext} {...rest} />;
|
return <UpdateUserProfile kcContext={kcContext} {...rest} />;
|
||||||
case "idp-review-user-profile.ftl":
|
case "idp-review-user-profile.ftl":
|
||||||
return <IdpReviewUserProfile kcContext={kcContext} {...rest} />;
|
return <IdpReviewUserProfile kcContext={kcContext} {...rest} />;
|
||||||
|
case "update-email.ftl":
|
||||||
|
return <UpdateEmail kcContext={kcContext} {...rest} />;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof kcContext, never>>(false);
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
})()}
|
})()}
|
||||||
|
@ -30,7 +30,8 @@ export type KcContext =
|
|||||||
| KcContext.LoginConfigTotp
|
| KcContext.LoginConfigTotp
|
||||||
| KcContext.LogoutConfirm
|
| KcContext.LogoutConfirm
|
||||||
| KcContext.UpdateUserProfile
|
| KcContext.UpdateUserProfile
|
||||||
| KcContext.IdpReviewUserProfile;
|
| KcContext.IdpReviewUserProfile
|
||||||
|
| KcContext.UpdateEmail;
|
||||||
|
|
||||||
export declare namespace KcContext {
|
export declare namespace KcContext {
|
||||||
export type Common = {
|
export type Common = {
|
||||||
@ -101,7 +102,8 @@ export declare namespace KcContext {
|
|||||||
registrationDisabled: boolean;
|
registrationDisabled: boolean;
|
||||||
login: {
|
login: {
|
||||||
username?: string;
|
username?: string;
|
||||||
rememberMe?: boolean;
|
rememberMe?: string;
|
||||||
|
password?: string;
|
||||||
};
|
};
|
||||||
usernameEditDisabled: boolean;
|
usernameEditDisabled: boolean;
|
||||||
social: {
|
social: {
|
||||||
@ -182,6 +184,9 @@ export declare namespace KcContext {
|
|||||||
realm: {
|
realm: {
|
||||||
loginWithEmailAllowed: boolean;
|
loginWithEmailAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
url: {
|
||||||
|
loginResetCredentialsUrl: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type LoginVerifyEmail = Common & {
|
export type LoginVerifyEmail = Common & {
|
||||||
@ -219,7 +224,7 @@ export declare namespace KcContext {
|
|||||||
registrationDisabled: boolean;
|
registrationDisabled: boolean;
|
||||||
login: {
|
login: {
|
||||||
username?: string;
|
username?: string;
|
||||||
rememberMe?: boolean;
|
rememberMe?: string;
|
||||||
};
|
};
|
||||||
usernameHidden?: boolean;
|
usernameHidden?: boolean;
|
||||||
social: {
|
social: {
|
||||||
@ -377,6 +382,13 @@ export declare namespace KcContext {
|
|||||||
attributesByName: Record<string, Attribute>;
|
attributesByName: Record<string, Attribute>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateEmail = Common & {
|
||||||
|
pageId: "update-email.ftl";
|
||||||
|
email: {
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Attribute = {
|
export type Attribute = {
|
||||||
|
@ -121,6 +121,10 @@ export function getKcContext<KcContextExtension extends { pageId: string } = nev
|
|||||||
return { "kcContext": undefined };
|
return { "kcContext": undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!("login" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const { url } = realKcContext;
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
@ -260,9 +260,7 @@ export const kcContextMocks: KcContext[] = [
|
|||||||
"displayInfo": true
|
"displayInfo": true
|
||||||
},
|
},
|
||||||
"usernameEditDisabled": false,
|
"usernameEditDisabled": false,
|
||||||
"login": {
|
"login": {},
|
||||||
"rememberMe": false
|
|
||||||
},
|
|
||||||
"registrationDisabled": false
|
"registrationDisabled": false
|
||||||
}),
|
}),
|
||||||
...(() => {
|
...(() => {
|
||||||
@ -331,7 +329,8 @@ export const kcContextMocks: KcContext[] = [
|
|||||||
"realm": {
|
"realm": {
|
||||||
...kcContextCommonMock.realm,
|
...kcContextCommonMock.realm,
|
||||||
"loginWithEmailAllowed": false
|
"loginWithEmailAllowed": false
|
||||||
}
|
},
|
||||||
|
url: loginUrl
|
||||||
}),
|
}),
|
||||||
id<KcContext.LoginVerifyEmail>({
|
id<KcContext.LoginVerifyEmail>({
|
||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
@ -376,9 +375,7 @@ export const kcContextMocks: KcContext[] = [
|
|||||||
"displayInfo": true
|
"displayInfo": true
|
||||||
},
|
},
|
||||||
"usernameHidden": false,
|
"usernameHidden": false,
|
||||||
"login": {
|
"login": {},
|
||||||
"rememberMe": false
|
|
||||||
},
|
|
||||||
"registrationDisabled": false
|
"registrationDisabled": false
|
||||||
}),
|
}),
|
||||||
id<KcContext.LoginPassword>({
|
id<KcContext.LoginPassword>({
|
||||||
@ -494,5 +491,12 @@ export const kcContextMocks: KcContext[] = [
|
|||||||
attributes,
|
attributes,
|
||||||
attributesByName
|
attributesByName
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
id<KcContext.UpdateEmail>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "update-email.ftl",
|
||||||
|
"email": {
|
||||||
|
value: "email@example.com"
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { UserProfileFormFields } from "keycloakify/login/pages/shared/UserProfileCommons";
|
import { UserProfileFormFields } from "keycloakify/login/pages/shared/UserProfileFormFields";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../kcContext";
|
||||||
|
@ -124,7 +124,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
|||||||
id="rememberMe"
|
id="rememberMe"
|
||||||
name="rememberMe"
|
name="rememberMe"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...(login.rememberMe
|
{...(login.rememberMe === "on"
|
||||||
? {
|
? {
|
||||||
"checked": true
|
"checked": true
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
|||||||
id="rememberMe"
|
id="rememberMe"
|
||||||
name="rememberMe"
|
name="rememberMe"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
{...(login.rememberMe
|
{...(login.rememberMe === "on"
|
||||||
? {
|
? {
|
||||||
"checked": true
|
"checked": true
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
import { UserProfileFormFields } from "./shared/UserProfileFormFields";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../kcContext";
|
||||||
|
88
src/login/pages/UpdateEmail.tsx
Normal file
88
src/login/pages/UpdateEmail.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function UpdateEmail(props: PageProps<Extract<KcContext, { pageId: "update-email.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url, messagesPerField, isAppInitiatedAction, email } = kcContext;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("updateEmailTitle")}>
|
||||||
|
<form id="kc-update-email-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
|
||||||
|
<div
|
||||||
|
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
|
||||||
|
>
|
||||||
|
<div className={getClassName("kcLabelWrapperClass")}>
|
||||||
|
<label htmlFor="email" className={getClassName("kcLabelClass")}>
|
||||||
|
{msg("email")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={getClassName("kcInputWrapperClass")}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
defaultValue={email.value ?? ""}
|
||||||
|
className={getClassName("kcInputClass")}
|
||||||
|
aria-invalid={messagesPerField.existsError("email")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={getClassName("kcFormGroupClass")}>
|
||||||
|
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
|
||||||
|
<div className={getClassName("kcFormOptionsWrapperClass")}></div>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonDefaultClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonBlockClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { UserProfileFormFields } from "keycloakify/login/pages/shared/UserProfileCommons";
|
import { UserProfileFormFields } from "keycloakify/login/pages/shared/UserProfileFormFields";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
|
||||||
import type { KcContext } from "../kcContext";
|
import type { KcContext } from "../kcContext";
|
||||||
|
@ -10,7 +10,11 @@ export declare namespace AndByDiscriminatingKey {
|
|||||||
U1,
|
U1,
|
||||||
U1Again extends Record<DiscriminatingKey, string>,
|
U1Again extends Record<DiscriminatingKey, string>,
|
||||||
U2 extends Record<DiscriminatingKey, string>
|
U2 extends Record<DiscriminatingKey, string>
|
||||||
> = U1 extends Pick<U2, DiscriminatingKey> ? Tf2<DiscriminatingKey, U1, U2, U1Again> : U1;
|
> = U1 extends Pick<U2, DiscriminatingKey>
|
||||||
|
? Tf2<DiscriminatingKey, U1, U2, U1Again>
|
||||||
|
: U1Again[DiscriminatingKey] & U2[DiscriminatingKey] extends never
|
||||||
|
? U1 | U2
|
||||||
|
: U1;
|
||||||
|
|
||||||
export type Tf2<
|
export type Tf2<
|
||||||
DiscriminatingKey extends string,
|
DiscriminatingKey extends string,
|
||||||
|
@ -7,7 +7,7 @@ setupSampleReactProject();
|
|||||||
generateKeycloakThemeResources({
|
generateKeycloakThemeResources({
|
||||||
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
|
||||||
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
|
||||||
"keycloakThemeEmailDirPath": pathJoin(sampleReactProjectDirPath, "keycloak_email"),
|
"emailThemeSrcDirPath": undefined,
|
||||||
"keycloakVersion": "11.0.3",
|
"keycloakVersion": "11.0.3",
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
"themeName": "keycloakify-demo-app",
|
"themeName": "keycloakify-demo-app",
|
||||||
|
@ -6,7 +6,7 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
|
|||||||
|
|
||||||
export async function setupSampleReactProject() {
|
export async function setupSampleReactProject() {
|
||||||
await downloadAndUnzip({
|
await downloadAndUnzip({
|
||||||
"url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
"url": "https://github.com/keycloakify/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||||
"destDirPath": sampleReactProjectDirPath,
|
"destDirPath": sampleReactProjectDirPath,
|
||||||
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
|
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
|
||||||
"isSilent": false
|
"isSilent": false
|
||||||
|
@ -2,72 +2,90 @@ import { AndByDiscriminatingKey } from "../../../src/tools/AndByDiscriminatingKe
|
|||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { Equals } from "tsafe";
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
type Base = { pageId: "a"; onlyA: string } | { pageId: "b"; onlyB: string } | { pageId: "only base"; onlyBase: string };
|
{
|
||||||
|
type Base = { pageId: "a"; onlyA: string } | { pageId: "b"; onlyB: string } | { pageId: "only base"; onlyBase: string };
|
||||||
|
|
||||||
type Extension = { pageId: "a"; onlyExtA: string } | { pageId: "b"; onlyExtB: string } | { pageId: "only ext"; onlyExt: string };
|
type Extension = { pageId: "a"; onlyExtA: string } | { pageId: "b"; onlyExtB: string } | { pageId: "only ext"; onlyExt: string };
|
||||||
|
|
||||||
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
||||||
|
|
||||||
type Expected =
|
type Expected =
|
||||||
| { pageId: "a"; onlyA: string; onlyExtA: string }
|
| { pageId: "a"; onlyA: string; onlyExtA: string }
|
||||||
| { pageId: "b"; onlyB: string; onlyExtB: string }
|
| { pageId: "b"; onlyB: string; onlyExtB: string }
|
||||||
| { pageId: "only base"; onlyBase: string }
|
| { pageId: "only base"; onlyBase: string }
|
||||||
| { pageId: "only ext"; onlyExt: string };
|
| { pageId: "only ext"; onlyExt: string };
|
||||||
|
|
||||||
assert<Equals<Got, Expected>>();
|
assert<Equals<Got, Expected>>();
|
||||||
|
|
||||||
const x: Got = null as any;
|
const x: Got = null as any;
|
||||||
|
|
||||||
if (x.pageId === "a") {
|
if (x.pageId === "a") {
|
||||||
x.onlyA;
|
x.onlyA;
|
||||||
x.onlyExtA;
|
x.onlyExtA;
|
||||||
|
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
x.onlyB;
|
x.onlyB;
|
||||||
|
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
x.onlyBase;
|
x.onlyBase;
|
||||||
|
|
||||||
//@ts-expect-error
|
//@ts-expect-error
|
||||||
x.onlyExt;
|
x.onlyExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "b") {
|
||||||
|
x.onlyB;
|
||||||
|
x.onlyExtB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only base") {
|
||||||
|
x.onlyBase;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x.pageId === "only ext") {
|
||||||
|
x.onlyExt;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyA;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyB;
|
||||||
|
|
||||||
|
//@ts-expect-error
|
||||||
|
x.onlyBase;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (x.pageId === "b") {
|
{
|
||||||
x.onlyB;
|
type Base = { pageId: "a"; onlyA: string } | { pageId: "b"; onlyB: string } | { pageId: "only base"; onlyBase: string };
|
||||||
x.onlyExtB;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
type Extension = { pageId: "only ext"; onlyExt: string };
|
||||||
x.onlyA;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
||||||
x.onlyBase;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
type Expected =
|
||||||
x.onlyExt;
|
| { pageId: "a"; onlyA: string }
|
||||||
}
|
| { pageId: "b"; onlyB: string }
|
||||||
|
| { pageId: "only base"; onlyBase: string }
|
||||||
if (x.pageId === "only base") {
|
| { pageId: "only ext"; onlyExt: string };
|
||||||
x.onlyBase;
|
|
||||||
|
assert<Equals<Got, Expected>>();
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyA;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyB;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyExt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (x.pageId === "only ext") {
|
|
||||||
x.onlyExt;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyA;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyB;
|
|
||||||
|
|
||||||
//@ts-expect-error
|
|
||||||
x.onlyBase;
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user