Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
e96ee5ba53 | |||
b421633a8a | |||
e2e0d62560 | |||
c71fb06940 | |||
e2171af99c | |||
8cebf049d4 | |||
ef139ed1cc | |||
d717de006a | |||
a44f091878 | |||
1b37ba5339 | |||
bbaa90e997 | |||
86e6c4a419 | |||
4159883791 | |||
d8b00da3a1 | |||
a24945bc1b | |||
158759493f | |||
36e32d6ddc | |||
84908e2ec0 | |||
a2dc51d811 | |||
fb3b0e2c29 | |||
1a3e4c68bb | |||
11b2342da0 | |||
80d4a808d3 | |||
da4146eb59 | |||
a0be35db8b |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@ -45,11 +45,9 @@ jobs:
|
|||||||
- uses: bahmutov/npm-install@v1
|
- uses: bahmutov/npm-install@v1
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
||||||
run: |
|
run: |
|
||||||
yarn build
|
|
||||||
yarn test
|
yarn test
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
||||||
run: |
|
run: |
|
||||||
npm run build
|
|
||||||
npm test
|
npm test
|
||||||
check_if_version_upgraded:
|
check_if_version_upgraded:
|
||||||
name: Check if version upgrade
|
name: Check if version upgrade
|
||||||
|
@ -28,6 +28,13 @@
|
|||||||
-
|
-
|
||||||
<a href="https://docs.keycloakify.dev">Documentation</a>
|
<a href="https://docs.keycloakify.dev">Documentation</a>
|
||||||
</p>
|
</p>
|
||||||
|
<p align="center"> ---- Project starter / Demo setup ---- </p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
|
||||||
|
-
|
||||||
|
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
|
||||||
|
</p>
|
||||||
|
<p align="center"> ---- </p>
|
||||||
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
13
package.json
13
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "6.0.2",
|
"version": "6.3.4",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -13,7 +13,8 @@
|
|||||||
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
|
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
||||||
"test": "yarn build:test && node dist_test/test/bin && node dist_test/test/lib",
|
"pretest": "yarn build:test",
|
||||||
|
"test": "node dist_test/test/bin && node dist_test/test/lib",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js",
|
"generate-messages": "node dist/bin/generate-i18n-messages.js",
|
||||||
"link_in_test_app": "node dist/bin/link_in_test_app.js",
|
"link_in_test_app": "node dist/bin/link_in_test_app.js",
|
||||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||||
@ -60,6 +61,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emotion/react": "^11.4.1",
|
"@emotion/react": "^11.4.1",
|
||||||
"@types/memoizee": "^0.4.7",
|
"@types/memoizee": "^0.4.7",
|
||||||
|
"@types/minimist": "^1.2.2",
|
||||||
"@types/node": "^17.0.25",
|
"@types/node": "^17.0.25",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
@ -75,15 +77,16 @@
|
|||||||
"@octokit/rest": "^18.12.0",
|
"@octokit/rest": "^18.12.0",
|
||||||
"cheerio": "^1.0.0-rc.5",
|
"cheerio": "^1.0.0-rc.5",
|
||||||
"cli-select": "^1.1.2",
|
"cli-select": "^1.1.2",
|
||||||
"evt": "^2.4.0",
|
"evt": "^2.4.1",
|
||||||
"memoizee": "^0.4.15",
|
"memoizee": "^0.4.15",
|
||||||
"minimal-polyfills": "^2.2.2",
|
"minimal-polyfills": "^2.2.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"powerhooks": "^0.20.15",
|
"powerhooks": "^0.20.16",
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"scripting-tools": "^0.19.13",
|
||||||
"tsafe": "^1.0.1",
|
"tsafe": "^1.0.1",
|
||||||
"tss-react": "^4.1.1",
|
"tss-react": "^4.1.3",
|
||||||
"zod": "^3.17.10"
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,15 @@ import { join as pathJoin, basename as pathBasename } from "path";
|
|||||||
import { transformCodebase } from "./tools/transformCodebase";
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||||
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
|
logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
|
||||||
|
|
||||||
process.exit(-1);
|
process.exit(-1);
|
||||||
}
|
}
|
||||||
@ -21,7 +25,8 @@ if (require.main === module) {
|
|||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
downloadBuiltinKeycloakTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": builtinKeycloakThemeTmpDirPath
|
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
transformCodebase({
|
transformCodebase({
|
||||||
@ -29,7 +34,7 @@ if (require.main === module) {
|
|||||||
"destDirPath": keycloakThemeEmailDirPath
|
"destDirPath": keycloakThemeEmailDirPath
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
|
logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
|
||||||
|
|
||||||
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||||
})();
|
})();
|
||||||
|
@ -4,31 +4,37 @@ import { keycloakThemeBuildingDirPath } from "./keycloakify";
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
|
||||||
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string }) {
|
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||||
const { keycloakVersion, destDirPath } = params;
|
const { keycloakVersion, destDirPath, isSilent } = params;
|
||||||
|
|
||||||
for (const ext of ["", "-community"]) {
|
for (const ext of ["", "-community"]) {
|
||||||
downloadAndUnzip({
|
downloadAndUnzip({
|
||||||
"destDirPath": destDirPath,
|
"destDirPath": destDirPath,
|
||||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
||||||
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache")
|
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
const { keycloakVersion } = await promptKeycloakVersion();
|
const { keycloakVersion } = await promptKeycloakVersion();
|
||||||
|
|
||||||
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
|
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
|
||||||
|
|
||||||
console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
downloadBuiltinKeycloakTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
destDirPath
|
destDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import { crawl } from "./tools/crawl";
|
|||||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
import { getProjectRoot } from "./tools/getProjectRoot";
|
||||||
import { rm_rf, rm_r } from "./tools/rm";
|
import { rm_rf, rm_r } from "./tools/rm";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
|
||||||
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
// update the version array for generating for newer version.
|
// update the version array for generating for newer version.
|
||||||
@ -12,8 +14,11 @@ import { rm_rf, rm_r } from "./tools/rm";
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const propertiesParser = require("properties-parser");
|
const propertiesParser = require("properties-parser");
|
||||||
|
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
||||||
console.log({ keycloakVersion });
|
logger.log(JSON.stringify({ keycloakVersion }));
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||||
|
|
||||||
@ -21,7 +26,8 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
|||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
downloadBuiltinKeycloakTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": tmpDirPath
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
type Dictionary = { [idiomId: string]: string };
|
||||||
@ -75,7 +81,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(`${filePath} wrote`);
|
logger.log(`${filePath} wrote`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,7 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
|
|||||||
|
|
||||||
export namespace BuildOptions {
|
export namespace BuildOptions {
|
||||||
export type Common = {
|
export type Common = {
|
||||||
|
isSilent: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
themeName: string;
|
themeName: string;
|
||||||
extraPages?: string[];
|
extraPages?: string[];
|
||||||
@ -71,8 +72,9 @@ export function readBuildOptions(params: {
|
|||||||
packageJson: string;
|
packageJson: string;
|
||||||
CNAME: string | undefined;
|
CNAME: string | undefined;
|
||||||
isExternalAssetsCliParamProvided: boolean;
|
isExternalAssetsCliParamProvided: boolean;
|
||||||
|
isSilent: boolean;
|
||||||
}): BuildOptions {
|
}): BuildOptions {
|
||||||
const { packageJson, CNAME, isExternalAssetsCliParamProvided } = params;
|
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
|
||||||
|
|
||||||
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
||||||
|
|
||||||
@ -130,7 +132,8 @@ export function readBuildOptions(params: {
|
|||||||
})(),
|
})(),
|
||||||
"version": version,
|
"version": version,
|
||||||
extraPages,
|
extraPages,
|
||||||
extraThemeProperties
|
extraThemeProperties,
|
||||||
|
isSilent
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
@ -27,7 +27,9 @@ export const pageIds = [
|
|||||||
"login-idp-link-email.ftl",
|
"login-idp-link-email.ftl",
|
||||||
"login-page-expired.ftl",
|
"login-page-expired.ftl",
|
||||||
"login-config-totp.ftl",
|
"login-config-totp.ftl",
|
||||||
"logout-confirm.ftl"
|
"logout-confirm.ftl",
|
||||||
|
"update-user-profile.ftl",
|
||||||
|
"idp-review-user-profile.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
@ -11,6 +11,7 @@ import { isInside } from "../tools/isInside";
|
|||||||
import type { BuildOptions } from "./BuildOptions";
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { Reflect } from "tsafe/Reflect";
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import { getLogger } from "../tools/logger";
|
||||||
|
|
||||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ export namespace BuildOptionsLike {
|
|||||||
themeName: string;
|
themeName: string;
|
||||||
extraPages?: string[];
|
extraPages?: string[];
|
||||||
extraThemeProperties?: string[];
|
extraThemeProperties?: string[];
|
||||||
|
isSilent: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Standalone = Common & {
|
export type Standalone = Common & {
|
||||||
@ -60,6 +62,7 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
}): { doBundlesEmailTemplate: boolean } {
|
}): { doBundlesEmailTemplate: boolean } {
|
||||||
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
||||||
|
|
||||||
|
const logger = getLogger({ isSilent: buildOptions.isSilent });
|
||||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
|
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
|
||||||
|
|
||||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||||
@ -117,7 +120,7 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
|
|
||||||
email: {
|
email: {
|
||||||
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||||
console.log(
|
logger.log(
|
||||||
[
|
[
|
||||||
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
|
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
|
||||||
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
|
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
|
||||||
@ -154,7 +157,8 @@ export function generateKeycloakThemeResources(params: {
|
|||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
downloadBuiltinKeycloakTheme({
|
||||||
keycloakVersion,
|
keycloakVersion,
|
||||||
"destDirPath": tmpDirPath
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent: buildOptions.isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
||||||
|
@ -5,6 +5,8 @@ import * as child_process from "child_process";
|
|||||||
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { readBuildOptions } from "./BuildOptions";
|
import { readBuildOptions } from "./BuildOptions";
|
||||||
|
import { getLogger } from "../tools/logger";
|
||||||
|
import { getCliOptions } from "../tools/cliOptions";
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
@ -12,7 +14,9 @@ export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build
|
|||||||
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
console.log("🔏 Building the keycloak theme...⌚");
|
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
logger.log("🔏 Building the keycloak theme...⌚");
|
||||||
|
|
||||||
const buildOptions = readBuildOptions({
|
const buildOptions = readBuildOptions({
|
||||||
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
|
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
|
||||||
@ -25,7 +29,8 @@ export function main() {
|
|||||||
|
|
||||||
return fs.readFileSync(cnameFilePath).toString("utf8");
|
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||||
})(),
|
})(),
|
||||||
"isExternalAssetsCliParamProvided": process.argv[2]?.toLowerCase() === "--external-assets"
|
"isExternalAssetsCliParamProvided": hasExternalAssets,
|
||||||
|
"isSilent": isSilent
|
||||||
});
|
});
|
||||||
|
|
||||||
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
|
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
|
||||||
@ -51,7 +56,7 @@ export function main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//We want, however, to test in a container running the latest Keycloak version
|
//We want, however, to test in a container running the latest Keycloak version
|
||||||
const containerKeycloakVersion = "18.0.2";
|
const containerKeycloakVersion = "19.0.1";
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer({
|
generateStartKeycloakTestingContainer({
|
||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
@ -59,7 +64,7 @@ export function main() {
|
|||||||
buildOptions
|
buildOptions
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
logger.log(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||||
|
15
src/bin/tools/cliOptions.ts
Normal file
15
src/bin/tools/cliOptions.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import parseArgv from "minimist";
|
||||||
|
|
||||||
|
export type CliOptions = {
|
||||||
|
isSilent: boolean;
|
||||||
|
hasExternalAssets: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliOptions = (processArgv: string[]): CliOptions => {
|
||||||
|
const argv = parseArgv(processArgv);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||||
|
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||||
|
};
|
||||||
|
};
|
@ -6,7 +6,13 @@ import { rm, rm_rf } from "./rm";
|
|||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
|
||||||
/** assert url ends with .zip */
|
/** assert url ends with .zip */
|
||||||
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string; cacheDirPath: string }) {
|
export function downloadAndUnzip(params: {
|
||||||
|
isSilent: boolean;
|
||||||
|
url: string;
|
||||||
|
destDirPath: string;
|
||||||
|
pathOfDirToExtractInArchive?: string;
|
||||||
|
cacheDirPath: string;
|
||||||
|
}) {
|
||||||
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
|
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
|
||||||
|
|
||||||
const extractDirPath = pathJoin(
|
const extractDirPath = pathJoin(
|
||||||
@ -54,7 +60,7 @@ export function downloadAndUnzip(params: { url: string; destDirPath: string; pat
|
|||||||
|
|
||||||
const zipFileBasename = pathBasename(url);
|
const zipFileBasename = pathBasename(url);
|
||||||
|
|
||||||
execSync(`curl -L ${url} -o ${zipFileBasename}`, { "cwd": extractDirPath });
|
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
|
||||||
|
|
||||||
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
|
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
|
||||||
"cwd": extractDirPath
|
"cwd": extractDirPath
|
||||||
|
27
src/bin/tools/logger.ts
Normal file
27
src/bin/tools/logger.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
type LoggerOpts = {
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Logger = {
|
||||||
|
log: (message: string, opts?: LoggerOpts) => void;
|
||||||
|
warn: (message: string) => void;
|
||||||
|
error: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
|
||||||
|
return {
|
||||||
|
log: (message, { force } = {}) => {
|
||||||
|
if (isSilent && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message);
|
||||||
|
},
|
||||||
|
warn: message => {
|
||||||
|
console.warn(message);
|
||||||
|
},
|
||||||
|
error: message => {
|
||||||
|
console.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
46
src/lib/components/IdpReviewUserProfile.tsx
Normal file
46
src/lib/components/IdpReviewUserProfile.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useState, memo } from "react";
|
||||||
|
import Template from "./Template";
|
||||||
|
import type { KcProps } from "./KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
|
import { useCssAndCx } from "../tools/useCssAndCx";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
|
||||||
|
const IdpReviewUserProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.IdpReviewUserProfile; i18n: I18n } & KcProps) => {
|
||||||
|
const { cx } = useCssAndCx();
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url } = kcContext;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
|
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-idp-review-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||||
|
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
|
<input
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IdpReviewUserProfile;
|
@ -20,6 +20,8 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
|
|||||||
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
|
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
|
||||||
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
|
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
|
||||||
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
|
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
|
||||||
|
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
|
||||||
|
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
|
||||||
|
|
||||||
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
|
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
|
||||||
const i18n = (function useClosure() {
|
const i18n = (function useClosure() {
|
||||||
@ -74,6 +76,10 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
|
|||||||
return <LoginConfigTotp {...{ kcContext, ...props }} />;
|
return <LoginConfigTotp {...{ kcContext, ...props }} />;
|
||||||
case "logout-confirm.ftl":
|
case "logout-confirm.ftl":
|
||||||
return <LogoutConfirm {...{ kcContext, ...props }} />;
|
return <LogoutConfirm {...{ kcContext, ...props }} />;
|
||||||
|
case "update-user-profile.ftl":
|
||||||
|
return <UpdateUserProfile {...{ kcContext, ...props }} />;
|
||||||
|
case "idp-review-user-profile.ftl":
|
||||||
|
return <IdpReviewUserProfile {...{ kcContext, ...props }} />;
|
||||||
}
|
}
|
||||||
})()}
|
})()}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
|
import React, { useMemo, memo, useState } from "react";
|
||||||
import Template from "./Template";
|
import Template from "./Template";
|
||||||
import type { KcProps } from "./KcProps";
|
import type { KcProps } from "./KcProps";
|
||||||
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
import { useCssAndCx } from "../tools/useCssAndCx";
|
import { useCssAndCx } from "../tools/useCssAndCx";
|
||||||
import type { ReactComponent } from "../tools/ReactComponent";
|
|
||||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
|
||||||
import { useFormValidationSlice } from "../useFormValidationSlice";
|
|
||||||
import type { I18n } from "../i18n";
|
import type { I18n } from "../i18n";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
|
||||||
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
|
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
|
||||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||||
@ -66,155 +64,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
|
|
||||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
|
||||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
|
|
||||||
const { cx, css } = useCssAndCx();
|
|
||||||
|
|
||||||
const { advancedMsg } = i18n;
|
|
||||||
|
|
||||||
const {
|
|
||||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
|
||||||
formValidationReducer,
|
|
||||||
attributesWithPassword
|
|
||||||
} = useFormValidationSlice({
|
|
||||||
kcContext,
|
|
||||||
i18n
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
|
||||||
}, [isFormSubmittable]);
|
|
||||||
|
|
||||||
const onChangeFactory = useCallbackFactory(
|
|
||||||
(
|
|
||||||
[name]: [string],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
target: { value }
|
|
||||||
}
|
|
||||||
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
|
|
||||||
) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "update value",
|
|
||||||
name,
|
|
||||||
"newValue": value
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
|
||||||
formValidationReducer({
|
|
||||||
"action": "focus lost",
|
|
||||||
name
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
let currentGroup = "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{attributesWithPassword.map((attribute, i) => {
|
|
||||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
|
||||||
|
|
||||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
|
||||||
|
|
||||||
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment key={i}>
|
|
||||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
|
||||||
<div className={formGroupClassName}>
|
|
||||||
<div className={cx(props.kcContentWrapperClass)}>
|
|
||||||
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
|
||||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{groupDisplayDescription !== "" && (
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
|
||||||
{advancedMsg(groupDisplayDescription)}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={formGroupClassName}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
|
||||||
{advancedMsg(attribute.displayName ?? "")}
|
|
||||||
</label>
|
|
||||||
{attribute.required && <>*</>}
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
{(() => {
|
|
||||||
const { options } = attribute.validators;
|
|
||||||
|
|
||||||
if (options !== undefined) {
|
|
||||||
return (
|
|
||||||
<select
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
onChange={onChangeFactory(attribute.name)}
|
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
|
||||||
value={value}
|
|
||||||
>
|
|
||||||
{options.options.map(option => (
|
|
||||||
<option key={option} value={option}>
|
|
||||||
{option}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type={(() => {
|
|
||||||
switch (attribute.name) {
|
|
||||||
case "password-confirm":
|
|
||||||
case "password":
|
|
||||||
return "password";
|
|
||||||
default:
|
|
||||||
return "text";
|
|
||||||
}
|
|
||||||
})()}
|
|
||||||
id={attribute.name}
|
|
||||||
name={attribute.name}
|
|
||||||
value={value}
|
|
||||||
onChange={onChangeFactory(attribute.name)}
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
aria-invalid={displayableErrors.length !== 0}
|
|
||||||
disabled={attribute.readOnly}
|
|
||||||
autoComplete={attribute.autocomplete}
|
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
{displayableErrors.length !== 0 && (
|
|
||||||
<span
|
|
||||||
id={`input-error-${attribute.name}`}
|
|
||||||
className={cx(
|
|
||||||
props.kcInputErrorMessageClass,
|
|
||||||
css({
|
|
||||||
"position": displayableErrors.length === 1 ? "absolute" : undefined,
|
|
||||||
"& > span": { "display": "block" }
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
aria-live="polite"
|
|
||||||
>
|
|
||||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default RegisterUserProfile;
|
export default RegisterUserProfile;
|
||||||
|
@ -11,6 +11,7 @@ import type { I18n } from "../i18n";
|
|||||||
import memoize from "memoizee";
|
import memoize from "memoizee";
|
||||||
import { useConst } from "powerhooks/useConst";
|
import { useConst } from "powerhooks/useConst";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||||
|
import { Markdown } from "../tools/Markdown";
|
||||||
|
|
||||||
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
|
|||||||
headerNode={msg("termsTitle")}
|
headerNode={msg("termsTitle")}
|
||||||
formNode={
|
formNode={
|
||||||
<>
|
<>
|
||||||
<div id="kc-terms-text">{evtTermMarkdown.state}</div>
|
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||||
<form className="form-actions" action={url.loginAction} method="POST">
|
<form className="form-actions" action={url.loginAction} method="POST">
|
||||||
<input
|
<input
|
||||||
className={cx(
|
className={cx(
|
||||||
|
71
src/lib/components/UpdateUserProfile.tsx
Normal file
71
src/lib/components/UpdateUserProfile.tsx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import React, { useState, memo } from "react";
|
||||||
|
import Template from "./Template";
|
||||||
|
import type { KcProps } from "./KcProps";
|
||||||
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
|
import { useCssAndCx } from "../tools/useCssAndCx";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||||
|
|
||||||
|
const UpdateUserProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n } & KcProps) => {
|
||||||
|
const { cx } = useCssAndCx();
|
||||||
|
|
||||||
|
const { msg, msgStr } = i18n;
|
||||||
|
|
||||||
|
const { url, isAppInitiatedAction } = kcContext;
|
||||||
|
|
||||||
|
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
{...{ kcContext, i18n, ...props }}
|
||||||
|
doFetchDefaultThemeResources={true}
|
||||||
|
headerNode={msg("loginProfileTitle")}
|
||||||
|
formNode={
|
||||||
|
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||||
|
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
|
||||||
|
|
||||||
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
|
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||||
|
<div className={cx(props.kcFormOptionsWrapperClass)}></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||||
|
{isAppInitiatedAction ? (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
value={msgStr("doSubmit")}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||||
|
type="submit"
|
||||||
|
name="cancel-aia"
|
||||||
|
value="true"
|
||||||
|
formNoValidate
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
className={cx(
|
||||||
|
props.kcButtonClass,
|
||||||
|
props.kcButtonPrimaryClass,
|
||||||
|
props.kcButtonBlockClass,
|
||||||
|
props.kcButtonLargeClass
|
||||||
|
)}
|
||||||
|
type="submit"
|
||||||
|
defaultValue={msgStr("doSubmit")}
|
||||||
|
disabled={!isFomSubmittable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default UpdateUserProfile;
|
170
src/lib/components/shared/UserProfileCommons.tsx
Normal file
170
src/lib/components/shared/UserProfileCommons.tsx
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import React, { memo, useEffect, Fragment } from "react";
|
||||||
|
import type { KcProps } from "../KcProps";
|
||||||
|
import type { Attribute } from "../../getKcContext/KcContextBase";
|
||||||
|
import { useCssAndCx } from "../../tools/useCssAndCx";
|
||||||
|
import type { ReactComponent } from "../../tools/ReactComponent";
|
||||||
|
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||||
|
import { useFormValidationSlice } from "../../useFormValidationSlice";
|
||||||
|
import type { I18n } from "../../i18n";
|
||||||
|
import type { Param0 } from "tsafe/Param0";
|
||||||
|
|
||||||
|
export type UserProfileFormFieldsProps = {
|
||||||
|
kcContext: Param0<typeof useFormValidationSlice>["kcContext"];
|
||||||
|
i18n: I18n;
|
||||||
|
} & KcProps &
|
||||||
|
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
||||||
|
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UserProfileFormFields = memo(
|
||||||
|
({ kcContext, onIsFormSubmittableValueChange, i18n, BeforeField, AfterField, ...props }: UserProfileFormFieldsProps) => {
|
||||||
|
const { cx, css } = useCssAndCx();
|
||||||
|
|
||||||
|
const { advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const {
|
||||||
|
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||||
|
formValidationReducer,
|
||||||
|
attributesWithPassword
|
||||||
|
} = useFormValidationSlice({
|
||||||
|
kcContext,
|
||||||
|
i18n
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||||
|
}, [isFormSubmittable]);
|
||||||
|
|
||||||
|
const onChangeFactory = useCallbackFactory(
|
||||||
|
(
|
||||||
|
[name]: [string],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
target: { value }
|
||||||
|
}
|
||||||
|
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
|
||||||
|
) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "update value",
|
||||||
|
name,
|
||||||
|
"newValue": value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
||||||
|
formValidationReducer({
|
||||||
|
"action": "focus lost",
|
||||||
|
name
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
let currentGroup = "";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{attributesWithPassword.map((attribute, i) => {
|
||||||
|
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||||
|
|
||||||
|
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||||
|
|
||||||
|
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment key={i}>
|
||||||
|
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={cx(props.kcContentWrapperClass)}>
|
||||||
|
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
||||||
|
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{groupDisplayDescription !== "" && (
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
||||||
|
{advancedMsg(groupDisplayDescription)}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{BeforeField && <BeforeField attribute={attribute} />}
|
||||||
|
|
||||||
|
<div className={formGroupClassName}>
|
||||||
|
<div className={cx(props.kcLabelWrapperClass)}>
|
||||||
|
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
||||||
|
{advancedMsg(attribute.displayName ?? "")}
|
||||||
|
</label>
|
||||||
|
{attribute.required && <>*</>}
|
||||||
|
</div>
|
||||||
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
|
{(() => {
|
||||||
|
const { options } = attribute.validators;
|
||||||
|
|
||||||
|
if (options !== undefined) {
|
||||||
|
return (
|
||||||
|
<select
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
onChange={onChangeFactory(attribute.name)}
|
||||||
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{options.options.map(option => (
|
||||||
|
<option key={option} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
case "password":
|
||||||
|
return "password";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChangeFactory(attribute.name)}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
|
disabled={attribute.readOnly}
|
||||||
|
autoComplete={attribute.autocomplete}
|
||||||
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
{displayableErrors.length !== 0 && (
|
||||||
|
<span
|
||||||
|
id={`input-error-${attribute.name}`}
|
||||||
|
className={cx(
|
||||||
|
props.kcInputErrorMessageClass,
|
||||||
|
css({
|
||||||
|
"position": displayableErrors.length === 1 ? "absolute" : undefined,
|
||||||
|
"& > span": { "display": "block" }
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{AfterField && <AfterField attribute={attribute} />}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -25,7 +25,9 @@ export type KcContextBase =
|
|||||||
| KcContextBase.LoginIdpLinkEmail
|
| KcContextBase.LoginIdpLinkEmail
|
||||||
| KcContextBase.LoginPageExpired
|
| KcContextBase.LoginPageExpired
|
||||||
| KcContextBase.LoginConfigTotp
|
| KcContextBase.LoginConfigTotp
|
||||||
| KcContextBase.LogoutConfirm;
|
| KcContextBase.LogoutConfirm
|
||||||
|
| KcContextBase.UpdateUserProfile
|
||||||
|
| KcContextBase.IdpReviewUserProfile;
|
||||||
|
|
||||||
export declare namespace KcContextBase {
|
export declare namespace KcContextBase {
|
||||||
export type Common = {
|
export type Common = {
|
||||||
@ -270,6 +272,23 @@ export declare namespace KcContextBase {
|
|||||||
skipLink?: boolean;
|
skipLink?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UpdateUserProfile = Common & {
|
||||||
|
pageId: "update-user-profile.ftl";
|
||||||
|
profile: {
|
||||||
|
attributes: Attribute[];
|
||||||
|
attributesByName: Record<string, Attribute>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type IdpReviewUserProfile = Common & {
|
||||||
|
pageId: "idp-review-user-profile.ftl";
|
||||||
|
profile: {
|
||||||
|
context: "IDP_REVIEW";
|
||||||
|
attributes: Attribute[];
|
||||||
|
attributesByName: Record<string, Attribute>;
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Attribute = {
|
export type Attribute = {
|
||||||
|
@ -47,8 +47,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
|||||||
"source": partialKcContextCustomMock
|
"source": partialKcContextCustomMock
|
||||||
});
|
});
|
||||||
|
|
||||||
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
|
if (
|
||||||
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
|
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
|
||||||
|
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
|
||||||
|
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
|
||||||
|
) {
|
||||||
|
assert(
|
||||||
|
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
|
||||||
|
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
|
||||||
|
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
|
||||||
|
);
|
||||||
|
|
||||||
const { attributes } = kcContextDefaultMock.profile;
|
const { attributes } = kcContextDefaultMock.profile;
|
||||||
|
|
||||||
@ -60,8 +68,6 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
|||||||
].filter(exclude(undefined));
|
].filter(exclude(undefined));
|
||||||
|
|
||||||
attributes.forEach(attribute => {
|
attributes.forEach(attribute => {
|
||||||
console.log("====>", attribute);
|
|
||||||
|
|
||||||
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
||||||
|
|
||||||
const augmentedAttribute: Attribute = {} as any;
|
const augmentedAttribute: Attribute = {} as any;
|
||||||
|
@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
|
|||||||
|
|
||||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
|
const attributes: Attribute[] = [
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"username-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-username-has-value": {},
|
||||||
|
"length": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"min": "3",
|
||||||
|
"max": "255"
|
||||||
|
},
|
||||||
|
"up-duplicate-username": {},
|
||||||
|
"up-username-mutation": {}
|
||||||
|
},
|
||||||
|
"displayName": "${username}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "username",
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "username",
|
||||||
|
"value": "xxxx"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"up-email-exists-as-username": {},
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-blank-attribute-value": {
|
||||||
|
"error-message": "missingEmailMessage",
|
||||||
|
"fail-on-null": false
|
||||||
|
},
|
||||||
|
"up-duplicate-email": {},
|
||||||
|
"email": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"pattern": {
|
||||||
|
"ignore.empty.value": true,
|
||||||
|
"pattern": "gmail\\.com$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"displayName": "${email}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"autocomplete": "email",
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "email"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"person-name-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-immutable-attribute": {},
|
||||||
|
"up-attribute-required-by-metadata-value": {}
|
||||||
|
},
|
||||||
|
"displayName": "${firstName}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "firstName"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"validators": {
|
||||||
|
"length": {
|
||||||
|
"max": "255",
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"person-name-prohibited-characters": {
|
||||||
|
"ignore.empty.value": true
|
||||||
|
},
|
||||||
|
"up-immutable-attribute": {},
|
||||||
|
"up-attribute-required-by-metadata-value": {}
|
||||||
|
},
|
||||||
|
"displayName": "${lastName}",
|
||||||
|
"annotations": {},
|
||||||
|
"required": true,
|
||||||
|
"groupAnnotations": {},
|
||||||
|
"readOnly": false,
|
||||||
|
"name": "lastName"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
|
||||||
|
|
||||||
export const kcContextCommonMock: KcContextBase.Common = {
|
export const kcContextCommonMock: KcContextBase.Common = {
|
||||||
"url": {
|
"url": {
|
||||||
"loginAction": "#",
|
"loginAction": "#",
|
||||||
@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
...registerCommon,
|
...registerCommon,
|
||||||
"profile": {
|
"profile": {
|
||||||
"context": "REGISTRATION_PROFILE" as const,
|
"context": "REGISTRATION_PROFILE" as const,
|
||||||
...(() => {
|
|
||||||
const attributes: Attribute[] = [
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"username-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-username-has-value": {},
|
|
||||||
"length": {
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"min": "3",
|
|
||||||
"max": "255"
|
|
||||||
},
|
|
||||||
"up-duplicate-username": {},
|
|
||||||
"up-username-mutation": {}
|
|
||||||
},
|
|
||||||
"displayName": "${username}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "username",
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "username",
|
|
||||||
"value": "xxxx"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"up-email-exists-as-username": {},
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-blank-attribute-value": {
|
|
||||||
"error-message": "missingEmailMessage",
|
|
||||||
"fail-on-null": false
|
|
||||||
},
|
|
||||||
"up-duplicate-email": {},
|
|
||||||
"email": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"pattern": {
|
|
||||||
"ignore.empty.value": true,
|
|
||||||
"pattern": "gmail\\.com$"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"displayName": "${email}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"autocomplete": "email",
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "email"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"person-name-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-immutable-attribute": {},
|
|
||||||
"up-attribute-required-by-metadata-value": {}
|
|
||||||
},
|
|
||||||
"displayName": "${firstName}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "firstName"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"validators": {
|
|
||||||
"length": {
|
|
||||||
"max": "255",
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"person-name-prohibited-characters": {
|
|
||||||
"ignore.empty.value": true
|
|
||||||
},
|
|
||||||
"up-immutable-attribute": {},
|
|
||||||
"up-attribute-required-by-metadata-value": {}
|
|
||||||
},
|
|
||||||
"displayName": "${lastName}",
|
|
||||||
"annotations": {},
|
|
||||||
"required": true,
|
|
||||||
"groupAnnotations": {},
|
|
||||||
"readOnly": false,
|
|
||||||
"name": "lastName"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return {
|
|
||||||
attributes,
|
attributes,
|
||||||
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
|
attributesByName
|
||||||
} as any;
|
|
||||||
})()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@ -423,5 +421,22 @@ export const kcContextMocks: KcContextBase[] = [
|
|||||||
"baseUrl": "#"
|
"baseUrl": "#"
|
||||||
},
|
},
|
||||||
"logoutConfirm": { "code": "123", skipLink: false }
|
"logoutConfirm": { "code": "123", skipLink: false }
|
||||||
|
}),
|
||||||
|
id<KcContextBase.UpdateUserProfile>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "update-user-profile.ftl",
|
||||||
|
"profile": {
|
||||||
|
attributes,
|
||||||
|
attributesByName
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
id<KcContextBase.IdpReviewUserProfile>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "idp-review-user-profile.ftl",
|
||||||
|
"profile": {
|
||||||
|
context: "IDP_REVIEW",
|
||||||
|
attributes,
|
||||||
|
attributesByName
|
||||||
|
}
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
|
||||||
import type baseMessages from "./generated_messages/18.0.1/login/en";
|
import type baseMessages from "./generated_messages/18.0.1/login/en";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||||
|
import { Markdown } from "../tools/Markdown";
|
||||||
|
|
||||||
export const fallbackLanguageTag = "en";
|
export const fallbackLanguageTag = "en";
|
||||||
|
|
||||||
@ -83,8 +83,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
refHasStartedFetching.current = true;
|
refHasStartedFetching.current = true;
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
@ -144,10 +142,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
|
|||||||
})()
|
})()
|
||||||
]).then(modules => modules.map(module => module.default));
|
]).then(modules => modules.map(module => module.default));
|
||||||
|
|
||||||
if (!isMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setI18n({
|
setI18n({
|
||||||
...createI18nTranslationFunctions({
|
...createI18nTranslationFunctions({
|
||||||
"fallbackMessages": {
|
"fallbackMessages": {
|
||||||
@ -180,10 +174,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return i18n ?? null;
|
return i18n ?? null;
|
||||||
@ -244,9 +234,9 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
|||||||
})();
|
})();
|
||||||
|
|
||||||
return doRenderMarkdown ? (
|
return doRenderMarkdown ? (
|
||||||
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
|
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||||
{messageWithArgsInjectedIfAny}
|
{messageWithArgsInjectedIfAny}
|
||||||
</ReactMarkdown>
|
</Markdown>
|
||||||
) : (
|
) : (
|
||||||
messageWithArgsInjectedIfAny
|
messageWithArgsInjectedIfAny
|
||||||
);
|
);
|
||||||
|
3
src/lib/tools/Markdown.ts
Normal file
3
src/lib/tools/Markdown.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
|
export { Markdown };
|
@ -304,13 +304,17 @@ export function useGetErrors(params: {
|
|||||||
return { getErrors };
|
return { getErrors };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NOTE: The attributesWithPassword returned is actually augmented with
|
||||||
|
* artificial password related attributes only if kcContext.passwordRequired === true
|
||||||
|
*/
|
||||||
export function useFormValidationSlice(params: {
|
export function useFormValidationSlice(params: {
|
||||||
kcContext: {
|
kcContext: {
|
||||||
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
|
||||||
profile: {
|
profile: {
|
||||||
attributes: Attribute[];
|
attributes: Attribute[];
|
||||||
};
|
};
|
||||||
passwordRequired: boolean;
|
passwordRequired?: boolean;
|
||||||
realm: { registrationEmailAsUsername: boolean };
|
realm: { registrationEmailAsUsername: boolean };
|
||||||
};
|
};
|
||||||
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
/** NOTE: Try to avoid passing a new ref every render for better performances. */
|
||||||
|
@ -14,6 +14,7 @@ generateKeycloakThemeResources({
|
|||||||
"extraPages": ["my-custom-page.ftl"],
|
"extraPages": ["my-custom-page.ftl"],
|
||||||
"extraThemeProperties": ["env=test"],
|
"extraThemeProperties": ["env=test"],
|
||||||
"isStandalone": true,
|
"isStandalone": true,
|
||||||
"urlPathname": "/keycloakify-demo-app/"
|
"urlPathname": "/keycloakify-demo-app/",
|
||||||
|
"isSilent": false
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -8,6 +8,7 @@ export function setupSampleReactProject() {
|
|||||||
downloadAndUnzip({
|
downloadAndUnzip({
|
||||||
"url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
"url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
|
||||||
"destDirPath": sampleReactProjectDirPath,
|
"destDirPath": sampleReactProjectDirPath,
|
||||||
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache")
|
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
|
||||||
|
"isSilent": false
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
36
yarn.lock
36
yarn.lock
@ -265,6 +265,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519"
|
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519"
|
||||||
integrity sha512-qDpXKGgwKywnQt/64fH1O0LiPA++QGIYeykEUiZ51HymKVRLnUSGcRuF60IfpPeeXiuRwiR/W4y7S5VzbrgLCA==
|
integrity sha512-qDpXKGgwKywnQt/64fH1O0LiPA++QGIYeykEUiZ51HymKVRLnUSGcRuF60IfpPeeXiuRwiR/W4y7S5VzbrgLCA==
|
||||||
|
|
||||||
|
"@types/minimist@^1.2.2":
|
||||||
|
version "1.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
|
||||||
|
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
|
||||||
|
|
||||||
"@types/node@^17.0.25":
|
"@types/node@^17.0.25":
|
||||||
version "17.0.45"
|
version "17.0.45"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
|
||||||
@ -743,10 +748,10 @@ event-emitter@^0.3.5:
|
|||||||
d "1"
|
d "1"
|
||||||
es5-ext "~0.10.14"
|
es5-ext "~0.10.14"
|
||||||
|
|
||||||
evt@^2.4.0:
|
evt@^2.4.1:
|
||||||
version "2.4.0"
|
version "2.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.0.tgz#5052d3a685bba8f7e7be699b358c1781bd4037bf"
|
resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.1.tgz#68beca2f7bd7eb755fdda5b263a80b934100e046"
|
||||||
integrity sha512-1V8FnExwqzX2fufT793mHfCnQay078kRdAEDHzJz863pgJK9aeDiWXZFs5aKmzJfIgi+svevtApVnleZXz4Jeg==
|
integrity sha512-eo7sZcfDbiVWD5Aw6STkIEMmchYLdeGnJB6tVaM9AXZc7pViin3PmQhb6fgFIFHfl0re9zSEHtSjyu70Y7dRJg==
|
||||||
dependencies:
|
dependencies:
|
||||||
minimal-polyfills "^2.2.2"
|
minimal-polyfills "^2.2.2"
|
||||||
run-exclusive "^2.2.16"
|
run-exclusive "^2.2.16"
|
||||||
@ -1188,6 +1193,11 @@ minimatch@^3.0.3, minimatch@^3.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
brace-expansion "^1.1.7"
|
brace-expansion "^1.1.7"
|
||||||
|
|
||||||
|
minimist@^1.2.6:
|
||||||
|
version "1.2.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||||
|
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||||
|
|
||||||
mkdirp@^1.0.4:
|
mkdirp@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||||
@ -1375,12 +1385,12 @@ please-upgrade-node@^3.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver-compare "^1.0.0"
|
semver-compare "^1.0.0"
|
||||||
|
|
||||||
powerhooks@^0.20.15:
|
powerhooks@^0.20.16:
|
||||||
version "0.20.15"
|
version "0.20.16"
|
||||||
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.15.tgz#e5093a11f3ed1badbc09944acf5a3fb15ac3f1d7"
|
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.16.tgz#03aadcccb4f70ca1c768369c01b84197cfeb97aa"
|
||||||
integrity sha512-28xNjPTtjPPP8vyi9SWXfn0U9hjxm/UzuarD2uofIXa/CPWmkCeTtikyLoR6yuiE4pOfvdDXVfU5im40GuzJaQ==
|
integrity sha512-pCDaiGe5p3NrTvtelDBjrclJtjmLQlf99EjHPpQCeQ1H1ZmwQDxV8DaSbguQq0jc1PGcJ5D6Ts3gcEr/e680fg==
|
||||||
dependencies:
|
dependencies:
|
||||||
evt "^2.4.0"
|
evt "^2.4.1"
|
||||||
memoizee "^0.4.15"
|
memoizee "^0.4.15"
|
||||||
resize-observer-polyfill "^1.5.1"
|
resize-observer-polyfill "^1.5.1"
|
||||||
tsafe "^1.0.1"
|
tsafe "^1.0.1"
|
||||||
@ -1733,10 +1743,10 @@ tslib@^2.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
|
||||||
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
|
||||||
|
|
||||||
tss-react@^4.1.1:
|
tss-react@^4.1.3:
|
||||||
version "4.1.1"
|
version "4.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.1.1.tgz#207220417e4b2f8eb26d8280ab9cdeb385063069"
|
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.1.3.tgz#170a2edd85e32ba3b752416eeb45438c35e90243"
|
||||||
integrity sha512-K1U2s/GGw+XycUjJGztJsLUhwm8KJWz5afL5WZU3SwMeQsA+gbETM6bSxVk2/DXBdw9uYLL9jkSYPAXh0tfYBw==
|
integrity sha512-Ul99aVoVngHY2nwFC87jZlGP+K3LfG9ZRakimOzRwgJz77f/zXWuY2FVzwaSc+OW804twNaxZZRtLlpagex8sg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@emotion/cache" "*"
|
"@emotion/cache" "*"
|
||||||
"@emotion/serialize" "*"
|
"@emotion/serialize" "*"
|
||||||
|
Reference in New Issue
Block a user