Compare commits
63 Commits
v10.0.0-rc
...
v10.0.0-rc
Author | SHA1 | Date | |
---|---|---|---|
e2f5eb79ad | |||
b6c8e9bca0 | |||
573839019e | |||
815bf10ae0 | |||
7c257d97a7 | |||
59f8814660 | |||
1a6993099f | |||
f62ded3c8e | |||
4eca6366cc | |||
51a45b355d | |||
e5765cb902 | |||
6e922d2033 | |||
5d1695ada8 | |||
6e3ce29067 | |||
2b9bbc4cef | |||
9557145f72 | |||
249877b9c5 | |||
ff2321fde5 | |||
1edd6e4193 | |||
c7d47f128e | |||
14cb07efb2 | |||
a51724208c | |||
050e2b2b99 | |||
3706f15f7e | |||
bdde9162d9 | |||
99b4933536 | |||
c5caf7e0da | |||
bcc5308cfb | |||
9fb902db5c | |||
7461e38034 | |||
dccd85a151 | |||
910604fdad | |||
508cb9158e | |||
915c500d32 | |||
60bd6621c8 | |||
b5f6262763 | |||
2b8c4422de | |||
a686432c65 | |||
449e625877 | |||
1ac07dafde | |||
3878e28b56 | |||
cf6bc8666b | |||
f76063eb40 | |||
ed52c5824d | |||
9333400322 | |||
3689cfcc0d | |||
b73eceb535 | |||
5dc3453fc9 | |||
cef1139a4b | |||
ac96959947 | |||
4d73d877ba | |||
9f1186302e | |||
319dcc0d15 | |||
e99fdb8561 | |||
f37a342a63 | |||
09a039894d | |||
3efbb1a9fd | |||
920ee62ee3 | |||
1ace44fe31 | |||
a60f05415b | |||
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
|
||||
|
||||
|
12
package.json
12
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.0-rc.83",
|
||||
"version": "10.0.0-rc.102",
|
||||
"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"
|
||||
@ -41,14 +40,14 @@
|
||||
"!dist/bin/",
|
||||
"dist/bin/main.js",
|
||||
"dist/bin/*.index.js",
|
||||
"!dist/bin/shared/*.js",
|
||||
"dist/bin/*.node",
|
||||
"dist/bin/shared/constants.js",
|
||||
"dist/bin/shared/*.d.ts",
|
||||
"dist/bin/shared/*.js.map",
|
||||
"!dist/vite-plugin/",
|
||||
"dist/vite-plugin/index.js",
|
||||
"dist/vite-plugin/index.d.ts",
|
||||
"dist/vite-plugin/vite-plugin.d.ts",
|
||||
"dist/vite-plugin/index.js"
|
||||
"dist/vite-plugin/vite-plugin.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"keycloak",
|
||||
@ -62,9 +61,6 @@
|
||||
"bluehats"
|
||||
],
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"peerDependencies": {
|
||||
"react": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"tsafe": "^1.6.6"
|
||||
},
|
||||
|
@ -16,7 +16,7 @@ if (fs.existsSync(join("dist", "bin", "main.original.js"))) {
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "bin")).forEach(fileBasename => {
|
||||
if (/[0-9]\.index.js/.test(fileBasename)) {
|
||||
if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) {
|
||||
fs.rmSync(join("dist", "bin", fileBasename));
|
||||
}
|
||||
});
|
||||
@ -111,9 +111,10 @@ run(
|
||||
)}`
|
||||
);
|
||||
|
||||
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename =>
|
||||
assert(!fileBasename.endsWith(".index.js"))
|
||||
);
|
||||
fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => {
|
||||
assert(!fileBasename.endsWith(".index.js"));
|
||||
assert(!fileBasename.endsWith(".node"));
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
srcDirPath: join("dist", "ncc_out"),
|
||||
|
@ -65,11 +65,14 @@ async function main() {
|
||||
fs
|
||||
.readFileSync(pathJoin(baseThemeDirPath, filePath))
|
||||
.toString("utf8")
|
||||
)
|
||||
).map(([key, value]: any) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value.replace(/''/g, "'")
|
||||
])
|
||||
) as Record<string, string>
|
||||
)
|
||||
.map(([key, value]) => [key, value.replace(/''/g, "'")])
|
||||
.map(([key, value]) => [
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value
|
||||
])
|
||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -10,14 +10,16 @@ fs.rmSync(".yarn_home", { recursive: true, force: true });
|
||||
run("yarn install");
|
||||
run("yarn build");
|
||||
|
||||
fs.rmSync(join("..", "keycloakify-starter", "node_modules"), {
|
||||
const starterName = "keycloakify-starter";
|
||||
|
||||
fs.rmSync(join("..", starterName, "node_modules"), {
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
|
||||
run("yarn install", { cwd: join("..", "keycloakify-starter") });
|
||||
run("yarn install", { cwd: join("..", starterName) });
|
||||
|
||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
|
||||
run(`npx tsx ${join("scripts", "link-in-app.ts")} ${starterName}`);
|
||||
|
||||
startRebuildOnSrcChange();
|
||||
|
||||
|
@ -28,9 +28,13 @@ export function startRebuildOnSrcChange() {
|
||||
|
||||
console.log(chalk.green("Watching for changes in src/"));
|
||||
|
||||
chokidar.watch(["src", "stories"], { ignoreInitial: true }).on("all", async () => {
|
||||
await waitForDebounce();
|
||||
chokidar
|
||||
.watch(["src", "stories"], { ignoreInitial: true })
|
||||
.on("all", async (event, path) => {
|
||||
console.log(chalk.bold(`${event}: ${path}`));
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
await waitForDebounce();
|
||||
|
||||
runYarnBuild();
|
||||
});
|
||||
}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
||||
|
@ -3,8 +3,7 @@ import { assert } from "tsafe/assert";
|
||||
import messages_fallbackLanguage from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import type { KcContext } from "../KcContext";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type KcContextLike = {
|
||||
locale?: {
|
||||
|
@ -2,4 +2,3 @@ import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||
export type { MessageKey, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
export { fallbackLanguageTag } from "./i18n";
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from "./i18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
const { getI18n } = createGetI18n(extraMessages);
|
||||
const { getI18n } = createGetI18n(messageBundle);
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
@ -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>
|
||||
|
@ -81,7 +81,8 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
)
|
||||
)
|
||||
.toString("utf8")
|
||||
.replace('import React from "react";\n', "");
|
||||
.replace('import React from "react";\n', "")
|
||||
.replace(/from "[./]+dist\//, 'from "keycloakify/');
|
||||
|
||||
{
|
||||
const targetDirPath = pathDirname(targetFilePath);
|
||||
|
@ -1,6 +1,12 @@
|
||||
import cheerio from "cheerio";
|
||||
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import {
|
||||
replaceImportsInJsCode,
|
||||
BuildContextLike as BuildContextLike_replaceImportsInJsCode
|
||||
} from "../replacers/replaceImportsInJsCode";
|
||||
import {
|
||||
replaceImportsInCssCode,
|
||||
BuildContextLike as BuildContextLike_replaceImportsInCssCode
|
||||
} from "../replacers/replaceImportsInCssCode";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import type { BuildContext } from "../../shared/buildContext";
|
||||
@ -12,14 +18,12 @@ import {
|
||||
} from "../../shared/constants";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
|
||||
export type BuildContextLike = {
|
||||
bundler: "vite" | "webpack";
|
||||
themeVersion: string;
|
||||
urlPathname: string | undefined;
|
||||
projectBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_replaceImportsInJsCode &
|
||||
BuildContextLike_replaceImportsInCssCode & {
|
||||
urlPathname: string | undefined;
|
||||
themeVersion: string;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
|
@ -211,8 +211,6 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#continue>
|
||||
</#if>
|
||||
|
||||
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
|
||||
|
||||
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
|
||||
<#if (
|
||||
["register.ftl", "register-user-profile.ftl", "terms.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
|
||||
@ -228,6 +226,8 @@ function decodeHtmlEntities(htmlStr){
|
||||
<#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]>
|
||||
</#attempt>
|
||||
</#if>
|
||||
|
||||
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
|
||||
|
||||
<#attempt>
|
||||
<#if !object[key]??>
|
||||
|
@ -7,13 +7,13 @@ import {
|
||||
lastKeycloakVersionWithAccountV1,
|
||||
accountV1ThemeName
|
||||
} from "../../shared/constants";
|
||||
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
|
||||
import {
|
||||
downloadKeycloakDefaultTheme,
|
||||
BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme
|
||||
} from "../../shared/downloadKeycloakDefaultTheme";
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme;
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
import type { ThemeType } from "../../shared/constants";
|
||||
import { type ThemeType, fallbackLanguageTag } from "../../shared/constants";
|
||||
import { crawl } from "../../tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
import { readFileSync } from "fs";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
|
||||
import * as recast from "recast";
|
||||
import * as babelParser from "@babel/parser";
|
||||
import babelGenerate from "@babel/generator";
|
||||
@ -11,6 +9,7 @@ import * as babelTypes from "@babel/types";
|
||||
import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile";
|
||||
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe/assert";
|
||||
|
||||
export function generateMessageProperties(params: {
|
||||
themeSrcDirPath: string;
|
||||
@ -18,32 +17,92 @@ export function generateMessageProperties(params: {
|
||||
}): { languageTag: string; propertiesFileSource: string }[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
readFileSync(file).toString("utf8").includes("createUseI18n")
|
||||
const baseMessagesDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"baseMessages"
|
||||
);
|
||||
|
||||
const extraMessages = files
|
||||
.map(file => {
|
||||
const root = recast.parse(readFileSync(file).toString("utf8"), {
|
||||
const baseMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
fs
|
||||
.readdirSync(baseMessagesDirPath)
|
||||
.filter(baseName => baseName !== "index.ts")
|
||||
.map(basename => ({
|
||||
languageTag: basename.replace(/\.ts$/, ""),
|
||||
filePath: pathJoin(baseMessagesDirPath, basename)
|
||||
}))
|
||||
.map(({ languageTag, filePath }) => {
|
||||
const lines = fs
|
||||
.readFileSync(filePath)
|
||||
.toString("utf8")
|
||||
.split(/\r?\n/);
|
||||
|
||||
let messagesJson = "{";
|
||||
|
||||
let isInDeclaration = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!isInDeclaration) {
|
||||
if (line.startsWith("const messages")) {
|
||||
isInDeclaration = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith("}")) {
|
||||
messagesJson += "}";
|
||||
break;
|
||||
}
|
||||
|
||||
messagesJson += line;
|
||||
}
|
||||
|
||||
const messages = JSON.parse(messagesJson) as Record<string, string>;
|
||||
|
||||
return [languageTag, messages];
|
||||
})
|
||||
);
|
||||
|
||||
const { i18nTsFilePath } = (() => {
|
||||
let files = crawl({
|
||||
dirPath: pathJoin(themeSrcDirPath, themeType),
|
||||
returnedPathsType: "absolute"
|
||||
});
|
||||
|
||||
files = files.filter(file => {
|
||||
const regex = /\.(js|ts|tsx)$/;
|
||||
return regex.test(file);
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => {
|
||||
const regex = /\.i18n\.(ts|js|tsx)$/;
|
||||
const aIsI18nFile = regex.test(a);
|
||||
const bIsI18nFile = regex.test(b);
|
||||
return aIsI18nFile === bIsI18nFile ? 0 : aIsI18nFile ? -1 : 1;
|
||||
});
|
||||
|
||||
files = files.sort((a, b) => a.length - b.length);
|
||||
|
||||
files = files.filter(file =>
|
||||
fs.readFileSync(file).toString("utf8").includes("createUseI18n(")
|
||||
);
|
||||
|
||||
const i18nTsFilePath: string | undefined = files[0];
|
||||
|
||||
return { i18nTsFilePath };
|
||||
})();
|
||||
|
||||
const messageBundle: { [languageTag: string]: Record<string, string> } | undefined =
|
||||
(() => {
|
||||
if (i18nTsFilePath === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const root = recast.parse(fs.readFileSync(i18nTsFilePath).toString("utf8"), {
|
||||
parser: {
|
||||
parse: (code: string) =>
|
||||
babelParser.parse(code, {
|
||||
@ -55,7 +114,7 @@ export function generateMessageProperties(params: {
|
||||
}
|
||||
});
|
||||
|
||||
const codes: string[] = [];
|
||||
let messageBundleDeclarationTsCode: string | undefined = undefined;
|
||||
|
||||
recast.visit(root, {
|
||||
visitCallExpression: function (path) {
|
||||
@ -63,111 +122,71 @@ export function generateMessageProperties(params: {
|
||||
path.node.callee.type === "Identifier" &&
|
||||
path.node.callee.name === "createUseI18n"
|
||||
) {
|
||||
codes.push(babelGenerate(path.node.arguments[0] as any).code);
|
||||
messageBundleDeclarationTsCode = babelGenerate(
|
||||
path.node.arguments[0] as any
|
||||
).code;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.traverse(path);
|
||||
}
|
||||
});
|
||||
|
||||
return codes;
|
||||
})
|
||||
.flat()
|
||||
.map(code => {
|
||||
let extraMessages: {
|
||||
assert(messageBundleDeclarationTsCode !== undefined);
|
||||
|
||||
let messageBundle: {
|
||||
[languageTag: string]: Record<string, string>;
|
||||
} = {};
|
||||
|
||||
try {
|
||||
eval(`${symToStr({ extraMessages })} = ${code}`);
|
||||
eval(
|
||||
`${symToStr({ messageBundle })} = ${messageBundleDeclarationTsCode}`
|
||||
);
|
||||
} catch {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: Make sure that the first argument of createUseI18n can be evaluated in a javascript",
|
||||
"runtime where only the node globals are available.",
|
||||
"WARNING: Make sure the messageBundle your provided as argument of createUseI18n can be statically evaluated.",
|
||||
"This is important because we need to put your i18n messages in messages_*.properties files",
|
||||
"or they won't be available server side.",
|
||||
"\n",
|
||||
"The following code could not be evaluated:",
|
||||
"\n",
|
||||
code
|
||||
messageBundleDeclarationTsCode
|
||||
].join(" ")
|
||||
);
|
||||
}
|
||||
|
||||
return extraMessages;
|
||||
});
|
||||
return messageBundle;
|
||||
})();
|
||||
|
||||
const languageTags = [
|
||||
...extraMessages.map(extraMessage => Object.keys(extraMessage)).flat(),
|
||||
...fs
|
||||
.readdirSync(
|
||||
pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"src",
|
||||
themeType,
|
||||
"i18n",
|
||||
"baseMessages"
|
||||
)
|
||||
)
|
||||
.filter(baseName => baseName !== "index.ts")
|
||||
.map(baseName => baseName.replace(/\.ts$/, ""))
|
||||
].reduce(...removeDuplicates<string>());
|
||||
|
||||
const keyValueMapByLanguageTag: Record<string, Record<string, string>> = {};
|
||||
|
||||
for (const languageTag of languageTags) {
|
||||
const keyValueMap: Record<string, string> = {
|
||||
termsText: ""
|
||||
};
|
||||
|
||||
for (const extraMessage of extraMessages) {
|
||||
const keyValueMap_i = extraMessage[languageTag];
|
||||
|
||||
if (keyValueMap_i === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(keyValueMap_i)) {
|
||||
if (key !== "termsText" && keyValueMap[key] !== undefined) {
|
||||
console.warn(
|
||||
[
|
||||
"WARNING: The following key is defined multiple times:",
|
||||
"\n",
|
||||
key,
|
||||
"\n",
|
||||
"The following value will be ignored:",
|
||||
"\n",
|
||||
value,
|
||||
"\n",
|
||||
"The following value was already defined:",
|
||||
"\n",
|
||||
keyValueMap[key]
|
||||
].join(" ")
|
||||
);
|
||||
continue;
|
||||
const mergedMessageBundle: { [languageTag: string]: Record<string, string> } =
|
||||
Object.fromEntries(
|
||||
Object.entries(baseMessageBundle).map(([languageTag, messages]) => [
|
||||
languageTag,
|
||||
{
|
||||
...messages,
|
||||
...(messageBundle === undefined
|
||||
? {}
|
||||
: messageBundle[languageTag] ??
|
||||
messageBundle[fallbackLanguageTag] ??
|
||||
messageBundle[Object.keys(messageBundle)[0]] ??
|
||||
{})
|
||||
}
|
||||
])
|
||||
);
|
||||
|
||||
keyValueMap[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
keyValueMapByLanguageTag[languageTag] = keyValueMap;
|
||||
}
|
||||
|
||||
const out: { languageTag: string; propertiesFileSource: string }[] = [];
|
||||
|
||||
for (const [languageTag, keyValueMap] of Object.entries(keyValueMapByLanguageTag)) {
|
||||
const propertiesFileSource = Object.entries(keyValueMap)
|
||||
.map(([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`)
|
||||
.join("\n");
|
||||
|
||||
out.push({
|
||||
const messageProperties: { languageTag: string; propertiesFileSource: string }[] =
|
||||
Object.entries(mergedMessageBundle).map(([languageTag, messages]) => ({
|
||||
languageTag,
|
||||
propertiesFileSource: ["", "parent=base", "", propertiesFileSource, ""].join(
|
||||
"\n"
|
||||
)
|
||||
});
|
||||
}
|
||||
propertiesFileSource: [
|
||||
"",
|
||||
...(themeType !== "account" ? ["parent=base"] : []),
|
||||
...Object.entries(messages).map(
|
||||
([key, value]) => `${key}=${escapeStringForPropertiesFile(value)}`
|
||||
),
|
||||
""
|
||||
].join("\n")
|
||||
}));
|
||||
|
||||
return out;
|
||||
return messageProperties;
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
recordIsImplementedByThemeType: BuildContext["recordIsImplementedByThemeType"];
|
||||
themeSrcDirPath: string;
|
||||
bundler: { type: "vite" } | { type: "webpack" };
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -113,7 +114,7 @@ export async function generateResourcesForMainTheme(params: {
|
||||
);
|
||||
|
||||
if (fs.existsSync(dirPath)) {
|
||||
assert(buildContext.bundler === "webpack");
|
||||
assert(buildContext.bundler.type === "webpack");
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
|
@ -34,10 +34,7 @@ export function readExtraPagesNames(params: {
|
||||
const rawSourceFile = fs.readFileSync(candidateFilPath).toString("utf8");
|
||||
|
||||
extraPages.push(
|
||||
...Array.from(
|
||||
rawSourceFile.matchAll(/["']?pageId["']?\s*:\s*["']([^.]+.ftl)["']/g),
|
||||
m => m[1]
|
||||
)
|
||||
...Array.from(rawSourceFile.matchAll(/["']([^.\s]+.ftl)["']:/g), m => m[1])
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,15 @@ export function readFieldNameUsage(params: {
|
||||
}): string[] {
|
||||
const { themeSrcDirPath, themeType } = params;
|
||||
|
||||
const fieldNames = new Set<string>();
|
||||
// NOTE: We pre-populate with the synthetic user attributes defined in useUserProfileForm (can't be parsed automatically)
|
||||
const fieldNames = new Set<string>([
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"username",
|
||||
"password",
|
||||
"password-confirm"
|
||||
]);
|
||||
|
||||
for (const srcDirPath of [
|
||||
pathJoin(getThisCodebaseRootDirPath(), "src", themeType),
|
||||
|
@ -85,7 +85,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
});
|
||||
|
||||
run_post_build_script: {
|
||||
if (buildContext.bundler !== "vite") {
|
||||
if (buildContext.bundler.type !== "vite") {
|
||||
break run_post_build_script;
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@ export type BuildContextLike = {
|
||||
projectBuildDirPath: string;
|
||||
assetsDirPath: string;
|
||||
urlPathname: string | undefined;
|
||||
bundler: "vite" | "webpack";
|
||||
bundler: { type: "vite" } | { type: "webpack" };
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -20,7 +20,7 @@ export function replaceImportsInJsCode(params: {
|
||||
const { jsCode, buildContext } = params;
|
||||
|
||||
const { fixedJsCode } = (() => {
|
||||
switch (buildContext.bundler) {
|
||||
switch (buildContext.bundler.type) {
|
||||
case "vite":
|
||||
return replaceImportsInJsCode_vite({
|
||||
jsCode,
|
||||
|
@ -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,7 +1,12 @@
|
||||
import { parse as urlParse } from "url";
|
||||
import { join as pathJoin } from "path";
|
||||
import {
|
||||
join as pathJoin,
|
||||
sep as pathSep,
|
||||
relative as pathRelative,
|
||||
resolve as pathResolve,
|
||||
dirname as pathDirname
|
||||
} from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
import { getNpmWorkspaceRootDirPath } from "../tools/getNpmWorkspaceRootDirPath";
|
||||
import type { CliCommandOptions } from "../main";
|
||||
import { z } from "zod";
|
||||
import * as fs from "fs";
|
||||
@ -21,9 +26,9 @@ import { type ThemeType } from "./constants";
|
||||
import { id } from "tsafe/id";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import chalk from "chalk";
|
||||
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
|
||||
|
||||
export type BuildContext = {
|
||||
bundler: "vite" | "webpack";
|
||||
themeVersion: string;
|
||||
themeNames: [string, ...string[]];
|
||||
extraThemeProperties: string[] | undefined;
|
||||
@ -40,7 +45,7 @@ export type BuildContext = {
|
||||
* In this case the urlPathname will be "/my-app/" */
|
||||
urlPathname: string | undefined;
|
||||
assetsDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
fetchOptions: ProxyFetchOptions;
|
||||
kcContextExclusionsFtlCode: string | undefined;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
themeSrcDirPath: string;
|
||||
@ -49,6 +54,15 @@ export type BuildContext = {
|
||||
keycloakVersionRange: KeycloakVersionRange;
|
||||
jarFileBasename: string;
|
||||
}[];
|
||||
bundler:
|
||||
| {
|
||||
type: "vite";
|
||||
}
|
||||
| {
|
||||
type: "webpack";
|
||||
packageJsonDirPath: string;
|
||||
packageJsonScripts: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
||||
export type BuildOptions = {
|
||||
@ -90,15 +104,54 @@ export function getBuildContext(params: {
|
||||
}): BuildContext {
|
||||
const { cliCommandOptions } = params;
|
||||
|
||||
const projectDirPath = (() => {
|
||||
if (cliCommandOptions.projectDirPath === undefined) {
|
||||
return process.cwd();
|
||||
const projectDirPath =
|
||||
cliCommandOptions.projectDirPath !== undefined
|
||||
? getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
})
|
||||
: process.cwd();
|
||||
|
||||
const { themeSrcDirPath } = (() => {
|
||||
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
if (themeSrcDirPath !== undefined) {
|
||||
return { themeSrcDirPath };
|
||||
}
|
||||
|
||||
return getAbsoluteAndInOsFormatPath({
|
||||
pathIsh: cliCommandOptions.projectDirPath,
|
||||
cwd: process.cwd()
|
||||
});
|
||||
for (const themeType of [...themeTypes, "email"]) {
|
||||
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
return { themeSrcDirPath: srcDirPath };
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
`Can't locate your Keycloak theme source directory in .${pathSep}${pathRelative(process.cwd(), srcDirPath)}`,
|
||||
`Make sure to either use the Keycloakify CLI in the root of your Keycloakify project or use the --project CLI option`,
|
||||
`If you are collocating your Keycloak theme with your app you must have a directory named 'keycloak-theme' or 'keycloak_theme' in your 'src' directory`
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const { resolvedViteConfig } = (() => {
|
||||
@ -135,6 +188,42 @@ export function getBuildContext(params: {
|
||||
return { resolvedViteConfig };
|
||||
})();
|
||||
|
||||
const packageJsonFilePath = (function getPackageJSonDirPath(upCount: number): string {
|
||||
const dirPath = pathResolve(
|
||||
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
|
||||
);
|
||||
|
||||
assert(dirPath !== pathSep, "Root package.json not found");
|
||||
|
||||
success: {
|
||||
const packageJsonFilePath = pathJoin(dirPath, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageJsonFilePath)) {
|
||||
break success;
|
||||
}
|
||||
|
||||
const parsedPackageJson = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
dependencies: z.record(z.string()).optional(),
|
||||
devDependencies: z.record(z.string()).optional()
|
||||
})
|
||||
.parse(JSON.parse(fs.readFileSync(packageJsonFilePath).toString("utf8")));
|
||||
|
||||
if (
|
||||
parsedPackageJson.dependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.devDependencies?.keycloakify === undefined &&
|
||||
parsedPackageJson.name !== "keycloakify" // NOTE: For local storybook build
|
||||
) {
|
||||
break success;
|
||||
}
|
||||
|
||||
return packageJsonFilePath;
|
||||
}
|
||||
|
||||
return getPackageJSonDirPath(upCount + 1);
|
||||
})(0);
|
||||
|
||||
const parsedPackageJson = (() => {
|
||||
type BuildOptions_packageJson = BuildOptions & {
|
||||
projectBuildDirPath?: string;
|
||||
@ -143,14 +232,14 @@ export function getBuildContext(params: {
|
||||
};
|
||||
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
name?: string;
|
||||
version?: string;
|
||||
homepage?: string;
|
||||
keycloakify?: BuildOptions_packageJson;
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
name: z.string(),
|
||||
name: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
homepage: z.string().optional(),
|
||||
keycloakify: id<z.ZodType<BuildOptions_packageJson>>(
|
||||
@ -228,10 +317,16 @@ export function getBuildContext(params: {
|
||||
assert<Equals<Got, Expected>>();
|
||||
}
|
||||
|
||||
const configurationPackageJsonFilePath = (() => {
|
||||
const rootPackageJsonFilePath = pathJoin(projectDirPath, "package.json");
|
||||
|
||||
return fs.existsSync(rootPackageJsonFilePath)
|
||||
? rootPackageJsonFilePath
|
||||
: packageJsonFilePath;
|
||||
})();
|
||||
|
||||
return zParsedPackageJson.parse(
|
||||
JSON.parse(
|
||||
fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")
|
||||
)
|
||||
JSON.parse(fs.readFileSync(configurationPackageJsonFilePath).toString("utf8"))
|
||||
);
|
||||
})();
|
||||
|
||||
@ -240,47 +335,6 @@ export function getBuildContext(params: {
|
||||
...resolvedViteConfig?.buildOptions
|
||||
};
|
||||
|
||||
const { themeSrcDirPath } = (() => {
|
||||
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl({
|
||||
dirPath: srcDirPath,
|
||||
returnedPathsType: "relative to dirPath"
|
||||
})
|
||||
.map(fileRelativePath => {
|
||||
for (const themeSrcDirBasename of ["keycloak-theme", "keycloak_theme"]) {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
if (split.length === 2) {
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
if (themeSrcDirPath !== undefined) {
|
||||
return { themeSrcDirPath };
|
||||
}
|
||||
|
||||
for (const themeType of [...themeTypes, "email"]) {
|
||||
if (!fs.existsSync(pathJoin(srcDirPath, themeType))) {
|
||||
continue;
|
||||
}
|
||||
return { themeSrcDirPath: srcDirPath };
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
"Can't locate your keycloak theme source directory.",
|
||||
"See: https://docs.keycloakify.dev/v/v10/keycloakify-in-my-app/collocation"
|
||||
].join("\n")
|
||||
)
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
})();
|
||||
|
||||
const recordIsImplementedByThemeType = objectFromEntries(
|
||||
(["login", "account", "email"] as const).map(themeType => [
|
||||
themeType,
|
||||
@ -290,12 +344,14 @@ export function getBuildContext(params: {
|
||||
|
||||
const themeNames = ((): [string, ...string[]] => {
|
||||
if (buildOptions.themeName === undefined) {
|
||||
return [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-")
|
||||
];
|
||||
return parsedPackageJson.name === undefined
|
||||
? ["keycloakify"]
|
||||
: [
|
||||
parsedPackageJson.name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-")
|
||||
];
|
||||
}
|
||||
|
||||
if (typeof buildOptions.themeName === "string") {
|
||||
@ -328,15 +384,29 @@ export function getBuildContext(params: {
|
||||
return pathJoin(projectDirPath, resolvedViteConfig.buildDir);
|
||||
})();
|
||||
|
||||
const { npmWorkspaceRootDirPath } = getNpmWorkspaceRootDirPath({
|
||||
projectDirPath,
|
||||
dependencyExpected: "keycloakify"
|
||||
});
|
||||
|
||||
const bundler = resolvedViteConfig !== undefined ? "vite" : "webpack";
|
||||
|
||||
return {
|
||||
bundler,
|
||||
bundler:
|
||||
resolvedViteConfig !== undefined
|
||||
? { type: "vite" }
|
||||
: (() => {
|
||||
const { scripts } = z
|
||||
.object({
|
||||
scripts: z.record(z.string()).optional()
|
||||
})
|
||||
.parse(
|
||||
JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
type: "webpack",
|
||||
packageJsonDirPath: pathDirname(packageJsonFilePath),
|
||||
packageJsonScripts: scripts ?? {}
|
||||
};
|
||||
})(),
|
||||
themeVersion: buildOptions.themeVersion ?? parsedPackageJson.version ?? "0.0.0",
|
||||
themeNames,
|
||||
extraThemeProperties: buildOptions.extraThemeProperties,
|
||||
@ -413,7 +483,11 @@ export function getBuildContext(params: {
|
||||
});
|
||||
}
|
||||
|
||||
return pathJoin(npmWorkspaceRootDirPath, "node_modules", ".cache");
|
||||
return pathJoin(
|
||||
pathDirname(packageJsonFilePath),
|
||||
"node_modules",
|
||||
".cache"
|
||||
);
|
||||
})(),
|
||||
"keycloakify"
|
||||
);
|
||||
@ -462,7 +536,6 @@ export function getBuildContext(params: {
|
||||
|
||||
return pathJoin(projectBuildDirPath, resolvedViteConfig.assetsDir);
|
||||
})(),
|
||||
npmWorkspaceRootDirPath,
|
||||
kcContextExclusionsFtlCode: (() => {
|
||||
if (buildOptions.kcContextExclusionsFtl === undefined) {
|
||||
return undefined;
|
||||
@ -482,6 +555,33 @@ export function getBuildContext(params: {
|
||||
environmentVariables: buildOptions.environmentVariables ?? [],
|
||||
recordIsImplementedByThemeType,
|
||||
themeSrcDirPath,
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: (function callee(upCount: number): string {
|
||||
const dirPath = pathResolve(
|
||||
pathJoin(...[projectDirPath, ...Array(upCount).fill("..")])
|
||||
);
|
||||
|
||||
assert(
|
||||
dirPath !== pathSep,
|
||||
"Couldn't find a place to run 'npm config get'"
|
||||
);
|
||||
|
||||
try {
|
||||
child_process.execSync("npm config get", {
|
||||
cwd: dirPath,
|
||||
stdio: "ignore"
|
||||
});
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
return callee(upCount + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return dirPath;
|
||||
})(0)
|
||||
}),
|
||||
jarTargets: (() => {
|
||||
const getDefaultJarFileBasename = (range: string) =>
|
||||
`keycloak-theme-for-kc-${range}.jar`;
|
||||
|
@ -67,3 +67,5 @@ export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||
|
||||
export const containerName = "keycloak-keycloakify";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
|
@ -37,10 +37,7 @@ export async function copyKeycloakResourcesToPublic(params: {
|
||||
buildContext: {
|
||||
loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(),
|
||||
cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath),
|
||||
npmWorkspaceRootDirPath: pathRelative(
|
||||
destDirPath,
|
||||
buildContext.npmWorkspaceRootDirPath
|
||||
)
|
||||
fetchOptions: buildContext.fetchOptions
|
||||
}
|
||||
},
|
||||
null,
|
||||
|
@ -6,7 +6,7 @@ import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -23,7 +23,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||
cacheDirPath: buildContext.cacheDirPath,
|
||||
npmWorkspaceRootDirPath: buildContext.npmWorkspaceRootDirPath,
|
||||
fetchOptions: buildContext.fetchOptions,
|
||||
uniqueIdOfOnOnArchiveFile: "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")
|
||||
];
|
||||
}
|
||||
|
@ -1,16 +1,15 @@
|
||||
import * as child_process from "child_process";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import chalk from "chalk";
|
||||
import { sep as pathSep, join as pathJoin } from "path";
|
||||
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
npmWorkspaceRootDirPath: string;
|
||||
bundler: BuildContext["bundler"];
|
||||
projectBuildDirPath: string;
|
||||
};
|
||||
|
||||
@ -21,95 +20,29 @@ export async function appBuild(params: {
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const { bundler } = buildContext;
|
||||
switch (buildContext.bundler.type) {
|
||||
case "vite":
|
||||
return appBuild_vite({ buildContext });
|
||||
case "webpack":
|
||||
return appBuild_webpack({ buildContext });
|
||||
}
|
||||
}
|
||||
|
||||
const { command, args, cwd } = (() => {
|
||||
switch (bundler) {
|
||||
case "vite":
|
||||
return {
|
||||
command: "npx",
|
||||
args: ["vite", "build"],
|
||||
cwd: buildContext.projectDirPath
|
||||
};
|
||||
case "webpack": {
|
||||
for (const dirPath of [
|
||||
buildContext.projectDirPath,
|
||||
buildContext.npmWorkspaceRootDirPath
|
||||
]) {
|
||||
try {
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs
|
||||
.readFileSync(pathJoin(dirPath, "package.json"))
|
||||
.toString("utf8")
|
||||
);
|
||||
async function appBuild_vite(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
const [scriptName] =
|
||||
Object.entries(parsedPackageJson.scripts).find(
|
||||
([, scriptValue]) => {
|
||||
assert(is<string>(scriptValue));
|
||||
if (
|
||||
scriptValue.includes("webpack") &&
|
||||
scriptValue.includes("--mode production")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
assert(buildContext.bundler.type === "vite");
|
||||
|
||||
if (
|
||||
scriptValue.includes("react-scripts") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const dIsSuccess = new Deferred<boolean>();
|
||||
|
||||
if (
|
||||
scriptValue.includes("react-app-rewired") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
console.log(chalk.blue("Running: 'npx vite build'"));
|
||||
|
||||
if (
|
||||
scriptValue.includes("craco") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
scriptValue.includes("ng") &&
|
||||
scriptValue.includes("build")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
) ?? [];
|
||||
|
||||
if (scriptName === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return {
|
||||
command: "npm",
|
||||
args: ["run", scriptName],
|
||||
cwd: dirPath
|
||||
};
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
"Keycloakify was unable to determine which script is responsible for building the app."
|
||||
);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||
|
||||
const child = child_process.spawn(command, args, { cwd, shell: true });
|
||||
const child = child_process.spawn("npx", ["vite", "build"], {
|
||||
cwd: buildContext.projectDirPath,
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => {
|
||||
if (data.toString("utf8").includes("gzip:")) {
|
||||
@ -121,9 +54,127 @@ export async function appBuild(params: {
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dResult.resolve({ isSuccess: code === 0 }));
|
||||
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
||||
|
||||
const { isSuccess } = await dResult.pr;
|
||||
const isSuccess = await dIsSuccess.pr;
|
||||
|
||||
return { isAppBuildSuccess: isSuccess };
|
||||
}
|
||||
|
||||
async function appBuild_webpack(params: {
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ isAppBuildSuccess: boolean }> {
|
||||
const { buildContext } = params;
|
||||
|
||||
assert(buildContext.bundler.type === "webpack");
|
||||
|
||||
const entries = Object.entries(buildContext.bundler.packageJsonScripts).filter(
|
||||
([, scriptCommand]) => scriptCommand.includes("keycloakify build")
|
||||
);
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
[
|
||||
`You should have a script in your package.json at ${buildContext.bundler.packageJsonDirPath}`,
|
||||
`that includes the 'keycloakify build' command`
|
||||
].join(" ")
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const entry =
|
||||
entries.length === 1
|
||||
? entries[0]
|
||||
: entries.find(([scriptName]) => scriptName === "build-keycloak-theme");
|
||||
|
||||
if (entry === undefined) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
"There's multiple candidate script for building your app, name one 'build-keycloak-theme'"
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const [scriptName, scriptCommand] = entry;
|
||||
|
||||
const { appBuildSubCommands } = (() => {
|
||||
const appBuildSubCommands: string[] = [];
|
||||
|
||||
for (const subCmd of scriptCommand.split("&&").map(s => s.trim())) {
|
||||
if (subCmd.includes("keycloakify build")) {
|
||||
break;
|
||||
}
|
||||
|
||||
appBuildSubCommands.push(subCmd);
|
||||
}
|
||||
|
||||
return { appBuildSubCommands };
|
||||
})();
|
||||
|
||||
if (appBuildSubCommands.length === 0) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`Your ${scriptName} script should look like "... && keycloakify build ..."`
|
||||
)
|
||||
);
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
let commandCwd = buildContext.bundler.packageJsonDirPath;
|
||||
|
||||
for (const subCommand of appBuildSubCommands) {
|
||||
const dIsSuccess = new Deferred<boolean>();
|
||||
|
||||
const [command, ...args] = subCommand.split(" ");
|
||||
|
||||
if (command === "cd") {
|
||||
const [pathIsh] = args;
|
||||
|
||||
commandCwd = getAbsoluteAndInOsFormatPath({
|
||||
pathIsh,
|
||||
cwd: commandCwd
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Running: '${subCommand}'`));
|
||||
|
||||
const child = child_process.spawn(command, args, {
|
||||
cwd: commandCwd,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: (() => {
|
||||
const separator = pathSep === "/" ? ":" : ";";
|
||||
|
||||
return [
|
||||
pathJoin(
|
||||
buildContext.bundler.packageJsonDirPath,
|
||||
"node_modules",
|
||||
".bin"
|
||||
),
|
||||
...(process.env.PATH ?? "").split(separator)
|
||||
].join(separator);
|
||||
})()
|
||||
},
|
||||
shell: true
|
||||
});
|
||||
|
||||
child.stdout.on("data", data => process.stdout.write(data));
|
||||
|
||||
child.stderr.on("data", data => process.stderr.write(data));
|
||||
|
||||
child.on("exit", code => dIsSuccess.resolve(code === 0));
|
||||
|
||||
const isSuccess = await dIsSuccess.pr;
|
||||
|
||||
if (!isSuccess) {
|
||||
return { isAppBuildSuccess: false };
|
||||
}
|
||||
}
|
||||
|
||||
return { isAppBuildSuccess: true };
|
||||
}
|
||||
|
@ -3,12 +3,11 @@ import * as child_process from "child_process";
|
||||
import { Deferred } from "evt/tools/Deferred";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { BuildContext } from "../shared/buildContext";
|
||||
import chalk from "chalk";
|
||||
|
||||
export type BuildContextLike = {
|
||||
projectDirPath: string;
|
||||
keycloakifyBuildDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
npmWorkspaceRootDirPath: string;
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
@ -21,6 +20,8 @@ export async function keycloakifyBuild(params: {
|
||||
|
||||
const dResult = new Deferred<{ isSuccess: boolean }>();
|
||||
|
||||
console.log(chalk.blue("Running: 'npx keycloakify build'"));
|
||||
|
||||
const child = child_process.spawn("npx", ["keycloakify", "build"], {
|
||||
cwd: buildContext.projectDirPath,
|
||||
env: {
|
||||
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"id": "34c5f904-d66e-4d8f-8876-8f00d9fa9d6c",
|
||||
"realm": "myrealm",
|
||||
"displayName": "",
|
||||
"displayNameHtml": "",
|
||||
"notBefore": 0,
|
||||
"defaultSignatureAlgorithm": "RS256",
|
||||
"revokeRefreshToken": false,
|
||||
@ -1356,11 +1358,11 @@
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
@ -1431,13 +1433,13 @@
|
||||
"subComponents": {},
|
||||
"config": {
|
||||
"allowed-protocol-mapper-types": [
|
||||
"saml-user-property-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-full-name-mapper",
|
||||
"oidc-address-mapper",
|
||||
"saml-user-attribute-mapper",
|
||||
"oidc-sha256-pairwise-sub-mapper",
|
||||
"saml-role-list-mapper",
|
||||
"oidc-address-mapper",
|
||||
"oidc-usermodel-attribute-mapper",
|
||||
"saml-user-property-mapper",
|
||||
"oidc-usermodel-property-mapper"
|
||||
]
|
||||
}
|
||||
@ -2127,17 +2129,20 @@
|
||||
"dockerAuthenticationFlow": "docker auth",
|
||||
"attributes": {
|
||||
"cibaBackchannelTokenDeliveryMode": "poll",
|
||||
"cibaExpiresIn": "120",
|
||||
"cibaAuthRequestedUserHint": "login_hint",
|
||||
"oauth2DeviceCodeLifespan": "600",
|
||||
"clientOfflineSessionMaxLifespan": "0",
|
||||
"oauth2DevicePollingInterval": "5",
|
||||
"clientSessionIdleTimeout": "0",
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"userProfileEnabled": "true",
|
||||
"clientOfflineSessionIdleTimeout": "0",
|
||||
"cibaInterval": "5",
|
||||
"realmReusableOtpCode": "false"
|
||||
"realmReusableOtpCode": "false",
|
||||
"cibaExpiresIn": "120",
|
||||
"oauth2DeviceCodeLifespan": "600",
|
||||
"parRequestUriLifespan": "60",
|
||||
"clientSessionMaxLifespan": "0",
|
||||
"frontendUrl": "",
|
||||
"acr.loa.map": "{}"
|
||||
},
|
||||
"keycloakVersion": "23.0.7",
|
||||
"userManagedAccessAllowed": false,
|
||||
|
@ -121,7 +121,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
if (!isAppBuildSuccess) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
`App build failed, exiting. Try running 'npm run build' and see what's wrong.`
|
||||
`App build failed, exiting. Try building your app (e.g 'npm run build') and see what's wrong.`
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
|
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);
|
||||
}
|
||||
}
|
@ -1,12 +1,12 @@
|
||||
import fetch from "make-fetch-happen";
|
||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||
import { mkdir, unlink, writeFile, readdir, readFile } from "fs/promises";
|
||||
import { dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { extractArchive } from "../extractArchive";
|
||||
import { existsAsync } from "../fs.existsAsync";
|
||||
import { getProxyFetchOptions } from "./fetchProxyOptions";
|
||||
import { extractArchive } from "./extractArchive";
|
||||
import { existsAsync } from "./fs.existsAsync";
|
||||
|
||||
import * as crypto from "crypto";
|
||||
import { rm } from "../fs.rm";
|
||||
import { rm } from "./fs.rm";
|
||||
|
||||
export async function downloadAndExtractArchive(params: {
|
||||
url: string;
|
||||
@ -20,15 +20,10 @@ export async function downloadAndExtractArchive(params: {
|
||||
}) => Promise<void>;
|
||||
}) => Promise<void>;
|
||||
cacheDirPath: string;
|
||||
npmWorkspaceRootDirPath: string;
|
||||
fetchOptions: FetchOptions | undefined;
|
||||
}): Promise<{ extractedDirPath: string }> {
|
||||
const {
|
||||
url,
|
||||
uniqueIdOfOnOnArchiveFile,
|
||||
onArchiveFile,
|
||||
cacheDirPath,
|
||||
npmWorkspaceRootDirPath
|
||||
} = params;
|
||||
const { url, uniqueIdOfOnOnArchiveFile, onArchiveFile, cacheDirPath, fetchOptions } =
|
||||
params;
|
||||
|
||||
const archiveFileBasename = url.split("?")[0].split("/").reverse()[0];
|
||||
|
||||
@ -55,10 +50,7 @@ export async function downloadAndExtractArchive(params: {
|
||||
|
||||
await mkdir(pathDirname(archiveFilePath), { recursive: true });
|
||||
|
||||
const response = await fetch(
|
||||
url,
|
||||
await getProxyFetchOptions({ npmWorkspaceRootDirPath })
|
||||
);
|
||||
const response = await fetch(url, fetchOptions);
|
||||
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
@ -1 +0,0 @@
|
||||
export * from "./downloadAndExtractArchive";
|
@ -1,61 +1,40 @@
|
||||
import { exec as execCallback } from "child_process";
|
||||
import { readFile } from "fs/promises";
|
||||
import { type FetchOptions } from "make-fetch-happen";
|
||||
import { promisify } from "util";
|
||||
|
||||
function ensureArray<T>(arg0: T | T[]) {
|
||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||
}
|
||||
|
||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error(
|
||||
"Illegal configuration, expected a single value but found multiple: " +
|
||||
arg0.map(String).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
type NPMConfig = Record<string, string | string[]>;
|
||||
|
||||
/**
|
||||
* Get npm configuration as map
|
||||
*/
|
||||
async function getNmpConfig(params: { npmWorkspaceRootDirPath: string }) {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
const stdout = await exec("npm config get", {
|
||||
encoding: "utf8",
|
||||
cwd: npmWorkspaceRootDirPath
|
||||
}).then(({ stdout }) => stdout);
|
||||
|
||||
const npmConfigReducer = (cfg: NPMConfig, [key, value]: [string, string]) =>
|
||||
key in cfg
|
||||
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
||||
: { ...cfg, [key]: value };
|
||||
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2) as [string, string])
|
||||
.reduce(npmConfigReducer, {} as NPMConfig);
|
||||
}
|
||||
import * as child_process from "child_process";
|
||||
import * as fs from "fs";
|
||||
|
||||
export type ProxyFetchOptions = Pick<
|
||||
FetchOptions,
|
||||
"proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
|
||||
>;
|
||||
|
||||
export async function getProxyFetchOptions(params: {
|
||||
npmWorkspaceRootDirPath: string;
|
||||
}): Promise<ProxyFetchOptions> {
|
||||
const { npmWorkspaceRootDirPath } = params;
|
||||
export function getProxyFetchOptions(params: {
|
||||
npmConfigGetCwd: string;
|
||||
}): ProxyFetchOptions {
|
||||
const { npmConfigGetCwd } = params;
|
||||
|
||||
const cfg = await getNmpConfig({ npmWorkspaceRootDirPath });
|
||||
const cfg = (() => {
|
||||
const output = child_process
|
||||
.execSync("npm config get", {
|
||||
cwd: npmConfigGetCwd
|
||||
})
|
||||
.toString("utf8");
|
||||
|
||||
return output
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2) as [string, string])
|
||||
.reduce(
|
||||
(
|
||||
cfg: Record<string, string | string[]>,
|
||||
[key, value]: [string, string]
|
||||
) =>
|
||||
key in cfg
|
||||
? { ...cfg, [key]: [...ensureArray(cfg[key]), value] }
|
||||
: { ...cfg, [key]: value },
|
||||
{}
|
||||
);
|
||||
})();
|
||||
|
||||
const proxy = ensureSingleOrNone(cfg["https-proxy"] ?? cfg["proxy"]);
|
||||
const noProxy = cfg["noproxy"] ?? cfg["no-proxy"];
|
||||
@ -71,17 +50,16 @@ export async function getProxyFetchOptions(params: {
|
||||
|
||||
if (typeof cafile !== "undefined" && cafile !== "null") {
|
||||
ca.push(
|
||||
...(await (async () => {
|
||||
function chunks<T>(arr: T[], size: number = 2) {
|
||||
return arr
|
||||
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
|
||||
.filter(Boolean) as T[][];
|
||||
}
|
||||
|
||||
const cafileContent = await readFile(cafile, "utf-8");
|
||||
...(() => {
|
||||
const cafileContent = fs.readFileSync(cafile).toString("utf8");
|
||||
|
||||
const newLinePlaceholder = "NEW_LINE_PLACEHOLDER_xIsPsK23svt";
|
||||
|
||||
const chunks = <T>(arr: T[], size: number = 2) =>
|
||||
arr
|
||||
.map((_, i) => i % size == 0 && arr.slice(i, i + size))
|
||||
.filter(Boolean) as T[][];
|
||||
|
||||
return chunks(cafileContent.split(/(-----END CERTIFICATE-----)/), 2).map(
|
||||
ca =>
|
||||
ca
|
||||
@ -90,7 +68,7 @@ export async function getProxyFetchOptions(params: {
|
||||
.replace(new RegExp(`^${newLinePlaceholder}`), "")
|
||||
.replace(new RegExp(newLinePlaceholder, "g"), "\\n")
|
||||
);
|
||||
})())
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
@ -102,3 +80,17 @@ export async function getProxyFetchOptions(params: {
|
||||
ca: ca.length === 0 ? undefined : ca
|
||||
};
|
||||
}
|
||||
|
||||
function ensureArray<T>(arg0: T | T[]) {
|
||||
return Array.isArray(arg0) ? arg0 : typeof arg0 === "undefined" ? [] : [arg0];
|
||||
}
|
||||
|
||||
function ensureSingleOrNone<T>(arg0: T | T[]) {
|
||||
if (!Array.isArray(arg0)) return arg0;
|
||||
if (arg0.length === 0) return undefined;
|
||||
if (arg0.length === 1) return arg0[0];
|
||||
throw new Error(
|
||||
"Illegal configuration, expected a single value but found multiple: " +
|
||||
arg0.map(String).join(", ")
|
||||
);
|
||||
}
|
@ -1,73 +0,0 @@
|
||||
import * as child_process from "child_process";
|
||||
import { join as pathJoin, resolve as pathResolve, sep as pathSep } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import * as fs from "fs";
|
||||
|
||||
export function getNpmWorkspaceRootDirPath(params: {
|
||||
projectDirPath: string;
|
||||
dependencyExpected: string;
|
||||
}) {
|
||||
const { projectDirPath, dependencyExpected } = params;
|
||||
|
||||
const npmWorkspaceRootDirPath = (function callee(depth: number): string {
|
||||
const cwd = pathResolve(
|
||||
pathJoin(...[projectDirPath, ...Array(depth).fill("..")])
|
||||
);
|
||||
|
||||
assert(cwd !== pathSep, "NPM workspace not found");
|
||||
|
||||
try {
|
||||
child_process.execSync("npm config get", {
|
||||
cwd,
|
||||
stdio: "ignore"
|
||||
});
|
||||
} catch (error) {
|
||||
if (String(error).includes("ENOWORKSPACES")) {
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const packageJsonFilePath = pathJoin(cwd, "package.json");
|
||||
|
||||
if (!fs.existsSync(packageJsonFilePath)) {
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
assert(fs.existsSync(packageJsonFilePath));
|
||||
|
||||
const parsedPackageJson = JSON.parse(
|
||||
fs.readFileSync(packageJsonFilePath).toString("utf8")
|
||||
);
|
||||
|
||||
let isExpectedDependencyFound = false;
|
||||
|
||||
for (const dependenciesOrDevDependencies of [
|
||||
"dependencies",
|
||||
"devDependencies"
|
||||
] as const) {
|
||||
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
|
||||
|
||||
if (dependencies === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(dependencies instanceof Object);
|
||||
|
||||
if (dependencies[dependencyExpected] === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
isExpectedDependencyFound = true;
|
||||
}
|
||||
|
||||
if (!isExpectedDependencyFound && parsedPackageJson.name !== dependencyExpected) {
|
||||
return callee(depth + 1);
|
||||
}
|
||||
|
||||
return cwd;
|
||||
})(0);
|
||||
|
||||
return { npmWorkspaceRootDirPath };
|
||||
}
|
@ -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>
|
||||
))}
|
||||
|
@ -3,8 +3,7 @@ import { assert } from "tsafe/assert";
|
||||
import messages_fallbackLanguage from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import type { KcContext } from "../KcContext";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
import { fallbackLanguageTag } from "keycloakify/bin/shared/constants";
|
||||
|
||||
export type KcContextLike = {
|
||||
locale?: {
|
||||
@ -260,8 +259,16 @@ function createI18nTranslationFunctionsFactory<MessageKey extends string, ExtraM
|
||||
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderAsHtml: boolean }): JSX.Element | string {
|
||||
const { key, args, doRenderAsHtml } = props;
|
||||
|
||||
if (realmMessageBundleUserProfile !== undefined && key in realmMessageBundleUserProfile) {
|
||||
const resolvedMessage = realmMessageBundleUserProfile[key];
|
||||
user_profile: {
|
||||
if (realmMessageBundleUserProfile === undefined) {
|
||||
break user_profile;
|
||||
}
|
||||
|
||||
const resolvedMessage = realmMessageBundleUserProfile[key] ?? realmMessageBundleUserProfile["${" + key + "}"];
|
||||
|
||||
if (resolvedMessage === undefined) {
|
||||
break user_profile;
|
||||
}
|
||||
|
||||
return doRenderAsHtml ? (
|
||||
<span
|
||||
|
@ -2,4 +2,3 @@ import type { GenericI18n, MessageKey, KcContextLike } from "./i18n";
|
||||
export type { MessageKey, KcContextLike };
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
export { createUseI18n } from "./useI18n";
|
||||
export { fallbackLanguageTag } from "./i18n";
|
||||
|
@ -7,12 +7,12 @@ import {
|
||||
} from "./i18n";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||
export function createUseI18n<ExtraMessageKey extends string = never>(messageBundle: {
|
||||
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||
}) {
|
||||
type I18n = GenericI18n<MessageKey | ExtraMessageKey>;
|
||||
|
||||
const { getI18n } = createGetI18n(extraMessages);
|
||||
const { getI18n } = createGetI18n(messageBundle);
|
||||
|
||||
function useI18n(params: { kcContext: KcContextLike }): { i18n: I18n } {
|
||||
const { kcContext } = params;
|
||||
|
@ -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>
|
||||
|
||||
|
@ -24,6 +24,7 @@ export default function Register(props: RegisterProps) {
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
|
||||
const [areTermsAccepted, setAreTermsAccepted] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -43,7 +44,15 @@ export default function Register(props: RegisterProps) {
|
||||
onIsFormSubmittableValueChange={setIsFormSubmittable}
|
||||
doMakeUserConfirmPassword={doMakeUserConfirmPassword}
|
||||
/>
|
||||
{termsAcceptanceRequired && <TermsAcceptance i18n={i18n} kcClsx={kcClsx} messagesPerField={messagesPerField} />}
|
||||
{termsAcceptanceRequired && (
|
||||
<TermsAcceptance
|
||||
i18n={i18n}
|
||||
kcClsx={kcClsx}
|
||||
messagesPerField={messagesPerField}
|
||||
areTermsAccepted={areTermsAccepted}
|
||||
onAreTermsAcceptedValueChange={setAreTermsAccepted}
|
||||
/>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={kcClsx("kcInputWrapperClass")}>
|
||||
@ -61,7 +70,7 @@ export default function Register(props: RegisterProps) {
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||
<input
|
||||
disabled={!isFormSubmittable}
|
||||
disabled={!isFormSubmittable || (termsAcceptanceRequired && !areTermsAccepted)}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
@ -73,8 +82,14 @@ export default function Register(props: RegisterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function TermsAcceptance(props: { i18n: I18n; kcClsx: KcClsx; messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get"> }) {
|
||||
const { i18n, kcClsx, messagesPerField } = props;
|
||||
function TermsAcceptance(props: {
|
||||
i18n: I18n;
|
||||
kcClsx: KcClsx;
|
||||
messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get">;
|
||||
areTermsAccepted: boolean;
|
||||
onAreTermsAcceptedValueChange: (areTermsAccepted: boolean) => void;
|
||||
}) {
|
||||
const { i18n, kcClsx, messagesPerField, areTermsAccepted, onAreTermsAcceptedValueChange } = props;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
@ -93,6 +108,8 @@ function TermsAcceptance(props: { i18n: I18n; kcClsx: KcClsx; messagesPerField:
|
||||
id="termsAccepted"
|
||||
name="termsAccepted"
|
||||
className={kcClsx("kcCheckboxInputClass")}
|
||||
checked={areTermsAccepted}
|
||||
onChange={e => onAreTermsAcceptedValueChange(e.target.checked)}
|
||||
aria-invalid={messagesPerField.existsError("termsAccepted")}
|
||||
/>
|
||||
<label htmlFor="termsAccepted" className={kcClsx("kcLabelClass")}>
|
||||
@ -101,9 +118,14 @@ function TermsAcceptance(props: { i18n: I18n; kcClsx: KcClsx; messagesPerField:
|
||||
</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"
|
||||
}
|
||||
}}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
import type { Attribute } from "../../../dist/login";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "register.ftl" });
|
||||
|
||||
@ -48,23 +49,84 @@ export const WithEmailAlreadyExists: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
export const WithRestrictedToMITStudents: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
registrationEmailAsUsername: true
|
||||
profile: {
|
||||
attributesByName: {
|
||||
email: {
|
||||
validators: {
|
||||
pattern: {
|
||||
pattern: "^[^@]+@([^.]+\\.)*((mit\\.edu)|(berkeley\\.edu))$",
|
||||
"error-message": "${profile.attributes.email.pattern.error}"
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
inputHelperTextBefore: "${profile.attributes.email.inputHelperTextBefore}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"x-keycloakify": {
|
||||
realmMessageBundleUserProfile: {
|
||||
"${profile.attributes.email.inputHelperTextBefore}": "Please use your MIT or Berkeley email.",
|
||||
"${profile.attributes.email.pattern.error}":
|
||||
"This is not an MIT (<strong>@mit.edu</strong>) nor a Berkeley (<strong>@berkeley.edu</strong>) email."
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithoutPassword: Story = {
|
||||
export const WithFavoritePet: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
passwordRequired: false
|
||||
profile: {
|
||||
attributesByName: {
|
||||
favoritePet: {
|
||||
name: "favorite-pet",
|
||||
displayName: "${profile.attributes.favoritePet}",
|
||||
validators: {
|
||||
options: {
|
||||
options: ["cat", "dog", "fish"]
|
||||
}
|
||||
},
|
||||
annotations: {
|
||||
inputOptionLabelsI18nPrefix: "profile.attributes.favoritePet.options"
|
||||
},
|
||||
required: false,
|
||||
readOnly: false
|
||||
} satisfies Attribute
|
||||
}
|
||||
},
|
||||
"x-keycloakify": {
|
||||
realmMessageBundleUserProfile: {
|
||||
"${profile.attributes.favoritePet}": "Favorite Pet",
|
||||
"${profile.attributes.favoritePet.options.cat}": "Fluffy Cat",
|
||||
"${profile.attributes.favoritePet.options.dog}": "Loyal Dog",
|
||||
"${profile.attributes.favoritePet.options.fish}": "Peaceful Fish"
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithEmailAsUsername: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
realm: {
|
||||
registrationEmailAsUsername: true
|
||||
},
|
||||
profile: {
|
||||
attributesByName: {
|
||||
username: undefined
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
@ -97,31 +159,6 @@ export const WithRecaptchaFrench: Story = {
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPresets: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
profile: {
|
||||
attributesByName: {
|
||||
firstName: {
|
||||
value: "Max"
|
||||
},
|
||||
lastName: {
|
||||
value: "Mustermann"
|
||||
},
|
||||
email: {
|
||||
value: "max.mustermann@gmail.com"
|
||||
},
|
||||
username: {
|
||||
value: "max.mustermann"
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithPasswordMinLength8: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
@ -133,3 +170,16 @@ export const WithPasswordMinLength8: Story = {
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
export const WithTermsAcceptance: Story = {
|
||||
render: () => (
|
||||
<KcPageStory
|
||||
kcContext={{
|
||||
termsAcceptanceRequired: true,
|
||||
"x-keycloakify": {
|
||||
realmMessageBundleTermsText: "<a href='https://example.com/terms'>Service Terms of Use</a>"
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
Reference in New Issue
Block a user