Compare commits
35 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
2ab2c9e05e | |||
b1e9ba3ac6 | |||
5822ed0185 | |||
17b295788d | |||
6cd5b958c7 | |||
df92cc5f73 | |||
03106cdee3 | |||
c4638daf1b | |||
e2f5eb79ad | |||
b6c8e9bca0 | |||
573839019e | |||
815bf10ae0 | |||
7c257d97a7 | |||
59f8814660 | |||
1a6993099f | |||
f62ded3c8e | |||
4eca6366cc | |||
51a45b355d | |||
e5765cb902 | |||
6e922d2033 | |||
5d1695ada8 | |||
6e3ce29067 | |||
2b9bbc4cef | |||
9557145f72 | |||
249877b9c5 | |||
ff2321fde5 | |||
1edd6e4193 | |||
bdde9162d9 | |||
99b4933536 | |||
c5caf7e0da | |||
bcc5308cfb | |||
9fb902db5c | |||
239f98aa9c | |||
f5d0511662 | |||
75582d2a26 |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- name: If this step fails run 'npm run format' then commit again.
|
||||
run: npm run format:check
|
||||
run: npm run _format --list-different
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: test_lint
|
||||
|
14
README.md
14
README.md
@ -17,9 +17,12 @@
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/kYFZG7fQmn">
|
||||
<img src="https://img.shields.io/discord/1097708346976505977"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
Check out our discord server!<br/>
|
||||
<a href="https://discord.gg/mJdYJSdcm4">
|
||||
<img src="https://dcbadge.limes.pink/api/server/kYFZG7fQmn"/>
|
||||
</a>
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://www.keycloakify.dev">Home</a>
|
||||
-
|
||||
@ -38,10 +41,9 @@
|
||||
<img width="400" src="https://github.com/keycloakify/keycloakify/assets/6702424/e66d105c-c06f-47d1-8a31-a6ab09da4e80">
|
||||
</p>
|
||||
|
||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), **23** [and up](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)!
|
||||
Keycloakify is fully compatible with Keycloak 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, [~~22~~](https://github.com/keycloakify/keycloakify/issues/389#issuecomment-1822509763), 23, 24, 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
|
||||
|
||||
> NOTE: Keycloak 24 introduces [important changes](https://www.keycloak.org/docs/latest/upgrading/index.html#changes-to-freemarker-templates-to-render-pages-based-on-the-user-profile-and-realm).
|
||||
> We're actively working on incorporating them into Keycloakify. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
||||
> NOTE: Keycloakify 10 is still in realase-candidate state. [Follow progress](https://github.com/keycloakify/keycloakify/pull/538).
|
||||
|
||||
## Sponsor
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.97",
|
||||
"version": "10.0.0-rc.105",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -15,7 +15,6 @@
|
||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||
"format": "yarn _format --write",
|
||||
"format:check": "yarn _format --list-different",
|
||||
"link-in-app": "tsx scripts/link-in-app.ts",
|
||||
"build-storybook": "tsx scripts/build-storybook.ts",
|
||||
"dump-keycloak-realm": "tsx scripts/dump-keycloak-realm.ts"
|
||||
|
@ -12,6 +12,7 @@ import { crawl } from "../src/bin/tools/crawl";
|
||||
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||
import { deepAssign } from "../src/tools/deepAssign";
|
||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||
|
||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
// update the version array for generating for newer version.
|
||||
@ -33,7 +34,9 @@ async function main() {
|
||||
".cache",
|
||||
"keycloakify"
|
||||
),
|
||||
npmWorkspaceRootDirPath: thisCodebaseRootDirPath
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: thisCodebaseRootDirPath
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -145,7 +145,12 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className="pficon pficon-ok"></span>}
|
||||
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
|
||||
<span className="kc-feedback-text">{message.summary}</span>
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -62,7 +62,6 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
|
||||
{index < application.realmRolesAvailable.length - 1 && ", "}
|
||||
</span>
|
||||
))}
|
||||
{!isArrayWithEmptyObject(application.realmRolesAvailable) && application.resourceRolesAvailable && ", "}
|
||||
{application.resourceRolesAvailable &&
|
||||
Object.keys(application.resourceRolesAvailable).map(resource => (
|
||||
<span key={resource}>
|
||||
|
@ -154,9 +154,14 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("totp")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
||||
@ -180,9 +185,14 @@ export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-label"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("userLabel")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -33,6 +33,7 @@ export async function buildJar(params: {
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
resourcesDirPath: string;
|
||||
doesImplementAccountV1Theme: boolean;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<void> {
|
||||
const {
|
||||
@ -40,6 +41,7 @@ export async function buildJar(params: {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
resourcesDirPath,
|
||||
doesImplementAccountV1Theme,
|
||||
buildContext
|
||||
} = params;
|
||||
|
||||
@ -61,7 +63,7 @@ export async function buildJar(params: {
|
||||
srcDirPath: resourcesDirPath,
|
||||
destDirPath: tmpResourcesDirPath,
|
||||
transformSourceCode:
|
||||
keycloakAccountV1Version !== null
|
||||
!doesImplementAccountV1Theme || keycloakAccountV1Version !== null
|
||||
? undefined
|
||||
: (params: {
|
||||
fileRelativePath: string;
|
||||
@ -105,7 +107,17 @@ export async function buildJar(params: {
|
||||
}
|
||||
});
|
||||
|
||||
if (keycloakAccountV1Version === null) {
|
||||
remove_account_v1_in_meta_inf: {
|
||||
if (!doesImplementAccountV1Theme) {
|
||||
// NOTE: We do not have account v1 anyway
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
if (keycloakAccountV1Version !== null) {
|
||||
// NOTE: No, we need to keep account-v1 in meta-inf
|
||||
break remove_account_v1_in_meta_inf;
|
||||
}
|
||||
|
||||
writeMetaInfKeycloakThemes({
|
||||
resourcesDirPath: tmpResourcesDirPath,
|
||||
getNewMetaInfKeycloakTheme: ({ metaInfKeycloakTheme }) => {
|
||||
@ -135,6 +147,7 @@ export async function buildJar(params: {
|
||||
}
|
||||
})();
|
||||
|
||||
// TODO: Remove this optimization, it's a bit hacky.
|
||||
if (doBreak) {
|
||||
break route_legacy_pages;
|
||||
}
|
||||
@ -194,7 +207,7 @@ export async function buildJar(params: {
|
||||
|
||||
await new Promise<void>((resolve, reject) =>
|
||||
child_process.exec(
|
||||
`mvn clean install -Dmaven.repo.local=${pathJoin(keycloakifyBuildTmpDirPath, ".m2")}`,
|
||||
`mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildTmpDirPath, ".m2")}"`,
|
||||
{ cwd: keycloakifyBuildTmpDirPath },
|
||||
error => {
|
||||
if (error !== null) {
|
||||
|
@ -12,6 +12,7 @@ export type BuildContextLike = BuildContextLike_buildJar & {
|
||||
keycloakifyBuildDirPath: string;
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
jarTargets: BuildContext["jarTargets"];
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -22,7 +23,9 @@ export async function buildJars(params: {
|
||||
}): Promise<void> {
|
||||
const { resourcesDirPath, buildContext } = params;
|
||||
|
||||
const doesImplementAccountTheme = buildContext.recordIsImplementedByThemeType.account;
|
||||
const doesImplementAccountV1Theme =
|
||||
buildContext.recordIsImplementedByThemeType.account &&
|
||||
!buildContext.doUseAccountV3;
|
||||
|
||||
await Promise.all(
|
||||
keycloakAccountV1Versions
|
||||
@ -30,7 +33,7 @@ export async function buildJars(params: {
|
||||
keycloakThemeAdditionalInfoExtensionVersions.map(
|
||||
keycloakThemeAdditionalInfoExtensionVersion => {
|
||||
const keycloakVersionRange = getKeycloakVersionRangeForJar({
|
||||
doesImplementAccountTheme,
|
||||
doesImplementAccountV1Theme,
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion
|
||||
});
|
||||
@ -55,6 +58,7 @@ export async function buildJars(params: {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
resourcesDirPath,
|
||||
doesImplementAccountV1Theme,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
@ -6,17 +6,17 @@ import type {
|
||||
import type { KeycloakVersionRange } from "../../shared/KeycloakVersionRange";
|
||||
|
||||
export function getKeycloakVersionRangeForJar(params: {
|
||||
doesImplementAccountTheme: boolean;
|
||||
doesImplementAccountV1Theme: boolean;
|
||||
keycloakAccountV1Version: KeycloakAccountV1Version;
|
||||
keycloakThemeAdditionalInfoExtensionVersion: KeycloakThemeAdditionalInfoExtensionVersion;
|
||||
}): KeycloakVersionRange | undefined {
|
||||
const {
|
||||
keycloakAccountV1Version,
|
||||
keycloakThemeAdditionalInfoExtensionVersion,
|
||||
doesImplementAccountTheme
|
||||
doesImplementAccountV1Theme
|
||||
} = params;
|
||||
|
||||
if (doesImplementAccountTheme) {
|
||||
if (doesImplementAccountV1Theme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
switch (keycloakAccountV1Version) {
|
||||
case null:
|
||||
@ -63,7 +63,7 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountTheme | undefined
|
||||
KeycloakVersionRange.WithAccountV1Theme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
@ -87,7 +87,7 @@ export function getKeycloakVersionRangeForJar(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountTheme | undefined
|
||||
KeycloakVersionRange.WithoutAccountV1Theme | undefined
|
||||
>
|
||||
>();
|
||||
|
||||
|
@ -34,6 +34,7 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
keycloakifyVersion: string;
|
||||
themeType: ThemeType;
|
||||
fieldNames: string[];
|
||||
isAccountV3: boolean;
|
||||
}) {
|
||||
const {
|
||||
themeName,
|
||||
@ -41,7 +42,8 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
buildContext,
|
||||
keycloakifyVersion,
|
||||
themeType,
|
||||
fieldNames
|
||||
fieldNames,
|
||||
isAccountV3
|
||||
} = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
@ -68,7 +70,8 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
const { fixedCssCode } = replaceImportsInCssCode({
|
||||
cssCode,
|
||||
cssFileRelativeDirPath: undefined,
|
||||
buildContext
|
||||
buildContext,
|
||||
isAccountV3
|
||||
});
|
||||
|
||||
$(element).text(fixedCssCode);
|
||||
@ -93,7 +96,7 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
new RegExp(
|
||||
`^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}`
|
||||
),
|
||||
`\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
`\${${!isAccountV3 ? "url.resourcesPath" : "resourceUrl"}}/${basenameOfTheKeycloakifyResourcesDir}/`
|
||||
)
|
||||
);
|
||||
})
|
||||
|
@ -1,4 +1,5 @@
|
||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||
<#assign themeType="KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr">
|
||||
const kcContext = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
if( kcContext.messagesPerField ){
|
||||
var existsError_singleFieldName = kcContext.messagesPerField.existsError;
|
||||
@ -27,7 +28,7 @@ if( kcContext.messagesPerField ){
|
||||
}
|
||||
kcContext.keycloakifyVersion = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
|
||||
kcContext.themeVersion = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
|
||||
kcContext.themeType = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
|
||||
kcContext.themeType = "${themeType}";
|
||||
kcContext.themeName = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
|
||||
kcContext.pageId = "${pageId}";
|
||||
if( kcContext.url && kcContext.url.resourcesPath ){
|
||||
|
@ -4,7 +4,8 @@ import {
|
||||
join as pathJoin,
|
||||
resolve as pathResolve,
|
||||
relative as pathRelative,
|
||||
dirname as pathDirname
|
||||
dirname as pathDirname,
|
||||
basename as pathBasename
|
||||
} from "path";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
@ -42,6 +43,7 @@ import {
|
||||
} from "../../shared/metaInfKeycloakThemes";
|
||||
import { objectEntries } from "tsafe/objectEntries";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
|
||||
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
BuildContextLike_downloadKeycloakStaticResources &
|
||||
@ -54,6 +56,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
themeSrcDirPath: string;
|
||||
bundler: { type: "vite" } | { type: "webpack" };
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -71,6 +74,7 @@ export async function generateResourcesForMainTheme(params: {
|
||||
};
|
||||
|
||||
for (const themeType of ["login", "account"] as const) {
|
||||
const isAccountV3 = themeType === "account" && buildContext.doUseAccountV3;
|
||||
if (!buildContext.recordIsImplementedByThemeType[themeType]) {
|
||||
continue;
|
||||
}
|
||||
@ -136,7 +140,8 @@ export async function generateResourcesForMainTheme(params: {
|
||||
const { fixedCssCode } = replaceImportsInCssCode({
|
||||
cssCode: sourceCode.toString("utf8"),
|
||||
cssFileRelativeDirPath: pathDirname(fileRelativePath),
|
||||
buildContext
|
||||
buildContext,
|
||||
isAccountV3
|
||||
});
|
||||
|
||||
return {
|
||||
@ -171,7 +176,8 @@ export async function generateResourcesForMainTheme(params: {
|
||||
fieldNames: readFieldNameUsage({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
})
|
||||
}),
|
||||
isAccountV3
|
||||
});
|
||||
|
||||
[
|
||||
@ -180,13 +186,15 @@ export async function generateResourcesForMainTheme(params: {
|
||||
case "login":
|
||||
return loginThemePageIds;
|
||||
case "account":
|
||||
return accountThemePageIds;
|
||||
return isAccountV3 ? ["index.ftl"] : accountThemePageIds;
|
||||
}
|
||||
})(),
|
||||
...readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
})
|
||||
...(isAccountV3
|
||||
? []
|
||||
: readExtraPagesNames({
|
||||
themeType,
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath
|
||||
}))
|
||||
].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
@ -196,40 +204,52 @@ export async function generateResourcesForMainTheme(params: {
|
||||
);
|
||||
});
|
||||
|
||||
generateMessageProperties({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
i18n_messages_generation: {
|
||||
if (isAccountV3) {
|
||||
break i18n_messages_generation;
|
||||
}
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
|
||||
recursive: true
|
||||
generateMessageProperties({
|
||||
themeSrcDirPath: buildContext.themeSrcDirPath,
|
||||
themeType
|
||||
}).forEach(({ languageTag, propertiesFileSource }) => {
|
||||
const messagesDirPath = pathJoin(themeTypeDirPath, "messages");
|
||||
|
||||
fs.mkdirSync(pathJoin(themeTypeDirPath, "messages"), {
|
||||
recursive: true
|
||||
});
|
||||
|
||||
const propertiesFilePath = pathJoin(
|
||||
messagesDirPath,
|
||||
`messages_${languageTag}.properties`
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
propertiesFilePath,
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const propertiesFilePath = pathJoin(
|
||||
messagesDirPath,
|
||||
`messages_${languageTag}.properties`
|
||||
);
|
||||
keycloak_static_resources: {
|
||||
if (isAccountV3) {
|
||||
break keycloak_static_resources;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
propertiesFilePath,
|
||||
Buffer.from(propertiesFileSource, "utf8")
|
||||
);
|
||||
});
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return lastKeycloakVersionWithAccountV1;
|
||||
case "login":
|
||||
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||
}
|
||||
})(),
|
||||
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeType,
|
||||
buildContext
|
||||
});
|
||||
await downloadKeycloakStaticResources({
|
||||
keycloakVersion: (() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return lastKeycloakVersionWithAccountV1;
|
||||
case "login":
|
||||
return buildContext.loginThemeResourcesFromKeycloakVersion;
|
||||
}
|
||||
})(),
|
||||
themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")),
|
||||
themeType,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeTypeDirPath, "theme.properties"),
|
||||
@ -238,12 +258,13 @@ export async function generateResourcesForMainTheme(params: {
|
||||
`parent=${(() => {
|
||||
switch (themeType) {
|
||||
case "account":
|
||||
return accountV1ThemeName;
|
||||
return isAccountV3 ? "base" : accountV1ThemeName;
|
||||
case "login":
|
||||
return "keycloak";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>(false);
|
||||
})()}`,
|
||||
...(isAccountV3 ? ["deprecatedMode=false"] : []),
|
||||
...(buildContext.extraThemeProperties ?? []),
|
||||
...buildContext.environmentVariables.map(
|
||||
({ name, default: defaultValue }) =>
|
||||
@ -268,13 +289,54 @@ export async function generateResourcesForMainTheme(params: {
|
||||
});
|
||||
}
|
||||
|
||||
if (buildContext.recordIsImplementedByThemeType.account) {
|
||||
bring_in_account_v1: {
|
||||
if (buildContext.doUseAccountV3) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
if (!buildContext.recordIsImplementedByThemeType.account) {
|
||||
break bring_in_account_v1;
|
||||
}
|
||||
|
||||
await bringInAccountV1({
|
||||
resourcesDirPath,
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
||||
bring_in_account_v3_i18n_messages: {
|
||||
if (!buildContext.doUseAccountV3) {
|
||||
break bring_in_account_v3_i18n_messages;
|
||||
}
|
||||
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: "https://repo1.maven.org/maven2/org/keycloak/keycloak-account-ui/25.0.1/keycloak-account-ui-25.0.1.jar",
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnArchiveFile: "bring_in_account_v3_i18n_messages",
|
||||
onArchiveFile: async ({ fileRelativePath, writeFile }) => {
|
||||
if (
|
||||
!fileRelativePath.startsWith(
|
||||
pathJoin("theme", "keycloak.v3", "account", "messages")
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
await writeFile({
|
||||
fileRelativePath: pathBasename(fileRelativePath)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: extractedDirPath,
|
||||
destDirPath: pathJoin(
|
||||
getThemeTypeDirPath({ themeType: "account" }),
|
||||
"messages"
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const metaInfKeycloakThemes: MetaInfKeycloakTheme = { themes: [] };
|
||||
|
||||
|
@ -34,10 +34,7 @@ export function readExtraPagesNames(params: {
|
||||
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
|
||||
|
||||
extraPages.push(
|
||||
...Array.from(
|
||||
rawSourceFile.matchAll(/["']([^.\s]+.ftl)["']:/g),
|
||||
m => (console.log(m), m[1])
|
||||
)
|
||||
...Array.from(rawSourceFile.matchAll(/["']([^.\s]+.ftl)["']:/g), m => m[1])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -12,11 +12,12 @@ assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
export function replaceImportsInCssCode(params: {
|
||||
cssCode: string;
|
||||
cssFileRelativeDirPath: string | undefined;
|
||||
isAccountV3: boolean;
|
||||
buildContext: BuildContextLike;
|
||||
}): {
|
||||
fixedCssCode: string;
|
||||
} {
|
||||
const { cssCode, cssFileRelativeDirPath, buildContext } = params;
|
||||
const { cssCode, cssFileRelativeDirPath, buildContext, isAccountV3 } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
/url\(["']?(\/[^/][^)"']+)["']?\)/g,
|
||||
@ -37,7 +38,7 @@ export function replaceImportsInCssCode(params: {
|
||||
break inline_style_in_html;
|
||||
}
|
||||
|
||||
return `url(\${url.resourcesPath}/${basenameOfTheKeycloakifyResourcesDir}${assetFileAbsoluteUrlPathname})`;
|
||||
return `url(\${${!isAccountV3 ? "url.resourcesPath" : "resourceUrl"}}/${basenameOfTheKeycloakifyResourcesDir}${assetFileAbsoluteUrlPathname})`;
|
||||
}
|
||||
|
||||
const assetFileRelativeUrlPathname = posix.relative(
|
||||
|
@ -3,11 +3,14 @@
|
||||
import { termost } from "termost";
|
||||
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
|
||||
import * as child_process from "child_process";
|
||||
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
|
||||
|
||||
export type CliCommandOptions = {
|
||||
projectDirPath: string | undefined;
|
||||
};
|
||||
|
||||
assertNoPnpmDlx();
|
||||
|
||||
const program = termost<CliCommandOptions>(
|
||||
{
|
||||
name: "keycloakify",
|
||||
|
@ -1,9 +1,9 @@
|
||||
export type KeycloakVersionRange =
|
||||
| KeycloakVersionRange.WithAccountTheme
|
||||
| KeycloakVersionRange.WithoutAccountTheme;
|
||||
| KeycloakVersionRange.WithAccountV1Theme
|
||||
| KeycloakVersionRange.WithoutAccountV1Theme;
|
||||
|
||||
export namespace KeycloakVersionRange {
|
||||
export type WithoutAccountTheme = "21-and-below" | "22-and-above";
|
||||
export type WithoutAccountV1Theme = "21-and-below" | "22-and-above";
|
||||
|
||||
export type WithAccountTheme = "21-and-below" | "23" | "24" | "25-and-above";
|
||||
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ export type BuildContext = {
|
||||
packageJsonDirPath: string;
|
||||
packageJsonScripts: Record<string, string>;
|
||||
};
|
||||
doUseAccountV3: boolean;
|
||||
};
|
||||
|
||||
export type BuildOptions = {
|
||||
@ -77,16 +78,17 @@ export type BuildOptions = {
|
||||
kcContextExclusionsFtl?: string;
|
||||
/** https://docs.keycloakify.dev/v/v10/targetting-specific-keycloak-versions */
|
||||
keycloakVersionTargets?: BuildOptions.KeycloakVersionTargets;
|
||||
doUseAccountV3?: boolean;
|
||||
};
|
||||
|
||||
export namespace BuildOptions {
|
||||
export type KeycloakVersionTargets =
|
||||
| ({ hasAccountTheme: true } & Record<
|
||||
KeycloakVersionRange.WithAccountTheme,
|
||||
KeycloakVersionRange.WithAccountV1Theme,
|
||||
string | boolean
|
||||
>)
|
||||
| ({ hasAccountTheme: false } & Record<
|
||||
KeycloakVersionRange.WithoutAccountTheme,
|
||||
KeycloakVersionRange.WithoutAccountV1Theme,
|
||||
string | boolean
|
||||
>);
|
||||
}
|
||||
@ -229,6 +231,7 @@ export function getBuildContext(params: {
|
||||
projectBuildDirPath?: string;
|
||||
staticDirPathInProjectBuildDirPath?: string;
|
||||
publicDirPath?: string;
|
||||
doUseAccountV3?: boolean;
|
||||
};
|
||||
|
||||
type ParsedPackageJson = {
|
||||
@ -297,7 +300,8 @@ export function getBuildContext(params: {
|
||||
|
||||
return zKeycloakVersionTargets;
|
||||
})()
|
||||
).optional()
|
||||
).optional(),
|
||||
doUseAccountV3: z.boolean().optional()
|
||||
});
|
||||
|
||||
{
|
||||
@ -386,6 +390,8 @@ export function getBuildContext(params: {
|
||||
|
||||
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
|
||||
|
||||
const doUseAccountV3 = buildOptions.doUseAccountV3 ?? false;
|
||||
|
||||
return {
|
||||
bundler:
|
||||
resolvedViteConfig !== undefined
|
||||
@ -606,10 +612,10 @@ export function getBuildContext(params: {
|
||||
}
|
||||
|
||||
const keycloakVersionRange: KeycloakVersionRange = (() => {
|
||||
const doesImplementAccountTheme =
|
||||
recordIsImplementedByThemeType.account;
|
||||
const doesImplementAccountV1Theme =
|
||||
!doUseAccountV3 && recordIsImplementedByThemeType.account;
|
||||
|
||||
if (doesImplementAccountTheme) {
|
||||
if (doesImplementAccountV1Theme) {
|
||||
const keycloakVersionRange = (() => {
|
||||
if (buildForKeycloakMajorVersionNumber <= 21) {
|
||||
return "21-and-below" as const;
|
||||
@ -631,7 +637,7 @@ export function getBuildContext(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountTheme
|
||||
KeycloakVersionRange.WithAccountV1Theme
|
||||
>
|
||||
>();
|
||||
|
||||
@ -648,7 +654,7 @@ export function getBuildContext(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountTheme
|
||||
KeycloakVersionRange.WithoutAccountV1Theme
|
||||
>
|
||||
>();
|
||||
|
||||
@ -696,7 +702,7 @@ export function getBuildContext(params: {
|
||||
const jarTargets_default = (() => {
|
||||
const jarTargets: BuildContext["jarTargets"] = [];
|
||||
|
||||
if (recordIsImplementedByThemeType.account) {
|
||||
if (!doUseAccountV3 && recordIsImplementedByThemeType.account) {
|
||||
for (const keycloakVersionRange of [
|
||||
"21-and-below",
|
||||
"23",
|
||||
@ -706,7 +712,7 @@ export function getBuildContext(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithAccountTheme
|
||||
KeycloakVersionRange.WithAccountV1Theme
|
||||
>
|
||||
>(true);
|
||||
jarTargets.push({
|
||||
@ -723,7 +729,7 @@ export function getBuildContext(params: {
|
||||
assert<
|
||||
Equals<
|
||||
typeof keycloakVersionRange,
|
||||
KeycloakVersionRange.WithoutAccountTheme
|
||||
KeycloakVersionRange.WithoutAccountV1Theme
|
||||
>
|
||||
>(true);
|
||||
jarTargets.push({
|
||||
@ -742,8 +748,9 @@ export function getBuildContext(params: {
|
||||
}
|
||||
|
||||
if (
|
||||
buildOptions.keycloakVersionTargets.hasAccountTheme !==
|
||||
recordIsImplementedByThemeType.account
|
||||
buildOptions.keycloakVersionTargets.hasAccountTheme !== doUseAccountV3
|
||||
? false
|
||||
: recordIsImplementedByThemeType.account
|
||||
) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
@ -863,6 +870,7 @@ export function getBuildContext(params: {
|
||||
}
|
||||
|
||||
return jarTargets;
|
||||
})()
|
||||
})(),
|
||||
doUseAccountV3
|
||||
};
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||
uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme",
|
||||
onArchiveFile: async params => {
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
@ -231,6 +231,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
"fonts",
|
||||
"PatternFlyIcons-webfont.woff"
|
||||
),
|
||||
pathJoin("patternfly", "dist", "img", "bg-login.jpg"),
|
||||
pathJoin("jquery", "dist", "jquery.min.js")
|
||||
];
|
||||
}
|
||||
|
15
src/bin/tools/assertNoPnpmDlx.ts
Normal file
15
src/bin/tools/assertNoPnpmDlx.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { sep as pathSep } from "path";
|
||||
import chalk from "chalk";
|
||||
|
||||
export function assertNoPnpmDlx() {
|
||||
if (__dirname.includes(`${pathSep}pnpm${pathSep}dlx${pathSep}`)) {
|
||||
console.log(
|
||||
[
|
||||
chalk.red("Please don't use `pnpm dlx keycloakify`"),
|
||||
"\nUse `npx keycloakify` or `pnpm exec keycloakify` instead since you want to use the keycloakify",
|
||||
"version that is installed in your project and not the latest version on NPM."
|
||||
].join(" ")
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ import { rm } from "./fs.rm";
|
||||
|
||||
export async function downloadAndExtractArchive(params: {
|
||||
url: string;
|
||||
uniqueIdOfOnOnArchiveFile: string;
|
||||
uniqueIdOfOnArchiveFile: string;
|
||||
onArchiveFile: (params: {
|
||||
fileRelativePath: string;
|
||||
readFile: () => Promise<Buffer>;
|
||||
@ -22,7 +22,7 @@ export async function downloadAndExtractArchive(params: {
|
||||
cacheDirPath: string;
|
||||
fetchOptions: FetchOptions | undefined;
|
||||
}): Promise<{ extractedDirPath: string }> {
|
||||
const { url, uniqueIdOfOnOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
||||
const { url, uniqueIdOfOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
||||
params;
|
||||
|
||||
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
|
||||
@ -63,7 +63,7 @@ export async function downloadAndExtractArchive(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const extractDirBasename = `${archiveFileBasename.split(".")[0]}_${uniqueIdOfOnOnArchiveFile}_${crypto
|
||||
const extractDirBasename = `${archiveFileBasename.split(".")[0]}_${uniqueIdOfOnArchiveFile}_${crypto
|
||||
.createHash("sha256")
|
||||
.update(onArchiveFile.toString())
|
||||
.digest("hex")
|
||||
|
@ -15,7 +15,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
displayMessage = true,
|
||||
displayRequiredFields = false,
|
||||
headerNode,
|
||||
showUsernameNode = null,
|
||||
socialProvidersNode = null,
|
||||
infoNode = null,
|
||||
documentTitle,
|
||||
@ -164,45 +163,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
{(() => {
|
||||
const node = !(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
|
||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={kcClsx("kcResetFlowIcon")}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
) : (
|
||||
<div id="kc-username" className={kcClsx("kcFormGroupClass")}>
|
||||
<label id="kc-attempted-username">{auth.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl} aria-label={msgStr("restartLoginTooltip")}>
|
||||
@ -212,8 +176,24 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
);
|
||||
|
||||
if (displayRequiredFields) {
|
||||
return (
|
||||
<div className={kcClsx("kcContentWrapperClass")}>
|
||||
<div className={clsx(kcClsx("kcLabelWrapperClass"), "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">{node}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return node;
|
||||
})()}
|
||||
</header>
|
||||
<div id="kc-content">
|
||||
<div id="kc-content-wrapper">
|
||||
|
@ -12,7 +12,6 @@ export type TemplateProps<KcContext, I18n> = {
|
||||
displayRequiredFields?: boolean;
|
||||
showAnotherWayIfPresent?: boolean;
|
||||
headerNode: ReactNode;
|
||||
showUsernameNode?: ReactNode;
|
||||
socialProvidersNode?: ReactNode;
|
||||
infoNode?: ReactNode;
|
||||
documentTitle?: string;
|
||||
|
@ -188,7 +188,7 @@ function FieldErrors(props: { attribute: Attribute; displayableErrors: FormField
|
||||
.filter(error => error.fieldIndex === fieldIndex)
|
||||
.map(({ errorMessage }, i, arr) => (
|
||||
<Fragment key={i}>
|
||||
<span key={i}>{errorMessage}</span>
|
||||
{errorMessage}
|
||||
{arr.length - 1 !== i && <br />}
|
||||
</Fragment>
|
||||
))}
|
||||
|
@ -612,7 +612,14 @@ function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18
|
||||
return [
|
||||
{
|
||||
errorMessageStr,
|
||||
errorMessage: <span key={0}>{errorMessageStr}</span>,
|
||||
errorMessage: (
|
||||
<span
|
||||
key={0}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: errorMessageStr
|
||||
}}
|
||||
/>
|
||||
),
|
||||
fieldIndex: undefined,
|
||||
source: {
|
||||
type: "server"
|
||||
|
@ -19,7 +19,7 @@ export default function Error(props: PageProps<Extract<KcContext, { pageId: "err
|
||||
headerNode={msg("errorTitle")}
|
||||
>
|
||||
<div id="kc-error-message">
|
||||
<p className="instruction">{message.summary}</p>
|
||||
<p className="instruction" dangerouslySetInnerHTML={{ __html: message.summary }} />
|
||||
{!skipLink && client !== undefined && client.baseUrl !== undefined && (
|
||||
<p>
|
||||
<a id="backToApplication" href={client.baseUrl}>
|
||||
|
@ -16,13 +16,33 @@ export default function Info(props: PageProps<Extract<KcContext, { pageId: "info
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
displayMessage={false}
|
||||
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
||||
headerNode={
|
||||
<span
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messageHeader ?? message.summary
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<div id="kc-info-message">
|
||||
<p className="instruction">
|
||||
{message.summary}
|
||||
{requiredActions && <b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>}
|
||||
</p>
|
||||
<p
|
||||
className="instruction"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: (() => {
|
||||
let html = message.summary;
|
||||
|
||||
if (requiredActions) {
|
||||
html += "<b>";
|
||||
|
||||
html += requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",");
|
||||
|
||||
html += "</b>";
|
||||
}
|
||||
|
||||
return html;
|
||||
})()
|
||||
}}
|
||||
/>
|
||||
{(() => {
|
||||
if (skipLink) {
|
||||
return null;
|
||||
|
@ -60,9 +60,10 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
||||
href={p.loginUrl}
|
||||
>
|
||||
{p.iconClasses && <i className={clsx(kcClsx("kcCommonLogoIdP"), p.iconClasses)} aria-hidden="true"></i>}
|
||||
<span className={clsx(kcClsx("kcFormSocialAccountNameClass"), p.iconClasses && "kc-social-icon-text")}>
|
||||
{p.displayName}
|
||||
</span>
|
||||
<span
|
||||
className={clsx(kcClsx("kcFormSocialAccountNameClass"), p.iconClasses && "kc-social-icon-text")}
|
||||
dangerouslySetInnerHTML={{ __html: p.displayName }}
|
||||
></span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
@ -105,9 +106,14 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
||||
aria-invalid={messagesPerField.existsError("username", "password")}
|
||||
/>
|
||||
{messagesPerField.existsError("username", "password") && (
|
||||
<span id="input-error" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.getFirstError("username", "password")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.getFirstError("username", "password")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -128,9 +134,14 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
||||
/>
|
||||
</PasswordWrapper>
|
||||
{usernameHidden && messagesPerField.existsError("username", "password") && (
|
||||
<span id="input-error" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.getFirstError("username", "password")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.getFirstError("username", "password")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -112,9 +112,14 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
||||
/>
|
||||
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("totp")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
||||
@ -138,9 +143,14 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-label"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("userLabel")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -70,9 +70,14 @@ export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "
|
||||
aria-invalid={messagesPerField.existsError("totp")}
|
||||
/>
|
||||
{messagesPerField.existsError("totp") && (
|
||||
<span id="input-error-otp-code" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-otp-code"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("totp")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -60,9 +60,14 @@ export default function LoginPassword(props: PageProps<Extract<KcContext, { page
|
||||
</PasswordWrapper>
|
||||
|
||||
{messagesPerField.existsError("password") && (
|
||||
<span id="input-error-password" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("password")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-password"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("password")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={kcClsx("kcFormGroupClass", "kcFormSettingClass")}>
|
||||
|
@ -43,9 +43,14 @@ export default function LoginRecoveryAuthnCodeInput(props: PageProps<Extract<KcC
|
||||
autoFocus
|
||||
/>
|
||||
{messagesPerField.existsError("recoveryCodeInput") && (
|
||||
<span id="input-error" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("recoveryCodeInput")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("recoveryCodeInput")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,9 +48,14 @@ export default function LoginResetPassword(props: PageProps<Extract<KcContext, {
|
||||
aria-invalid={messagesPerField.existsError("username")}
|
||||
/>
|
||||
{messagesPerField.existsError("username") && (
|
||||
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("username")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-username"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("username")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,9 +46,14 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
|
||||
</PasswordWrapper>
|
||||
|
||||
{messagesPerField.existsError("password") && (
|
||||
<span id="input-error-password" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("password")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-password"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("password")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -74,9 +79,14 @@ export default function LoginUpdatePassword(props: PageProps<Extract<KcContext,
|
||||
</PasswordWrapper>
|
||||
|
||||
{messagesPerField.existsError("password-confirm") && (
|
||||
<span id="input-error-password-confirm" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("password-confirm")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-password-confirm"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("password-confirm")
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -118,9 +118,14 @@ function TermsAcceptance(props: {
|
||||
</div>
|
||||
{messagesPerField.existsError("termsAccepted") && (
|
||||
<div className={kcClsx("kcLabelWrapperClass")}>
|
||||
<span id="input-error-terms-accepted" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("termsAccepted")}
|
||||
</span>
|
||||
<span
|
||||
id="input-error-terms-accepted"
|
||||
className={kcClsx("kcInputErrorMessageClass")}
|
||||
aria-live="polite"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: messagesPerField.get("termsAccepted")
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -18,7 +18,7 @@ export default function SamlPostForm(props: PageProps<Extract<KcContext, { pageI
|
||||
}
|
||||
|
||||
// Storybook
|
||||
if (samlPost.url === "") {
|
||||
if (samlPost.url === "#") {
|
||||
alert("In a real Keycloak the user would be redirected immediately");
|
||||
return;
|
||||
}
|
||||
|
80
stories/account/pages/Applications.stories.tsx
Normal file
80
stories/account/pages/Applications.stories.tsx
Normal file
@ -0,0 +1,80 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "applications.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "account/applications.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
pageId: "applications.ftl",
|
||||
applications: {
|
||||
applications: [
|
||||
{
|
||||
realmRolesAvailable: [
|
||||
{
|
||||
name: "realmRoleName1",
|
||||
description: "realm role description 1"
|
||||
},
|
||||
{
|
||||
name: "realmRoleName2",
|
||||
description: "realm role description 2"
|
||||
}
|
||||
],
|
||||
resourceRolesAvailable: {
|
||||
resource1: [
|
||||
{
|
||||
roleName: "Resource Role Name 1",
|
||||
roleDescription: "Resource role 1 description",
|
||||
clientName: "Client Name 1",
|
||||
clientId: "client1"
|
||||
}
|
||||
],
|
||||
resource2: [
|
||||
{
|
||||
roleName: "Resource Role Name 2",
|
||||
clientName: "Client Name 1",
|
||||
clientId: "client1"
|
||||
}
|
||||
]
|
||||
},
|
||||
additionalGrants: ["grant1", "grant2"],
|
||||
clientScopesGranted: ["scope1", "scope2"],
|
||||
effectiveUrl: "#",
|
||||
client: {
|
||||
clientId: "application1",
|
||||
name: "Application 1",
|
||||
consentRequired: true
|
||||
}
|
||||
},
|
||||
{
|
||||
realmRolesAvailable: [
|
||||
{
|
||||
name: "Realm Role Name 1"
|
||||
}
|
||||
],
|
||||
resourceRolesAvailable: {},
|
||||
additionalGrants: [],
|
||||
clientScopesGranted: [],
|
||||
effectiveUrl: "#",
|
||||
client: {
|
||||
clientId: "application2",
|
||||
name: "Application 2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
@ -212,7 +212,7 @@ export const WithErrorMessage: Story = {
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
message: {
|
||||
summary: "The time allotted for the connection has elapsed. The login process will restart from the beginning.",
|
||||
summary: "The time allotted for the connection has elapsed.<br/>The login process will restart from the beginning.",
|
||||
type: "error"
|
||||
}
|
||||
}}
|
||||
|
@ -396,6 +396,7 @@ describe("css replacer", () => {
|
||||
}
|
||||
`,
|
||||
cssFileRelativeDirPath: "assets/",
|
||||
isAccountV3: false,
|
||||
buildContext: {
|
||||
urlPathname: undefined
|
||||
}
|
||||
@ -434,6 +435,7 @@ describe("css replacer", () => {
|
||||
}
|
||||
`,
|
||||
cssFileRelativeDirPath: "assets/",
|
||||
isAccountV3: false,
|
||||
buildContext: {
|
||||
urlPathname: "/a/b/"
|
||||
}
|
||||
@ -472,6 +474,7 @@ describe("css replacer", () => {
|
||||
}
|
||||
`,
|
||||
cssFileRelativeDirPath: undefined,
|
||||
isAccountV3: false,
|
||||
buildContext: {
|
||||
urlPathname: "/a/b/"
|
||||
}
|
||||
@ -510,6 +513,7 @@ describe("css replacer", () => {
|
||||
}
|
||||
`,
|
||||
cssFileRelativeDirPath: undefined,
|
||||
isAccountV3: false,
|
||||
buildContext: {
|
||||
urlPathname: undefined
|
||||
}
|
||||
|
Reference in New Issue
Block a user