Compare commits
20 Commits
v7.0.0-rc.
...
v7.0.2
Author | SHA1 | Date | |
---|---|---|---|
87ebad7efb | |||
3294aaed3b | |||
0e21f3eab6 | |||
9fcf692cb8 | |||
da577ea3cc | |||
6ae1d8938a | |||
3e18a7390c | |||
5f43f1afc6 | |||
2fc9c03430 | |||
d951a9ba02 | |||
93385af675 | |||
dd75d0ece7 | |||
dcd37ed916 | |||
2e4d722d7f | |||
09543400ca | |||
8b101e5043 | |||
b31fff9c2b | |||
0c5b100dd9 | |||
8937d19891 | |||
0fdd9e75a6 |
@ -40,6 +40,12 @@
|
||||
|
||||
# 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.
|
||||
|
||||
## 6.13
|
||||
|
||||
- Build work behind corporate proxies, [see issue](https://github.com/InseeFrLab/keycloakify/issues/257).
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "7.0.0-rc.10",
|
||||
"version": "7.0.2",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -43,6 +43,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<li>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
</a>
|
||||
|
@ -64,6 +64,7 @@ export declare namespace KcContext {
|
||||
password: {
|
||||
passwordSet: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Account = Common & {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
@ -7,6 +6,7 @@ import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||
|
||||
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||
@ -62,6 +62,10 @@ export function getKcContext<KcContextExtension extends { pageId: string } = nev
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
if (!("account" in realKcContext)) {
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
{
|
||||
const { url } = realKcContext;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { KcContext } from "./KcContext";
|
||||
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||
? KcContext
|
||||
|
@ -1,8 +1,8 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import type { KcContext } from "./KcContext";
|
||||
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||
import { id } from "tsafe/id";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||
|
||||
@ -154,7 +154,8 @@ export const kcContextMocks: KcContext[] = [
|
||||
"pageId": "password.ftl",
|
||||
"password": {
|
||||
"passwordSet": true
|
||||
}
|
||||
},
|
||||
"stateChecker": "state checker"
|
||||
}),
|
||||
id<KcContext.Account>({
|
||||
...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;
|
||||
|
||||
@ -55,7 +55,7 @@ export default function LogoutConfirm(props: PageProps<Extract<KcContext, { page
|
||||
</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="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 }) {
|
||||
const { keycloakVersion, destDirPath, isSilent } = params;
|
||||
|
||||
for (const ext of ["", "-community"]) {
|
||||
await downloadAndUnzip({
|
||||
"destDirPath": destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
||||
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
||||
isSilent
|
||||
});
|
||||
}
|
||||
await Promise.all(
|
||||
["", "-community"].map(ext =>
|
||||
downloadAndUnzip({
|
||||
"destDirPath": destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
||||
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
||||
isSilent
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
|
@ -110,14 +110,16 @@
|
||||
}
|
||||
};
|
||||
|
||||
out["pageId"] = "${pageId}";
|
||||
<#if account??>
|
||||
out["url"]["getLogoutUrl"] = function () {
|
||||
<#attempt>
|
||||
return "${url.getLogoutUrl()}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
};
|
||||
</#if>
|
||||
|
||||
out["url"]["getLogoutUrl"] = function () {
|
||||
<#attempt>
|
||||
return "${url.getLogoutUrl()}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
};
|
||||
out["pageId"] = "${pageId}";
|
||||
|
||||
return out;
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||
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 { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||
import * as fs from "fs";
|
||||
@ -42,7 +42,7 @@ export async function main() {
|
||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||
buildOptions,
|
||||
//We have to leave it at that otherwise we break our default theme.
|
||||
//Problem is that we can't guarantee that the the old resources
|
||||
//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"
|
||||
});
|
||||
@ -121,19 +121,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:`,
|
||||
"",
|
||||
`👉 $ ./${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",
|
||||
"",
|
||||
"Once your container is up and running: ",
|
||||
`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: `,
|
||||
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||
'- Create a realm named "myrealm"',
|
||||
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
||||
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
|
||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||
"",
|
||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
||||
""
|
||||
`- Create a realm: myrealm`,
|
||||
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
|
||||
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
|
||||
`- Create a client id myclient`,
|
||||
` Root URL: https://www.keycloak.org/app/`,
|
||||
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
|
||||
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
|
||||
` Web origins: *`,
|
||||
` Login Theme: ${buildOptions.themeName}`,
|
||||
` Save (button at the bottom of the page)`,
|
||||
``,
|
||||
`- 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")
|
||||
);
|
||||
}
|
||||
|
@ -121,6 +121,10 @@ export function getKcContext<KcContextExtension extends { pageId: string } = nev
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
if (!("login" in realKcContext)) {
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
{
|
||||
const { url } = realKcContext;
|
||||
|
||||
|
@ -10,7 +10,11 @@ export declare namespace AndByDiscriminatingKey {
|
||||
U1,
|
||||
U1Again 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<
|
||||
DiscriminatingKey extends string,
|
||||
|
@ -2,72 +2,90 @@ import { AndByDiscriminatingKey } from "../../../src/tools/AndByDiscriminatingKe
|
||||
import { assert } from "tsafe/assert";
|
||||
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 =
|
||||
| { pageId: "a"; onlyA: string; onlyExtA: string }
|
||||
| { pageId: "b"; onlyB: string; onlyExtB: string }
|
||||
| { pageId: "only base"; onlyBase: string }
|
||||
| { pageId: "only ext"; onlyExt: string };
|
||||
type Expected =
|
||||
| { pageId: "a"; onlyA: string; onlyExtA: string }
|
||||
| { pageId: "b"; onlyB: string; onlyExtB: string }
|
||||
| { pageId: "only base"; onlyBase: 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") {
|
||||
x.onlyA;
|
||||
x.onlyExtA;
|
||||
if (x.pageId === "a") {
|
||||
x.onlyA;
|
||||
x.onlyExtA;
|
||||
|
||||
//@ts-expect-error
|
||||
x.onlyB;
|
||||
//@ts-expect-error
|
||||
x.onlyB;
|
||||
|
||||
//@ts-expect-error
|
||||
x.onlyBase;
|
||||
//@ts-expect-error
|
||||
x.onlyBase;
|
||||
|
||||
//@ts-expect-error
|
||||
x.onlyExt;
|
||||
//@ts-expect-error
|
||||
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;
|
||||
x.onlyExtB;
|
||||
{
|
||||
type Base = { pageId: "a"; onlyA: string } | { pageId: "b"; onlyB: string } | { pageId: "only base"; onlyBase: string };
|
||||
|
||||
//@ts-expect-error
|
||||
x.onlyA;
|
||||
type Extension = { pageId: "only ext"; onlyExt: string };
|
||||
|
||||
//@ts-expect-error
|
||||
x.onlyBase;
|
||||
type Got = AndByDiscriminatingKey<"pageId", Extension, Base>;
|
||||
|
||||
//@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;
|
||||
type Expected =
|
||||
| { pageId: "a"; onlyA: string }
|
||||
| { pageId: "b"; onlyB: string }
|
||||
| { pageId: "only base"; onlyBase: string }
|
||||
| { pageId: "only ext"; onlyExt: string };
|
||||
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
Reference in New Issue
Block a user