diff --git a/src/bin/download-builtin-keycloak-theme.ts b/src/bin/download-builtin-keycloak-theme.ts
index e1e1515f..62ea29bc 100644
--- a/src/bin/download-builtin-keycloak-theme.ts
+++ b/src/bin/download-builtin-keycloak-theme.ts
@@ -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) {
diff --git a/src/bin/eject-keycloak-page.ts b/src/bin/eject-keycloak-page.ts
index 28f76709..1d221f92 100644
--- a/src/bin/eject-keycloak-page.ts
+++ b/src/bin/eject-keycloak-page.ts
@@ -16,6 +16,7 @@ import { existsSync } from "fs";
import { join as pathJoin, relative as pathRelative } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
+import { getThemeSrcDirPath } from "./getThemeSrcDirPath";
(async () => {
const projectRootDir = getProjectRoot();
@@ -50,7 +51,13 @@ import { assert, Equals } from "tsafe/assert";
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)) {
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
diff --git a/src/bin/getThemeSrcDirPath.ts b/src/bin/getThemeSrcDirPath.ts
new file mode 100644
index 00000000..ee9db95b
--- /dev/null
+++ b/src/bin/getThemeSrcDirPath.ts
@@ -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 };
+}
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index 7086d5f8..44207026 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -1,27 +1,43 @@
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
-import { keycloakThemeEmailDirPath } from "./keycloakify";
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
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 logger = getLogger({ isSilent });
- if (fs.existsSync(keycloakThemeEmailDirPath)) {
- logger.warn(`There is already a ${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
+ const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
+
+ 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);
}
const { keycloakVersion } = await promptKeycloakVersion();
- const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
+ const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
await downloadBuiltinKeycloakTheme({
keycloakVersion,
@@ -31,18 +47,20 @@ import { getLogger } from "./tools/logger";
transformCodebase({
"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"));
}
- logger.log(
- `${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} ready to be customized, feel free to remove every file you do not customize`
- );
+ logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
-})();
+}
+
+if (require.main === module) {
+ main();
+}
diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts
index 9a333277..6450f766 100644
--- a/src/bin/keycloakify/BuildOptions.ts
+++ b/src/bin/keycloakify/BuildOptions.ts
@@ -22,6 +22,7 @@ type ParsedPackageJson = {
artifactId?: string;
groupId?: string;
bundler?: Bundler;
+ keycloakVersionDefaultAssets?: string;
};
};
@@ -38,7 +39,8 @@ const zParsedPackageJson = z.object({
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
"artifactId": z.string().optional(),
"groupId": z.string().optional(),
- "bundler": z.enum(bundlers).optional()
+ "bundler": z.enum(bundlers).optional(),
+ "keycloakVersionDefaultAssets": z.string().optional()
})
.optional()
});
@@ -59,6 +61,7 @@ export namespace BuildOptions {
groupId: string;
artifactId: string;
bundler: Bundler;
+ keycloakVersionDefaultAssets: string;
};
export type Standalone = Common & {
@@ -125,7 +128,8 @@ export function readBuildOptions(params: {
const common: BuildOptions.Common = (() => {
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
.replace(/^@(.*)/, "$1")
@@ -167,7 +171,8 @@ export function readBuildOptions(params: {
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
extraAccountPages,
extraThemeProperties,
- isSilent
+ isSilent,
+ "keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3"
};
})();
diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
index 192bb648..30092d1b 100644
--- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
+++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
@@ -13,7 +13,8 @@
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
- "isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
+ "isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel",
+ "location", "occupation"
]>
<#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 () {
- <#attempt>
- return "${url.getLogoutUrl()}";
- <#recover>
- #attempt>
- };
+ out["pageId"] = "${pageId}";
return out;
@@ -162,9 +165,9 @@
key == "updateProfileCtx" &&
are_same_path(path, [])
) || (
- <#-- https://github.com/InseeFrLab/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/InseeFrLab/keycloakify/issues/109#issuecomment-1134610163 -->
+ <#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
+ <#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
+ <#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
key == "loginAction" &&
are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts
index 0f5dd1ff..f13f5cd7 100644
--- a/src/bin/keycloakify/generateFtl/generateFtl.ts
+++ b/src/bin/keycloakify/generateFtl/generateFtl.ts
@@ -35,7 +35,8 @@ export const loginThemePageIds = [
"login-config-totp.ftl",
"logout-confirm.ftl",
"update-user-profile.ftl",
- "idp-review-user-profile.ftl"
+ "idp-review-user-profile.ftl",
+ "update-email.ftl"
] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
diff --git a/src/bin/keycloakify/generateKeycloakThemeResources.ts b/src/bin/keycloakify/generateKeycloakThemeResources.ts
index 5095496b..60f330c4 100644
--- a/src/bin/keycloakify/generateKeycloakThemeResources.ts
+++ b/src/bin/keycloakify/generateKeycloakThemeResources.ts
@@ -10,7 +10,6 @@ import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
-import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@@ -56,13 +55,11 @@ export namespace BuildOptionsLike {
export async function generateKeycloakThemeResources(params: {
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
- keycloakThemeEmailDirPath: string;
+ emailThemeSrcDirPath: string | undefined;
keycloakVersion: string;
buildOptions: BuildOptionsLike;
}): Promise<{ doBundlesEmailTemplate: boolean }> {
- const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
-
- const logger = getLogger({ isSilent: buildOptions.isSilent });
+ const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, keycloakVersion, buildOptions } = params;
const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
@@ -228,13 +225,7 @@ export async function generateKeycloakThemeResources(params: {
let doBundlesEmailTemplate: boolean;
email: {
- if (!fs.existsSync(keycloakThemeEmailDirPath)) {
- 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")
- );
+ if (emailThemeSrcDirPath === undefined) {
doBundlesEmailTemplate = false;
break email;
}
@@ -242,7 +233,7 @@ export async function generateKeycloakThemeResources(params: {
doBundlesEmailTemplate = true;
transformCodebase({
- "srcDirPath": keycloakThemeEmailDirPath,
+ "srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email")
});
}
diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts
index 38ede8c9..7d2f6334 100644
--- a/src/bin/keycloakify/keycloakify.ts
+++ b/src/bin/keycloakify/keycloakify.ts
@@ -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";
@@ -9,12 +9,12 @@ import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
import jar from "../tools/jar";
import { assert } from "tsafe/assert";
-import type { Equals } from "tsafe";
+import { Equals } from "tsafe";
+import { getEmailThemeSrcDirPath } from "../initialize-email-theme";
const reactProjectDirPath = process.cwd();
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
-export const keycloakThemeEmailDirPath = pathJoin(reactProjectDirPath, "src", "keycloak-theme", "email");
export async function main() {
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
@@ -38,13 +38,18 @@ export async function main() {
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
- keycloakThemeEmailDirPath,
+ "emailThemeSrcDirPath": (() => {
+ const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath();
+
+ if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
+ return;
+ }
+
+ return emailThemeSrcDirPath;
+ })(),
"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
- //will still be available on the newer keycloak version.
- "keycloakVersion": "11.0.3"
+ "keycloakVersion": buildOptions.keycloakVersionDefaultAssets
});
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:`,
"",
- `👉 $ ./${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")
);
}
diff --git a/src/bin/tools/tee.ts b/src/bin/tools/tee.ts
index 5c53cb42..1bdb98e5 100644
--- a/src/bin/tools/tee.ts
+++ b/src/bin/tools/tee.ts
@@ -7,6 +7,8 @@ export default function tee(input: Readable) {
let aFull = false;
let bFull = false;
+ a.setMaxListeners(Infinity);
+
a.on("drain", () => {
aFull = false;
if (!aFull && !bFull) input.resume();
diff --git a/src/login/Fallback.tsx b/src/login/Fallback.tsx
index 80124987..413040c1 100644
--- a/src/login/Fallback.tsx
+++ b/src/login/Fallback.tsx
@@ -25,6 +25,7 @@ const LoginConfigTotp = lazy(() => import("keycloakify/login/pages/LoginConfigTo
const LogoutConfirm = lazy(() => import("keycloakify/login/pages/LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
+const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
export default function Fallback(props: PageProps
) {
const { kcContext, ...rest } = props;
@@ -75,6 +76,8 @@ export default function Fallback(props: PageProps) {
return ;
case "idp-review-user-profile.ftl":
return ;
+ case "update-email.ftl":
+ return ;
}
assert>(false);
})()}
diff --git a/src/login/kcContext/KcContext.ts b/src/login/kcContext/KcContext.ts
index 6cd40f73..475154f9 100644
--- a/src/login/kcContext/KcContext.ts
+++ b/src/login/kcContext/KcContext.ts
@@ -30,7 +30,8 @@ export type KcContext =
| KcContext.LoginConfigTotp
| KcContext.LogoutConfirm
| KcContext.UpdateUserProfile
- | KcContext.IdpReviewUserProfile;
+ | KcContext.IdpReviewUserProfile
+ | KcContext.UpdateEmail;
export declare namespace KcContext {
export type Common = {
@@ -101,7 +102,8 @@ export declare namespace KcContext {
registrationDisabled: boolean;
login: {
username?: string;
- rememberMe?: boolean;
+ rememberMe?: string;
+ password?: string;
};
usernameEditDisabled: boolean;
social: {
@@ -182,6 +184,9 @@ export declare namespace KcContext {
realm: {
loginWithEmailAllowed: boolean;
};
+ url: {
+ loginResetCredentialsUrl: string;
+ };
};
export type LoginVerifyEmail = Common & {
@@ -219,7 +224,7 @@ export declare namespace KcContext {
registrationDisabled: boolean;
login: {
username?: string;
- rememberMe?: boolean;
+ rememberMe?: string;
};
usernameHidden?: boolean;
social: {
@@ -377,6 +382,13 @@ export declare namespace KcContext {
attributesByName: Record;
};
};
+
+ export type UpdateEmail = Common & {
+ pageId: "update-email.ftl";
+ email: {
+ value?: string;
+ };
+ };
}
export type Attribute = {
diff --git a/src/login/kcContext/getKcContext.ts b/src/login/kcContext/getKcContext.ts
index 10b8c5b0..0bd877e5 100644
--- a/src/login/kcContext/getKcContext.ts
+++ b/src/login/kcContext/getKcContext.ts
@@ -121,6 +121,10 @@ export function getKcContext {
@@ -331,7 +329,8 @@ export const kcContextMocks: KcContext[] = [
"realm": {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": false
- }
+ },
+ url: loginUrl
}),
id({
...kcContextCommonMock,
@@ -376,9 +375,7 @@ export const kcContextMocks: KcContext[] = [
"displayInfo": true
},
"usernameHidden": false,
- "login": {
- "rememberMe": false
- },
+ "login": {},
"registrationDisabled": false
}),
id({
@@ -494,5 +491,12 @@ export const kcContextMocks: KcContext[] = [
attributes,
attributesByName
}
+ }),
+ id({
+ ...kcContextCommonMock,
+ "pageId": "update-email.ftl",
+ "email": {
+ value: "email@example.com"
+ }
})
];
diff --git a/src/login/pages/IdpReviewUserProfile.tsx b/src/login/pages/IdpReviewUserProfile.tsx
index a6d2fbdf..9afaa8ba 100644
--- a/src/login/pages/IdpReviewUserProfile.tsx
+++ b/src/login/pages/IdpReviewUserProfile.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
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 { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
diff --git a/src/login/pages/Login.tsx b/src/login/pages/Login.tsx
index c15af59d..f85049c3 100644
--- a/src/login/pages/Login.tsx
+++ b/src/login/pages/Login.tsx
@@ -124,7 +124,7 @@ export default function Login(props: PageProps, I18n>) {
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+
+ const { getClassName } = useGetClassName({
+ doUseDefaultCss,
+ classes
+ });
+
+ const { msg, msgStr } = i18n;
+
+ const { url, messagesPerField, isAppInitiatedAction, email } = kcContext;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/login/pages/UpdateUserProfile.tsx b/src/login/pages/UpdateUserProfile.tsx
index 18983f3f..1051022a 100644
--- a/src/login/pages/UpdateUserProfile.tsx
+++ b/src/login/pages/UpdateUserProfile.tsx
@@ -1,6 +1,6 @@
import { useState } from "react";
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 { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
diff --git a/src/login/pages/shared/UserProfileCommons.tsx b/src/login/pages/shared/UserProfileFormFields.tsx
similarity index 100%
rename from src/login/pages/shared/UserProfileCommons.tsx
rename to src/login/pages/shared/UserProfileFormFields.tsx
diff --git a/src/tools/AndByDiscriminatingKey.ts b/src/tools/AndByDiscriminatingKey.ts
index 637ab87d..630c6d13 100644
--- a/src/tools/AndByDiscriminatingKey.ts
+++ b/src/tools/AndByDiscriminatingKey.ts
@@ -10,7 +10,11 @@ export declare namespace AndByDiscriminatingKey {
U1,
U1Again extends Record,
U2 extends Record
- > = U1 extends Pick ? Tf2 : U1;
+ > = U1 extends Pick
+ ? Tf2
+ : U1Again[DiscriminatingKey] & U2[DiscriminatingKey] extends never
+ ? U1 | U2
+ : U1;
export type Tf2<
DiscriminatingKey extends string,
diff --git a/test/bin/generateKeycloakThemeResources.ts b/test/bin/generateKeycloakThemeResources.ts
index c1bf5d78..f76e7bfd 100644
--- a/test/bin/generateKeycloakThemeResources.ts
+++ b/test/bin/generateKeycloakThemeResources.ts
@@ -7,7 +7,7 @@ setupSampleReactProject();
generateKeycloakThemeResources({
"reactAppBuildDirPath": pathJoin(sampleReactProjectDirPath, "build"),
"keycloakThemeBuildingDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak_theme"),
- "keycloakThemeEmailDirPath": pathJoin(sampleReactProjectDirPath, "keycloak_email"),
+ "emailThemeSrcDirPath": undefined,
"keycloakVersion": "11.0.3",
"buildOptions": {
"themeName": "keycloakify-demo-app",
diff --git a/test/bin/setupSampleReactProject.ts b/test/bin/setupSampleReactProject.ts
index 8ac1aada..b2ea1f00 100644
--- a/test/bin/setupSampleReactProject.ts
+++ b/test/bin/setupSampleReactProject.ts
@@ -6,7 +6,7 @@ export const sampleReactProjectDirPath = pathJoin(getProjectRoot(), "sample_reac
export async function setupSampleReactProject() {
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,
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
"isSilent": false
diff --git a/test/lib/tools/AndByDiscriminatingKey.type.ts b/test/lib/tools/AndByDiscriminatingKey.type.ts
index 919662dc..f45e897f 100644
--- a/test/lib/tools/AndByDiscriminatingKey.type.ts
+++ b/test/lib/tools/AndByDiscriminatingKey.type.ts
@@ -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>();
+ assert>();
-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>();
}