Compare commits
52 Commits
Author | SHA1 | Date | |
---|---|---|---|
947efe8d63 | |||
64189bf8fe | |||
400c630418 | |||
402360b436 | |||
9f001f1521 | |||
368e3a32c5 | |||
002e3d4b3d | |||
f94f9b51c9 | |||
055b15bd46 | |||
0e70b0b0de | |||
8faf9a3eed | |||
075d9f9de5 | |||
840079be32 | |||
50ae962f09 | |||
61aa1f9896 | |||
d88e0e4dd5 | |||
18c36eb4de | |||
80aeabad51 | |||
419e1f473a | |||
80988125e8 | |||
271ad2da71 | |||
b2732f2595 | |||
53820e1e34 | |||
09dd45e437 | |||
1f654a7820 | |||
0690f40bad | |||
2285883149 | |||
af87e41bb8 | |||
9ba884483d | |||
f5a300953a | |||
ab9a962f58 | |||
484adb607f | |||
e1f38d4196 | |||
5de629acf2 | |||
8b4b24a036 | |||
75ab130249 | |||
981ca7e9a4 | |||
acb4e260a7 | |||
ff20b0a844 | |||
1b77c69a01 | |||
158275f5c2 | |||
a085c8093e | |||
cb358bd745 | |||
e788c46601 | |||
d551b4bffb | |||
c168c7b156 | |||
7a46115042 | |||
249a7bde89 | |||
813740a002 | |||
7840c2a6f5 | |||
8f6c0d36d9 | |||
12690b892b |
@ -327,6 +327,24 @@
|
|||||||
"contributions": [
|
"contributions": [
|
||||||
"doc"
|
"doc"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "EternalSide",
|
||||||
|
"name": "Lesha",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/118743608?v=4",
|
||||||
|
"profile": "http://t.me/AAT_L",
|
||||||
|
"contributions": [
|
||||||
|
"code"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"login": "bacongobbler",
|
||||||
|
"name": "Matthew Fisher",
|
||||||
|
"avatar_url": "https://avatars.githubusercontent.com/u/1360539?v=4",
|
||||||
|
"profile": "https://blog.bacongobbler.com",
|
||||||
|
"contributions": [
|
||||||
|
"doc"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"contributorsPerLine": 7,
|
"contributorsPerLine": 7,
|
||||||
|
@ -168,6 +168,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zvn2060"><img src="https://avatars.githubusercontent.com/u/45450852?v=4?s=100" width="100px;" alt="HI_OuO"/><br /><sub><b>HI_OuO</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=zvn2060" title="Code">💻</a></td>
|
||||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tripheo0412"><img src="https://avatars.githubusercontent.com/u/25382052?v=4?s=100" width="100px;" alt="Tri Hoang"/><br /><sub><b>Tri Hoang</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=tripheo0412" title="Documentation">📖</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="http://t.me/AAT_L"><img src="https://avatars.githubusercontent.com/u/118743608?v=4?s=100" width="100px;" alt="Lesha"/><br /><sub><b>Lesha</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=EternalSide" title="Code">💻</a></td>
|
||||||
|
<td align="center" valign="top" width="14.28%"><a href="https://blog.bacongobbler.com"><img src="https://avatars.githubusercontent.com/u/1360539?v=4?s=100" width="100px;" alt="Matthew Fisher"/><br /><sub><b>Matthew Fisher</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=bacongobbler" title="Documentation">📖</a></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "11.8.2",
|
"version": "11.8.22",
|
||||||
"description": "Framework to create custom Keycloak UIs",
|
"description": "Framework to create custom Keycloak UIs",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -280,6 +280,24 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
"fonts",
|
"fonts",
|
||||||
"OpenSans-Semibold-webfont.woff2"
|
"OpenSans-Semibold-webfont.woff2"
|
||||||
),
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff2"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.woff"
|
||||||
|
),
|
||||||
|
pathJoin(
|
||||||
|
"patternfly",
|
||||||
|
"dist",
|
||||||
|
"fonts",
|
||||||
|
"OpenSans-SemiboldItalic-webfont.ttf"
|
||||||
|
),
|
||||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||||
pathJoin("jquery", "dist", "jquery.min.js"),
|
pathJoin("jquery", "dist", "jquery.min.js"),
|
||||||
pathJoin("rfc4648", "lib", "rfc4648.js")
|
pathJoin("rfc4648", "lib", "rfc4648.js")
|
||||||
|
@ -20,7 +20,7 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "add-story",
|
commandName: "add-story",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
@ -74,7 +74,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
if (themeType === "admin") {
|
if (themeType === "admin") {
|
||||||
console.log(
|
console.log(
|
||||||
`${chalk.red("✗")} Sorry, there is no Storybook support for the Account UI.`
|
`${chalk.red("✗")} Sorry, there is no Storybook support for the Admin UI.`
|
||||||
);
|
);
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
@ -11,7 +11,7 @@ import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "copy-keycloak-resources-to-public",
|
commandName: "copy-keycloak-resources-to-public",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -22,7 +22,7 @@ import { runPrettier, getIsPrettierAvailable } from "./tools/runPrettier";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "eject-page",
|
commandName: "eject-page",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,7 @@ import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "initialize-account-theme",
|
commandName: "initialize-account-theme",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -7,7 +7,7 @@ import { command as updateKcGenCommand } from "./update-kc-gen";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "initialize-admin-theme",
|
commandName: "initialize-admin-theme",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -17,8 +17,8 @@ import chalk from "chalk";
|
|||||||
export async function command(params: { buildContext: BuildContext }) {
|
export async function command(params: { buildContext: BuildContext }) {
|
||||||
const { buildContext } = params;
|
const { buildContext } = params;
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "initialize-account-theme",
|
commandName: "initialize-email-theme",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -49,12 +49,15 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { value: emailThemeType } = await cliSelect({
|
const { value: emailThemeType } = await cliSelect({
|
||||||
values: ["native (FreeMarker)" as const, "jsx-email (React)" as const]
|
values: [
|
||||||
|
"native (FreeMarker)" as const,
|
||||||
|
"Another email templating solution" as const
|
||||||
|
]
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (emailThemeType === "jsx-email (React)") {
|
if (emailThemeType === "Another email templating solution") {
|
||||||
console.log(
|
console.log(
|
||||||
[
|
[
|
||||||
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
"There is currently no automated support for keycloakify-email, it has to be done manually, see documentation:",
|
||||||
@ -103,14 +106,21 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
|
|
||||||
const moduleName = `@keycloakify/email-native`;
|
const moduleName = `@keycloakify/email-native`;
|
||||||
|
|
||||||
const [version] = (
|
const [version] = ((): string[] => {
|
||||||
JSON.parse(
|
const cmdOutput = child_process
|
||||||
child_process
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
.execSync(`npm show ${moduleName} versions --json`)
|
.toString("utf8")
|
||||||
.toString("utf8")
|
.trim();
|
||||||
.trim()
|
|
||||||
) as string[]
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
)
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(version => !version.includes("-"));
|
.filter(version => !version.includes("-"));
|
||||||
|
|
||||||
|
@ -220,31 +220,39 @@ export async function buildJar(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise<void>((resolve, reject) =>
|
{
|
||||||
child_process.exec(
|
const mvnBuildCmd = `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`;
|
||||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
|
|
||||||
{ cwd: keycloakifyBuildCacheDirPath },
|
|
||||||
error => {
|
|
||||||
if (error !== null) {
|
|
||||||
console.error(
|
|
||||||
`Build jar failed: ${JSON.stringify(
|
|
||||||
{
|
|
||||||
jarFileBasename,
|
|
||||||
keycloakAccountV1Version,
|
|
||||||
keycloakThemeAdditionalInfoExtensionVersion
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
reject(error);
|
await new Promise<void>((resolve, reject) =>
|
||||||
return;
|
child_process.exec(
|
||||||
|
mvnBuildCmd,
|
||||||
|
{ cwd: keycloakifyBuildCacheDirPath },
|
||||||
|
error => {
|
||||||
|
if (error !== null) {
|
||||||
|
console.error(
|
||||||
|
[
|
||||||
|
`Build jar failed: ${JSON.stringify(
|
||||||
|
{
|
||||||
|
jarFileBasename,
|
||||||
|
keycloakAccountV1Version,
|
||||||
|
keycloakThemeAdditionalInfoExtensionVersion
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
)}`,
|
||||||
|
"Try running the following command to debug the issue (you are probably under a restricted network and you need to configure your proxy):",
|
||||||
|
`cd ${keycloakifyBuildCacheDirPath} && ${mvnBuildCmd}`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
reject(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
}
|
}
|
||||||
resolve();
|
)
|
||||||
}
|
);
|
||||||
)
|
}
|
||||||
);
|
|
||||||
|
|
||||||
await fs.rename(
|
await fs.rename(
|
||||||
pathJoin(
|
pathJoin(
|
||||||
|
@ -190,7 +190,7 @@ function decodeHtmlEntities(htmlStr){
|
|||||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
<#-- https://github.com/keycloakify/keycloakify/discussions/406#discussioncomment-7514787 -->
|
||||||
key == "loginAction" &&
|
key == "loginAction" &&
|
||||||
areSamePath(path, ["url"]) &&
|
areSamePath(path, ["url"]) &&
|
||||||
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl", "logout-confirm.ftl", "login-oauth2-device-verify-user-code.ftl", "frontchannel-logout.ftl"]?seq_contains(xKeycloakify.pageId) &&
|
||||||
!(auth?has_content && auth.showTryAnotherWayLink())
|
!(auth?has_content && auth.showTryAnotherWayLink())
|
||||||
) || (
|
) || (
|
||||||
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
|
||||||
|
@ -6,8 +6,7 @@ import {
|
|||||||
join as pathJoin,
|
join as pathJoin,
|
||||||
relative as pathRelative,
|
relative as pathRelative,
|
||||||
dirname as pathDirname,
|
dirname as pathDirname,
|
||||||
extname as pathExtname,
|
basename as pathBasename
|
||||||
sep as pathSep
|
|
||||||
} from "path";
|
} from "path";
|
||||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||||
@ -37,6 +36,7 @@ import propertiesParser from "properties-parser";
|
|||||||
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
import { createObjectThatThrowsIfAccessed } from "../../tools/createObjectThatThrowsIfAccessed";
|
||||||
import { listInstalledModules } from "../../tools/listInstalledModules";
|
import { listInstalledModules } from "../../tools/listInstalledModules";
|
||||||
import { isInside } from "../../tools/isInside";
|
import { isInside } from "../../tools/isInside";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||||
BuildContextLike_generateMessageProperties & {
|
BuildContextLike_generateMessageProperties & {
|
||||||
@ -57,6 +57,8 @@ export async function generateResources(params: {
|
|||||||
buildContext: BuildContextLike;
|
buildContext: BuildContextLike;
|
||||||
resourcesDirPath: string;
|
resourcesDirPath: string;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
const { resourcesDirPath, buildContext } = params;
|
const { resourcesDirPath, buildContext } = params;
|
||||||
|
|
||||||
const [themeName] = buildContext.themeNames;
|
const [themeName] = buildContext.themeNames;
|
||||||
@ -661,7 +663,7 @@ export async function generateResources(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const themeVariantName of buildContext.themeNames) {
|
for (const themeVariantName of [...buildContext.themeNames].reverse()) {
|
||||||
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
for (const themeType of [...THEME_TYPES, "email"] as const) {
|
||||||
copy_main_theme_to_theme_variant_theme: {
|
copy_main_theme_to_theme_variant_theme: {
|
||||||
let isNative: boolean;
|
let isNative: boolean;
|
||||||
@ -676,44 +678,59 @@ export async function generateResources(params: {
|
|||||||
isNative = !v.isImplemented && v.isImplemented_native;
|
isNative = !v.isImplemented && v.isImplemented_native;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (themeVariantName === themeName) {
|
if (!isNative && themeVariantName === themeName) {
|
||||||
break copy_main_theme_to_theme_variant_theme;
|
break copy_main_theme_to_theme_variant_theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
srcDirPath: pathJoin(resourcesDirPath, "theme", themeName, themeType),
|
srcDirPath: getThemeTypeDirPath({ themeName, themeType }),
|
||||||
destDirPath: pathJoin(
|
destDirPath: getThemeTypeDirPath({
|
||||||
resourcesDirPath,
|
themeName: themeVariantName,
|
||||||
"theme",
|
|
||||||
themeVariantName,
|
|
||||||
themeType
|
themeType
|
||||||
),
|
}),
|
||||||
transformSourceCode: isNative
|
transformSourceCode: ({ fileRelativePath, sourceCode }) => {
|
||||||
? undefined
|
patch_xKeycloakify_themeName: {
|
||||||
: ({ fileRelativePath, sourceCode }) => {
|
if (!fileRelativePath.endsWith(".ftl")) {
|
||||||
if (
|
break patch_xKeycloakify_themeName;
|
||||||
pathExtname(fileRelativePath) === ".ftl" &&
|
}
|
||||||
fileRelativePath.split(pathSep).length === 1
|
|
||||||
) {
|
|
||||||
const modifiedSourceCode = Buffer.from(
|
|
||||||
Buffer.from(sourceCode)
|
|
||||||
.toString("utf-8")
|
|
||||||
.replace(
|
|
||||||
`"themeName": "${themeName}"`,
|
|
||||||
`"themeName": "${themeVariantName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert(
|
if (
|
||||||
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
!isNative &&
|
||||||
);
|
pathBasename(fileRelativePath) !== fileRelativePath
|
||||||
|
) {
|
||||||
|
break patch_xKeycloakify_themeName;
|
||||||
|
}
|
||||||
|
|
||||||
return { modifiedSourceCode };
|
const modifiedSourceCode = Buffer.from(
|
||||||
}
|
Buffer.from(sourceCode)
|
||||||
|
.toString("utf-8")
|
||||||
|
.replace(
|
||||||
|
...id<[string | RegExp, string]>(
|
||||||
|
isNative
|
||||||
|
? [
|
||||||
|
/xKeycloakify\.themeName/g,
|
||||||
|
`"${themeVariantName}"`
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
`"themeName": "${themeName}"`,
|
||||||
|
`"themeName": "${themeVariantName}"`
|
||||||
|
]
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
);
|
||||||
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
if (!isNative) {
|
||||||
}
|
assert(
|
||||||
|
Buffer.compare(modifiedSourceCode, sourceCode) !== 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { modifiedSourceCode: sourceCode };
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
run_writeMessagePropertiesFiles: {
|
run_writeMessagePropertiesFiles: {
|
||||||
@ -732,42 +749,8 @@ export async function generateResources(params: {
|
|||||||
themeName: themeVariantName
|
themeName: themeVariantName
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
replace_xKeycloakify_themeName_in_native_ftl_files: {
|
|
||||||
{
|
|
||||||
const v = buildContext.implementedThemeTypes[themeType];
|
|
||||||
|
|
||||||
if (v.isImplemented || !v.isImplemented_native) {
|
|
||||||
break replace_xKeycloakify_themeName_in_native_ftl_files;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailThemeDirPath = getThemeTypeDirPath({
|
|
||||||
themeName,
|
|
||||||
themeType
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
srcDirPath: emailThemeDirPath,
|
|
||||||
destDirPath: emailThemeDirPath,
|
|
||||||
transformSourceCode: ({ filePath, sourceCode }) => {
|
|
||||||
if (!filePath.endsWith(".ftl")) {
|
|
||||||
return { modifiedSourceCode: sourceCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedSourceCode: Buffer.from(
|
|
||||||
sourceCode
|
|
||||||
.toString("utf8")
|
|
||||||
.replace(
|
|
||||||
/xKeycloakify\.themeName/g,
|
|
||||||
`"${themeName}"`
|
|
||||||
),
|
|
||||||
"utf8"
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`Generated resources in ${Date.now() - start}ms`);
|
||||||
}
|
}
|
||||||
|
@ -13,13 +13,15 @@ import * as fs from "fs";
|
|||||||
|
|
||||||
assert<Equals<ApiVersion, "v1">>();
|
assert<Equals<ApiVersion, "v1">>();
|
||||||
|
|
||||||
export function maybeDelegateCommandToCustomHandler(params: {
|
export async function maybeDelegateCommandToCustomHandler(params: {
|
||||||
commandName: CommandName;
|
commandName: CommandName;
|
||||||
buildContext: BuildContext;
|
buildContext: BuildContext;
|
||||||
}): { hasBeenHandled: boolean } {
|
}): Promise<{ hasBeenHandled: boolean }> {
|
||||||
const { commandName, buildContext } = params;
|
const { commandName, buildContext } = params;
|
||||||
|
|
||||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
const nodeModulesBinDirPath = await getNodeModulesBinDirPath({
|
||||||
|
packageJsonFilePath: buildContext.packageJsonFilePath
|
||||||
|
});
|
||||||
|
|
||||||
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
if (!fs.readdirSync(nodeModulesBinDirPath).includes(BIN_NAME)) {
|
||||||
return { hasBeenHandled: false };
|
return { hasBeenHandled: false };
|
||||||
|
@ -105,14 +105,21 @@ export async function initializeSpa(params: {
|
|||||||
|
|
||||||
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
const moduleName = `@keycloakify/keycloak-${themeType}-ui`;
|
||||||
|
|
||||||
const version = (
|
const version = ((): string[] => {
|
||||||
JSON.parse(
|
const cmdOutput = child_process
|
||||||
child_process
|
.execSync(`npm show ${moduleName} versions --json`)
|
||||||
.execSync(`npm show ${moduleName} versions --json`)
|
.toString("utf8")
|
||||||
.toString("utf8")
|
.trim();
|
||||||
.trim()
|
|
||||||
) as string[]
|
const versions = JSON.parse(cmdOutput) as string | string[];
|
||||||
)
|
|
||||||
|
// NOTE: Bug in some older npm versions
|
||||||
|
if (typeof versions === "string") {
|
||||||
|
return [versions];
|
||||||
|
}
|
||||||
|
|
||||||
|
return versions;
|
||||||
|
})()
|
||||||
.reverse()
|
.reverse()
|
||||||
.filter(version => !version.includes("-"))
|
.filter(version => !version.includes("-"))
|
||||||
.find(version =>
|
.find(version =>
|
||||||
|
@ -101,7 +101,6 @@ function addOrEditTestUser(params: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
newUser.username = defaultUser_default.username;
|
newUser.username = defaultUser_default.username;
|
||||||
newUser.email = defaultUser_default.email;
|
|
||||||
|
|
||||||
delete_existing_password_credential_if_any: {
|
delete_existing_password_credential_if_any: {
|
||||||
const i = newUser.credentials.findIndex(
|
const i = newUser.credentials.findIndex(
|
||||||
|
@ -53,11 +53,17 @@ export async function command(params: {
|
|||||||
.execSync("docker --version", {
|
.execSync("docker --version", {
|
||||||
stdio: ["ignore", "pipe", "ignore"]
|
stdio: ["ignore", "pipe", "ignore"]
|
||||||
})
|
})
|
||||||
?.toString("utf8");
|
.toString("utf8");
|
||||||
} catch {}
|
} catch {
|
||||||
|
commandOutput = "";
|
||||||
|
}
|
||||||
|
|
||||||
if (commandOutput?.includes("Docker") || commandOutput?.includes("podman")) {
|
commandOutput = commandOutput.trim().toLowerCase();
|
||||||
break exit_if_docker_not_installed;
|
|
||||||
|
for (const term of ["docker", "podman"]) {
|
||||||
|
if (commandOutput.includes(term)) {
|
||||||
|
break exit_if_docker_not_installed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@ -781,6 +787,40 @@ export async function command(params: {
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ignore_patternfly: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"@patternfly"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_patternfly;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ignore_keycloak_ui_shared: {
|
||||||
|
if (
|
||||||
|
!isInside({
|
||||||
|
dirPath: pathJoin(
|
||||||
|
buildContext.themeSrcDirPath,
|
||||||
|
"shared",
|
||||||
|
"keycloak-ui-shared"
|
||||||
|
),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
break ignore_keycloak_ui_shared;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Detected changes in ${filePath}`);
|
console.log(`Detected changes in ${filePath}`);
|
||||||
|
@ -45,12 +45,12 @@ export async function getExtensionModuleFileSourceCodeReadyToBeCopied(params: {
|
|||||||
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
`This file has been claimed for ownership from ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
`To relinquish ownership and restore this file to its original content, run the following command:`,
|
||||||
``,
|
``,
|
||||||
`$ npx keycloakify own --path '${path}' --revert`
|
`$ npx keycloakify own --path "${path}" --revert`
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
`WARNING: Before modifying this file, run the following command:`,
|
`WARNING: Before modifying this file, run the following command:`,
|
||||||
``,
|
``,
|
||||||
`$ npx keycloakify own --path '${path}'`,
|
`$ npx keycloakify own --path "${path}"`,
|
||||||
``,
|
``,
|
||||||
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
`This file is provided by ${extensionModuleName} version ${extensionModuleVersion}.`,
|
||||||
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
`It was copied into your repository by the postinstall script: \`keycloakify sync-extensions\`.`
|
||||||
@ -100,9 +100,19 @@ function addCommentToSourceCode(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".ftl")) {
|
if (fileRelativePath.endsWith(".ftl")) {
|
||||||
return toResult(
|
const comment = [`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join(
|
||||||
[`<#--`, ...commentLines.map(line => ` ${line}`), `-->`].join("\n")
|
"\n"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (sourceCode.trim().startsWith("<#ftl")) {
|
||||||
|
const [first, ...rest] = sourceCode.split(">");
|
||||||
|
|
||||||
|
const last = rest.join(">");
|
||||||
|
|
||||||
|
return [`${first}>`, comment, last].join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return toResult(comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
if (fileRelativePath.endsWith(".html") || fileRelativePath.endsWith(".svg")) {
|
||||||
|
@ -14,6 +14,8 @@ export function getAbsoluteAndInOsFormatPath(params: {
|
|||||||
|
|
||||||
let pathOut = pathIsh;
|
let pathOut = pathIsh;
|
||||||
|
|
||||||
|
pathOut = pathOut.replace(/^['"]/, "").replace(/['"]$/, "");
|
||||||
|
|
||||||
pathOut = pathOut.replace(/\//g, pathSep);
|
pathOut = pathOut.replace(/\//g, pathSep);
|
||||||
|
|
||||||
if (pathOut.startsWith("~")) {
|
if (pathOut.startsWith("~")) {
|
||||||
|
@ -1,10 +1,29 @@
|
|||||||
import { sep as pathSep } from "path";
|
import { sep as pathSep, dirname as pathDirname, join as pathJoin } from "path";
|
||||||
|
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
|
||||||
|
import { getInstalledModuleDirPath } from "./getInstalledModuleDirPath";
|
||||||
|
import { existsAsync } from "./fs.existsAsync";
|
||||||
|
import { z } from "zod";
|
||||||
|
import * as fs from "fs/promises";
|
||||||
|
import { assert, is, type Equals } from "tsafe/assert";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
|
||||||
let cache: string | undefined = undefined;
|
let cache_bestEffort: string | undefined = undefined;
|
||||||
|
|
||||||
export function getNodeModulesBinDirPath() {
|
/** NOTE: Careful, this function can fail when the binary
|
||||||
if (cache !== undefined) {
|
* Used is not in the node_modules directory of the project
|
||||||
return cache;
|
* (for example when running tests with vscode extension we'll get
|
||||||
|
* '/Users/dylan/.vscode/extensions/vitest.explorer-1.16.0/dist/worker.js'
|
||||||
|
*
|
||||||
|
* instead of
|
||||||
|
* '/Users/joseph/.nvm/versions/node/v22.12.0/bin/node'
|
||||||
|
* or
|
||||||
|
* '/Users/joseph/github/keycloakify-starter/node_modules/.bin/vite'
|
||||||
|
*
|
||||||
|
* as the value of process.argv[1]
|
||||||
|
*/
|
||||||
|
function getNodeModulesBinDirPath_bestEffort() {
|
||||||
|
if (cache_bestEffort !== undefined) {
|
||||||
|
return cache_bestEffort;
|
||||||
}
|
}
|
||||||
|
|
||||||
const binPath = process.argv[1];
|
const binPath = process.argv[1];
|
||||||
@ -30,9 +49,122 @@ export function getNodeModulesBinDirPath() {
|
|||||||
segments.unshift(segment);
|
segments.unshift(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundNodeModules) {
|
||||||
|
throw new Error(`Could not find node_modules in path ${binPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
const nodeModulesBinDirPath = segments.join(pathSep);
|
const nodeModulesBinDirPath = segments.join(pathSep);
|
||||||
|
|
||||||
cache = nodeModulesBinDirPath;
|
cache_bestEffort = nodeModulesBinDirPath;
|
||||||
|
|
||||||
return nodeModulesBinDirPath;
|
return nodeModulesBinDirPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cache_withPackageJsonFileDirPath:
|
||||||
|
| { packageJsonFilePath: string; nodeModulesBinDirPath: string }
|
||||||
|
| undefined = undefined;
|
||||||
|
|
||||||
|
async function getNodeModulesBinDirPath_withPackageJsonFileDirPath(params: {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
const { packageJsonFilePath } = params;
|
||||||
|
|
||||||
|
use_cache: {
|
||||||
|
if (cache_withPackageJsonFileDirPath === undefined) {
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
cache_withPackageJsonFileDirPath.packageJsonFilePath !== packageJsonFilePath
|
||||||
|
) {
|
||||||
|
cache_withPackageJsonFileDirPath = undefined;
|
||||||
|
break use_cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [...]node_modules/keycloakify
|
||||||
|
const installedModuleDirPath = await getInstalledModuleDirPath({
|
||||||
|
// Here it will always be "keycloakify" but since we are in tools/ we make something generic
|
||||||
|
moduleName: await (async () => {
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = (() => {
|
||||||
|
type TargetType = ParsedPackageJson;
|
||||||
|
|
||||||
|
const zTargetType = z.object({
|
||||||
|
name: z.string()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<z.infer<typeof zTargetType>, TargetType>>;
|
||||||
|
|
||||||
|
return id<z.ZodType<TargetType>>(zTargetType);
|
||||||
|
})();
|
||||||
|
|
||||||
|
const parsedPackageJson = JSON.parse(
|
||||||
|
(
|
||||||
|
await fs.readFile(
|
||||||
|
pathJoin(getThisCodebaseRootDirPath(), "package.json")
|
||||||
|
)
|
||||||
|
).toString("utf8")
|
||||||
|
);
|
||||||
|
|
||||||
|
zParsedPackageJson.parse(parsedPackageJson);
|
||||||
|
|
||||||
|
assert(is<ParsedPackageJson>(parsedPackageJson));
|
||||||
|
|
||||||
|
return parsedPackageJson.name;
|
||||||
|
})(),
|
||||||
|
packageJsonDirPath: pathDirname(packageJsonFilePath)
|
||||||
|
});
|
||||||
|
|
||||||
|
const segments = installedModuleDirPath.split(pathSep);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const segment = segments.pop();
|
||||||
|
|
||||||
|
if (segment === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
`Could not find .bin directory relative to ${packageJsonFilePath}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment !== "node_modules") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const candidate = pathJoin(segments.join(pathSep), segment, ".bin");
|
||||||
|
|
||||||
|
if (!(await existsAsync(candidate))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache_withPackageJsonFileDirPath = {
|
||||||
|
packageJsonFilePath,
|
||||||
|
nodeModulesBinDirPath: candidate
|
||||||
|
};
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cache_withPackageJsonFileDirPath.nodeModulesBinDirPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: string;
|
||||||
|
}): Promise<string>;
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: undefined;
|
||||||
|
}): string;
|
||||||
|
export function getNodeModulesBinDirPath(params: {
|
||||||
|
packageJsonFilePath: string | undefined;
|
||||||
|
}): string | Promise<string> {
|
||||||
|
const { packageJsonFilePath } = params ?? {};
|
||||||
|
|
||||||
|
return packageJsonFilePath === undefined
|
||||||
|
? getNodeModulesBinDirPath_bestEffort()
|
||||||
|
: getNodeModulesBinDirPath_withPackageJsonFileDirPath({ packageJsonFilePath });
|
||||||
|
}
|
||||||
|
@ -15,7 +15,9 @@ export async function getIsPrettierAvailable(): Promise<boolean> {
|
|||||||
return getIsPrettierAvailable.cache;
|
return getIsPrettierAvailable.cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeModulesBinDirPath = getNodeModulesBinDirPath();
|
const nodeModulesBinDirPath = getNodeModulesBinDirPath({
|
||||||
|
packageJsonFilePath: undefined
|
||||||
|
});
|
||||||
|
|
||||||
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
const prettierBinPath = pathJoin(nodeModulesBinDirPath, "prettier");
|
||||||
|
|
||||||
@ -51,7 +53,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
|||||||
// We make sure to only do that when linking, otherwise we import properly.
|
// We make sure to only do that when linking, otherwise we import properly.
|
||||||
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
if (readThisNpmPackageVersion().startsWith("0.0.0")) {
|
||||||
eval(
|
eval(
|
||||||
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath(), "..", "prettier"))}")`
|
`${symToStr({ prettier })} = require("${pathResolve(pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..", "prettier"))}")`
|
||||||
);
|
);
|
||||||
|
|
||||||
assert(!is<undefined>(prettier));
|
assert(!is<undefined>(prettier));
|
||||||
@ -64,7 +66,7 @@ export async function getPrettier(): Promise<PrettierAndConfigHash> {
|
|||||||
|
|
||||||
const configHash = await (async () => {
|
const configHash = await (async () => {
|
||||||
const configFilePath = await prettier.resolveConfigFile(
|
const configFilePath = await prettier.resolveConfigFile(
|
||||||
pathJoin(getNodeModulesBinDirPath(), "..")
|
pathJoin(getNodeModulesBinDirPath({ packageJsonFilePath: undefined }), "..")
|
||||||
);
|
);
|
||||||
|
|
||||||
if (configFilePath === null) {
|
if (configFilePath === null) {
|
||||||
|
@ -19,7 +19,7 @@ export async function command(params: { buildContext: BuildContext }) {
|
|||||||
await command({ buildContext });
|
await command({ buildContext });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
|
const { hasBeenHandled } = await maybeDelegateCommandToCustomHandler({
|
||||||
commandName: "update-kc-gen",
|
commandName: "update-kc-gen",
|
||||||
buildContext
|
buildContext
|
||||||
});
|
});
|
||||||
|
@ -769,6 +769,8 @@ export declare namespace Validators {
|
|||||||
export type PasswordPolicies = {
|
export type PasswordPolicies = {
|
||||||
/** The minimum length of the password */
|
/** The minimum length of the password */
|
||||||
length?: number;
|
length?: number;
|
||||||
|
/** The maximum length of the password */
|
||||||
|
maxLength?: number;
|
||||||
/** The minimum number of digits required in the password */
|
/** The minimum number of digits required in the password */
|
||||||
digits?: number;
|
digits?: number;
|
||||||
/** The minimum number of lowercase characters required in the password */
|
/** The minimum number of lowercase characters required in the password */
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useEffect, useReducer, Fragment } from "react";
|
import { useEffect, Fragment } from "react";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
import type { KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import {
|
import {
|
||||||
useUserProfileForm,
|
useUserProfileForm,
|
||||||
@ -89,7 +90,6 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
|||||||
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
{advancedMsg(attribute.annotations.inputHelperTextAfter)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{AfterField !== undefined && (
|
{AfterField !== undefined && (
|
||||||
<AfterField
|
<AfterField
|
||||||
attribute={attribute}
|
attribute={attribute}
|
||||||
@ -106,6 +106,10 @@ export default function UserProfileFormFields(props: UserProfileFormFieldsProps<
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{/* See: https://github.com/keycloak/keycloak/issues/38029 */}
|
||||||
|
{kcContext.locale !== undefined && formFieldStates.find(x => x.attribute.name === "locale") === undefined && (
|
||||||
|
<input type="hidden" name="locale" value={i18n.currentLanguage.languageTag} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -249,15 +253,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -509,6 +509,8 @@ function formStateSelector(params: { state: internal.State }): FormState {
|
|||||||
switch (error.source.name) {
|
switch (error.source.name) {
|
||||||
case "length":
|
case "length":
|
||||||
return hasLostFocusAtLeastOnce;
|
return hasLostFocusAtLeastOnce;
|
||||||
|
case "maxLength":
|
||||||
|
return hasLostFocusAtLeastOnce;
|
||||||
case "digits":
|
case "digits":
|
||||||
return hasLostFocusAtLeastOnce;
|
return hasLostFocusAtLeastOnce;
|
||||||
case "lowerCase":
|
case "lowerCase":
|
||||||
@ -967,6 +969,34 @@ function createGetErrors(params: { kcContext: KcContextLike_useGetErrors }) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_password_policy_x: {
|
||||||
|
const policyName = "maxLength";
|
||||||
|
|
||||||
|
const policy = passwordPolicies[policyName];
|
||||||
|
|
||||||
|
if (!policy) {
|
||||||
|
break check_password_policy_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxLength = policy;
|
||||||
|
|
||||||
|
if (value.length <= maxLength) {
|
||||||
|
break check_password_policy_x;
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
advancedMsgArgs: [
|
||||||
|
"invalidPasswordMaxLengthMessage" satisfies MessageKey_defaultSet,
|
||||||
|
`${maxLength}`
|
||||||
|
] as const,
|
||||||
|
fieldIndex: undefined,
|
||||||
|
source: {
|
||||||
|
type: "passwordPolicy",
|
||||||
|
name: policyName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
check_password_policy_x: {
|
check_password_policy_x: {
|
||||||
const policyName = "digits";
|
const policyName = "digits";
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
@ -200,15 +200,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useState, useEffect, useReducer } from "react";
|
import { useState } from "react";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { clsx } from "keycloakify/tools/clsx";
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -107,15 +107,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import type { JSX } from "keycloakify/tools/JSX";
|
import type { JSX } from "keycloakify/tools/JSX";
|
||||||
import { useEffect, useReducer } from "react";
|
import { useIsPasswordRevealed } from "keycloakify/tools/useIsPasswordRevealed";
|
||||||
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
import { kcSanitize } from "keycloakify/lib/kcSanitize";
|
||||||
import { assert } from "keycloakify/tools/assert";
|
|
||||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
import type { KcContext } from "../KcContext";
|
import type { KcContext } from "../KcContext";
|
||||||
@ -146,15 +145,7 @@ function PasswordWrapper(props: { kcClsx: KcClsx; i18n: I18n; passwordInputId: s
|
|||||||
|
|
||||||
const { msgStr } = i18n;
|
const { msgStr } = i18n;
|
||||||
|
|
||||||
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer((isPasswordRevealed: boolean) => !isPasswordRevealed, false);
|
const { isPasswordRevealed, toggleIsPasswordRevealed } = useIsPasswordRevealed({ passwordInputId });
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const passwordInputElement = document.getElementById(passwordInputId);
|
|
||||||
|
|
||||||
assert(passwordInputElement instanceof HTMLInputElement);
|
|
||||||
|
|
||||||
passwordInputElement.type = isPasswordRevealed ? "text" : "password";
|
|
||||||
}, [isPasswordRevealed]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={kcClsx("kcInputGroup")}>
|
<div className={kcClsx("kcInputGroup")}>
|
||||||
|
@ -98,7 +98,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
|||||||
defaultValue={login.username ?? ""}
|
defaultValue={login.username ?? ""}
|
||||||
type="text"
|
type="text"
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="off"
|
autoComplete="username"
|
||||||
aria-invalid={messagesPerField.existsError("username")}
|
aria-invalid={messagesPerField.existsError("username")}
|
||||||
/>
|
/>
|
||||||
{messagesPerField.existsError("username") && (
|
{messagesPerField.existsError("username") && (
|
||||||
|
45
src/tools/useIsPasswordRevealed.ts
Normal file
45
src/tools/useIsPasswordRevealed.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { useEffect, useReducer } from "react";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initially false, state that enables to dynamically control if
|
||||||
|
* the type of a password input is "password" (false) or "text" (true).
|
||||||
|
*/
|
||||||
|
export function useIsPasswordRevealed(params: { passwordInputId: string }) {
|
||||||
|
const { passwordInputId } = params;
|
||||||
|
|
||||||
|
const [isPasswordRevealed, toggleIsPasswordRevealed] = useReducer(
|
||||||
|
(isPasswordRevealed: boolean) => !isPasswordRevealed,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const passwordInputElement = document.getElementById(passwordInputId);
|
||||||
|
|
||||||
|
assert(passwordInputElement instanceof HTMLInputElement);
|
||||||
|
|
||||||
|
const type = isPasswordRevealed ? "text" : "password";
|
||||||
|
|
||||||
|
passwordInputElement.type = type;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutations => {
|
||||||
|
mutations.forEach(mutation => {
|
||||||
|
if (mutation.attributeName !== "type") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (passwordInputElement.type === type) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
passwordInputElement.type = type;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(passwordInputElement, { attributes: true });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
}, [isPasswordRevealed]);
|
||||||
|
|
||||||
|
return { isPasswordRevealed, toggleIsPasswordRevealed };
|
||||||
|
}
|
Reference in New Issue
Block a user