Compare commits

...

61 Commits

Author SHA1 Message Date
4d3220820b Update dependency tss-react to ^4.3.3 2022-10-04 03:50:15 +00:00
a4ac9fb0f3 Update garronej_modules_update 2022-10-01 17:47:21 +00:00
1ff79ecf07 Update garronej_modules_update 2022-09-29 08:53:47 +00:00
1166b16420 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-27 21:35:52 +02:00
213224942f Bump version 2022-09-27 21:35:16 +02:00
ff16e66275 #177: Provide a simple way to disable fetching of default resources 2022-09-27 21:30:33 +02:00
3c338e983f Update garronej_modules_update 2022-09-14 16:33:35 +00:00
2c11ba6520 Bump version 2022-09-13 10:50:36 +02:00
9a21656706 Apply #164 on v6 2022-09-13 10:49:20 +02:00
e96ee5ba53 Bump version 2022-09-12 23:49:14 +02:00
b421633a8a Default test container use 19.0.1 2022-09-12 23:48:34 +02:00
e2e0d62560 Cleaner npm scripts 2022-09-11 01:53:56 +02:00
c71fb06940 Update garronej_modules_update 2022-09-10 20:14:00 +00:00
e2171af99c Bump version 2022-09-09 17:24:58 +02:00
8cebf049d4 Render Markdown in Terms 2022-09-09 17:24:43 +02:00
ef139ed1cc Bump version 2022-09-09 14:37:41 +02:00
d717de006a Update kcContext def 2022-09-09 14:37:27 +02:00
a44f091878 Bump version 2022-09-09 12:56:31 +02:00
1b37ba5339 Feat idp-review-user-profile.ftl #164 2022-09-09 12:56:09 +02:00
bbaa90e997 Rename misnamed default exports, removing doInsertPasswordFields 2022-09-09 10:24:50 +02:00
86e6c4a419 Bump version (changelog ignore) 2022-09-09 02:10:18 +02:00
4159883791 Feature update-user-profile.ftl #164 2022-09-09 02:07:49 +02:00
d8b00da3a1 Update tss-react 2022-09-08 15:27:41 +02:00
a24945bc1b Bump version 2022-09-08 15:18:21 +02:00
158759493f Merge pull request #170 from Tasyp/feature/silent
feat: add silent flag
2022-09-08 15:17:45 +02:00
36e32d6ddc Update src/bin/download-builtin-keycloak-theme.ts 2022-09-08 15:13:21 +02:00
84908e2ec0 Update src/bin/generate-i18n-messages.ts 2022-09-08 15:13:15 +02:00
a2dc51d811 Update src/bin/create-keycloak-email-directory.ts 2022-09-08 15:13:09 +02:00
fb3b0e2c29 fix: make sure external assets flag is a boolean 2022-09-08 15:46:27 +03:00
1a3e4c68bb test: update tests to include silent flag 2022-09-08 15:43:03 +03:00
11b2342da0 feat: add silent flag 2022-09-08 13:52:10 +03:00
80d4a808d3 Update readme 2022-09-07 13:45:34 +02:00
da4146eb59 Bump version (changelog ignore) 2022-09-07 13:40:18 +02:00
a0be35db8b Fix compat with StrictMode react 18 2022-09-07 13:39:54 +02:00
14db9cd523 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 12:02:35 +02:00
0c315385dd Update fix tests 2022-09-07 12:00:23 +02:00
c0a0eb02fb Remove Open collectivity 2022-09-07 11:59:42 +02:00
ee407c32ad Create FUNDING.yaml 2022-09-07 11:53:18 +02:00
9262d21829 Bump version 2022-09-07 11:50:41 +02:00
a13f710325 Important fix for --external-assets 2022-09-07 11:50:24 +02:00
eac1a6036f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 11:26:18 +02:00
987f3d7586 Bump version (changelog ignore) 2022-09-07 11:26:10 +02:00
875322669c Rename isAppAndKeycloakServerSharingSameDomain to areAppAndKeycloakServerSharingSameDomain #145 2022-09-07 11:25:46 +02:00
33a264b3d0 Update README.md 2022-09-07 00:32:38 +02:00
c059eff170 Update README 2022-09-06 19:14:39 +02:00
b4a22fc9dd Fix readme 2022-09-06 19:13:46 +02:00
6d1cbdc463 Bump version 2022-09-06 19:12:59 +02:00
2bfbba4daf Upgrade tss-react 2022-09-06 17:43:30 +02:00
21ffe82bde Bump version 2022-09-06 17:40:06 +02:00
8e6f597027 Fix bug with --external-assets 2022-09-06 17:39:47 +02:00
16c5065560 Bump version 2022-09-05 00:09:07 +02:00
c4b985f1a4 Fix replacers 2022-09-05 00:08:50 +02:00
042747c7d2 Bump version 2022-09-04 23:19:53 +02:00
e4a46f31de Make it allright not to provide validators object on mock data 2022-09-04 23:19:33 +02:00
6d9e62d2b4 Remove unessesary log 2022-09-04 21:48:46 +02:00
9caaa507b1 Bump version 2022-09-01 22:35:15 +02:00
5c7d3c5b44 lib target ES2017 instead of ES2020 2022-09-01 22:35:01 +02:00
8bac57d87a Bump version 2022-09-01 21:31:41 +02:00
b8d759cd63 Minor refactor for dryness 2022-09-01 21:31:27 +02:00
da72e3e5ac Bump version (changelog ignore) 2022-09-01 21:16:39 +02:00
2afd36fee0 Avoid fetching locale twitch (react 18 useEffect) 2022-09-01 21:16:25 +02:00
48 changed files with 1924 additions and 1475 deletions

4
.github/FUNDING.yaml vendored Normal file
View File

@ -0,0 +1,4 @@
# These are supported funding model platforms
github: [garronej]
custom: ['https://www.ringerhq.com/experts/garronej']

View File

@ -3,11 +3,9 @@ on:
push:
branches:
- main
- v6
pull_request:
branches:
- main
- v6
jobs:
@ -47,11 +45,9 @@ jobs:
- uses: bahmutov/npm-install@v1
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
run: |
yarn build
yarn test
- if: steps.step1.outputs.npm_or_yarn == 'npm'
run: |
npm run build
npm test
check_if_version_upgraded:
name: Check if version upgrade

View File

@ -28,6 +28,13 @@
-
<a href="https://docs.keycloakify.dev">Documentation</a>
</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>
@ -36,8 +43,18 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
> 🗣 V6 have been released 🎉
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
# Changelog highlights
## 6.4.0
- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default `<KcApp />`
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded.
@ -45,7 +62,7 @@
- Real i18n API.
- Actual documentation for build options.
Checkout the migration guide.
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "6.0.0-beta.8",
"version": "6.4.3",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -13,7 +13,8 @@
"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",
"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",
"link_in_test_app": "node dist/bin/link_in_test_app.js",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
@ -60,6 +61,7 @@
"devDependencies": {
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"copyfiles": "^2.4.1",
@ -75,15 +77,16 @@
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.4.0",
"evt": "^2.4.4",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"powerhooks": "^0.20.15",
"powerhooks": "^0.20.20",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tsafe": "^1.0.1",
"tss-react": "^4.0.0",
"tsafe": "^1.1.1",
"tss-react": "^4.3.3",
"zod": "^3.17.10"
}
}

View File

@ -6,11 +6,15 @@ import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
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);
}
@ -21,7 +25,8 @@ if (require.main === module) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
@ -29,7 +34,7 @@ if (require.main === module) {
"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 });
})();

View File

@ -4,31 +4,37 @@ import { keycloakThemeBuildingDirPath } from "./keycloakify";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string }) {
const { keycloakVersion, destDirPath } = params;
export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
const { keycloakVersion, destDirPath, isSilent } = params;
for (const ext of ["", "-community"]) {
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache")
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
isSilent
});
}
}
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
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({
keycloakVersion,
destDirPath
destDirPath,
isSilent
});
})();
}

View File

@ -5,6 +5,8 @@ import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
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,
// update the version array for generating for newer version.
@ -12,8 +14,11 @@ import { rm_rf, rm_r } from "./tools/rm";
//@ts-ignore
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"]) {
console.log({ keycloakVersion });
logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
@ -21,7 +26,8 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath
"destDirPath": tmpDirPath,
isSilent
});
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`);
});
});
}

View File

@ -11,7 +11,7 @@ type ParsedPackageJson = {
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
isAppAndKeycloakServerSharingSameDomain?: boolean;
areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
@ -23,7 +23,7 @@ const zParsedPackageJson = z.object({
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"isAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
@ -35,6 +35,7 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeName: string;
extraPages?: string[];
@ -56,11 +57,11 @@ export namespace BuildOptions {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -71,8 +72,9 @@ export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
isSilent: boolean;
}): BuildOptions {
const { packageJson, CNAME, isExternalAssetsCliParamProvided } = params;
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
@ -130,7 +132,8 @@ export function readBuildOptions(params: {
})(),
"version": version,
extraPages,
extraThemeProperties
extraThemeProperties,
isSilent
};
})();
@ -140,10 +143,10 @@ export function readBuildOptions(params: {
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.isAppAndKeycloakServerSharingSameDomain) {
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": true
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
@ -155,14 +158,14 @@ export function readBuildOptions(params: {
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "isAppAndKeycloakServerSharingSameDomain": true }',
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": false,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});

View File

@ -272,6 +272,11 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list object as array_item>
<#if !array_item??>
<#local out_seq += ["null,"]>
<#continue>
</#if>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>

View File

@ -27,7 +27,9 @@ export const pageIds = [
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl"
"logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl"
] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -46,11 +48,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -76,7 +78,7 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}

View File

@ -11,6 +11,7 @@ import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -19,6 +20,7 @@ export namespace BuildOptionsLike {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
};
export type Standalone = Common & {
@ -34,11 +36,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true;
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false;
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@ -60,6 +62,7 @@ export function generateKeycloakThemeResources(params: {
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
@ -97,7 +100,7 @@ export function generateKeycloakThemeResources(params: {
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
@ -117,7 +120,7 @@ export function generateKeycloakThemeResources(params: {
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(
logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
@ -154,7 +157,8 @@ export function generateKeycloakThemeResources(params: {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");

View File

@ -5,6 +5,8 @@ import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
const reactProjectDirPath = process.cwd();
@ -12,7 +14,9 @@ export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
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({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
@ -25,7 +29,8 @@ export function main() {
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": process.argv[2]?.toLowerCase() === "--external-assets"
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
@ -51,7 +56,7 @@ export function main() {
});
//We want, however, to test in a container running the latest Keycloak version
const containerKeycloakVersion = "18.0.2";
const containerKeycloakVersion = "19.0.1";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath,
@ -59,7 +64,7 @@ export function main() {
buildOptions
});
console.log(
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,

View File

@ -41,10 +41,10 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
const { jsCode, buildOptions } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
(...[, n, u, e]) => `
${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(n, "p");
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
@ -57,7 +57,7 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + p; },
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
@ -73,13 +73,13 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + ${group} + "static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : "") + ${group2} + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };

View File

@ -81,8 +81,6 @@ const testAppPaths = (() => {
.filter(exclude(undefined));
})();
console.log(testAppPaths);
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);

View 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
};
};

View File

@ -6,7 +6,13 @@ import { rm, rm_rf } from "./rm";
import * as crypto from "crypto";
/** 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 extractDirPath = pathJoin(
@ -54,7 +60,7 @@ export function downloadAndUnzip(params: { url: string; destDirPath: string; pat
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}/**/*"`}`, {
"cwd": extractDirPath

27
src/bin/tools/logger.ts Normal file
View 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);
}
};
};

View File

@ -4,15 +4,20 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Error; i18n: I18n } & KcProps) => {
const Error = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Error; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { message, client } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("errorTitle")}
formNode={
@ -29,6 +34,7 @@ const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Er
}
/>
);
});
}
);
export default Error;

View File

@ -0,0 +1,57 @@
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,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.IdpReviewUserProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
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;

View File

@ -5,7 +5,13 @@ import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Info; i18n: I18n } & KcProps) => {
const Info = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Info; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined);
@ -14,8 +20,7 @@ const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Inf
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
formNode={
@ -46,6 +51,7 @@ const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Inf
}
/>
);
});
}
);
export default Info;

View File

@ -20,8 +20,15 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const KcApp = memo(
({
kcContext,
i18n: userProvidedI18n,
...kcProps
}: { kcContext: KcContextBase; i18n?: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
@ -36,46 +43,53 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext
return null;
}
const props = { i18n, ...kcProps };
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login {...{ kcContext, i18n, ...props }} />;
return <Login {...{ kcContext, ...props }} />;
case "register.ftl":
return <Register {...{ kcContext, i18n, ...props }} />;
return <Register {...{ kcContext, ...props }} />;
case "register-user-profile.ftl":
return <RegisterUserProfile {...{ kcContext, i18n, ...props }} />;
return <RegisterUserProfile {...{ kcContext, ...props }} />;
case "info.ftl":
return <Info {...{ kcContext, i18n, ...props }} />;
return <Info {...{ kcContext, ...props }} />;
case "error.ftl":
return <Error {...{ kcContext, i18n, ...props }} />;
return <Error {...{ kcContext, ...props }} />;
case "login-reset-password.ftl":
return <LoginResetPassword {...{ kcContext, i18n, ...props }} />;
return <LoginResetPassword {...{ kcContext, ...props }} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, i18n, ...props }} />;
return <LoginVerifyEmail {...{ kcContext, ...props }} />;
case "terms.ftl":
return <Terms {...{ kcContext, i18n, ...props }} />;
return <Terms {...{ kcContext, ...props }} />;
case "login-otp.ftl":
return <LoginOtp {...{ kcContext, i18n, ...props }} />;
return <LoginOtp {...{ kcContext, ...props }} />;
case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, i18n, ...props }} />;
return <LoginUpdatePassword {...{ kcContext, ...props }} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, i18n, ...props }} />;
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, i18n, ...props }} />;
return <LoginPageExpired {...{ kcContext, ...props }} />;
case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, i18n, ...props }} />;
return <LoginConfigTotp {...{ kcContext, ...props }} />;
case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, i18n, ...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>
);
});
}
);
export default KcApp;

View File

@ -7,7 +7,13 @@ import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Login; i18n: I18n } & KcProps) => {
const Login = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Login; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
@ -32,8 +38,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")}
@ -41,7 +46,9 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}>
<div
id="kc-form-wrapper"
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}
className={cx(
realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass]
)}
>
{realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
@ -190,6 +197,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
}
/>
);
});
}
);
export default Login;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => {
const LoginConfigTotp = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx();
@ -20,8 +26,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("loginTotpTitle")}
formNode={
<>
@ -181,6 +186,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
}
/>
);
});
}
);
export default LoginConfigTotp;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n } & KcProps) => {
const LoginIdpLinkConfirm = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, idpAlias } = kcContext;
const { msg } = i18n;
@ -14,8 +20,7 @@ const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("confirmLinkIdpTitle")}
formNode={
<form id="kc-register-form" action={url.loginAction} method="post">
@ -43,6 +48,7 @@ const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
}
/>
);
});
}
);
export default LoginIdpLinkConfirm;

View File

@ -4,15 +4,20 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n } & KcProps) => {
const LoginIdpLinkEmail = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={
<>
@ -29,6 +34,7 @@ const LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcCo
}
/>
);
});
}
);
export default LoginIdpLinkEmail;

View File

@ -7,7 +7,13 @@ import { pathJoin } from "../../bin/tools/pathJoin";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => {
const LoginOtp = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginOtp; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { otpLogin, url } = kcContext;
const { cx } = useCssAndCx();
@ -33,8 +39,7 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("doLogIn")}
formNode={
<form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
@ -72,7 +77,12 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
@ -84,7 +94,8 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
}
/>
);
});
}
);
declare const $: any;

View File

@ -4,15 +4,20 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n } & KcProps) => {
const LoginPageExpired = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url } = kcContext;
const { msg } = i18n;
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("pageExpiredTitle")}
formNode={
@ -33,6 +38,7 @@ const LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcCon
}
/>
);
});
}
);
export default LoginPageExpired;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n } & KcProps) => {
const LoginResetPassword = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, realm, auth } = kcContext;
const { msg, msgStr } = i18n;
@ -14,8 +20,7 @@ const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("emailForgotTitle")}
formNode={
@ -52,7 +57,12 @@ const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
/>
@ -63,6 +73,7 @@ const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
infoNode={msg("emailInstruction")}
/>
);
});
}
);
export default LoginResetPassword;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => {
const LoginUpdatePassword = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
@ -14,8 +20,7 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("updatePasswordTitle")}
formNode={
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
@ -114,6 +119,7 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
}
/>
);
});
}
);
export default LoginUpdatePassword;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n } & KcProps) => {
const LoginUpdateProfile = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
@ -14,8 +20,7 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("loginProfileTitle")}
formNode={
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
@ -73,7 +78,13 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="lastName" name="lastName" defaultValue={user.lastName ?? ""} className={cx(props.kcInputClass)} />
<input
type="text"
id="lastName"
name="lastName"
defaultValue={user.lastName ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
@ -117,6 +128,7 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
}
/>
);
});
}
);
export default LoginUpdateProfile;

View File

@ -4,15 +4,20 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n } & KcProps) => {
const LoginVerifyEmail = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msg } = i18n;
const { url, user } = kcContext;
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("emailVerifyTitle")}
formNode={
@ -29,6 +34,7 @@ const LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcCon
}
/>
);
});
}
);
export default LoginVerifyEmail;

View File

@ -5,7 +5,13 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n } & KcProps) => {
const LogoutConfirm = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, client, logoutConfirm } = kcContext;
const { cx } = useCssAndCx();
@ -14,8 +20,7 @@ const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContex
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("logoutConfirmTitle")}
formNode={
@ -57,6 +62,7 @@ const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContex
}
/>
);
});
}
);
export default LogoutConfirm;

View File

@ -5,7 +5,13 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Register; i18n: I18n } & KcProps) => {
const Register = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Register; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n;
@ -14,8 +20,7 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
headerNode={msg("registerTitle")}
formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
@ -143,7 +148,12 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doRegister")}
/>
@ -153,6 +163,7 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
}
/>
);
});
}
);
export default Register;

View File

@ -1,14 +1,18 @@
import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
import React, { useMemo, memo, useState } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
import type { KcContextBase } 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 { UserProfileFormFields } from "./shared/UserProfileCommons";
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
const RegisterUserProfile = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props_
}: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n;
@ -27,10 +31,9 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
return (
<Template
{...{ kcContext, i18n, ...props }}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true}
doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")}
formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
@ -53,7 +56,12 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doRegister")}
disabled={!isFomSubmittable}
@ -64,157 +72,7 @@ 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;

View File

@ -11,6 +11,7 @@ import type { I18n } from "../i18n";
import memoize from "memoizee";
import { useConst } from "powerhooks/useConst";
import { useConstCallback } from "powerhooks/useConstCallback";
import { Markdown } from "../tools/Markdown";
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
@ -53,7 +54,13 @@ export function useDownloadTerms(params: {
}, []);
}
const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Terms; i18n: I18n } & KcProps) => {
const Terms = memo(
({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Terms; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown);
@ -68,13 +75,12 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("termsTitle")}
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">
<input
className={cx(
@ -102,6 +108,7 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
}
/>
);
});
}
);
export default Terms;

View File

@ -0,0 +1,77 @@
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,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.UpdateUserProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n;
const { url, isAppInitiatedAction } = kcContext;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
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;

View 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>
);
})}
</>
);
}
);

View File

@ -25,7 +25,9 @@ export type KcContextBase =
| KcContextBase.LoginIdpLinkEmail
| KcContextBase.LoginPageExpired
| KcContextBase.LoginConfigTotp
| KcContextBase.LogoutConfirm;
| KcContextBase.LogoutConfirm
| KcContextBase.UpdateUserProfile
| KcContextBase.IdpReviewUserProfile;
export declare namespace KcContextBase {
export type Common = {
@ -270,6 +272,23 @@ export declare namespace KcContextBase {
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 = {

View File

@ -47,8 +47,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
"source": partialKcContextCustomMock
});
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
if (
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;
@ -82,7 +90,9 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes.forEach(partialAttribute => {
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");

View File

@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
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 = {
"url": {
"loginAction": "#",
@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
...registerCommon,
"profile": {
"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,
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
} as any;
})()
attributesByName
}
})
];
@ -423,5 +421,22 @@ export const kcContextMocks: KcContextBase[] = [
"baseUrl": "#"
},
"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
}
})
];

View File

@ -1,10 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import React, { useEffect, useState, useRef } from "react";
import type baseMessages from "./generated_messages/18.0.1/login/en";
import { assert } from "tsafe/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { Markdown } from "../tools/Markdown";
export const fallbackLanguageTag = "en";
@ -76,12 +76,14 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
const refHasStartedFetching = useRef(false);
useEffect(() => {
if (doSkip) {
if (doSkip || refHasStartedFetching.current) {
return;
}
let isMounted = true;
refHasStartedFetching.current = true;
(async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
@ -140,10 +142,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
})()
]).then(modules => modules.map(module => module.default));
if (!isMounted) {
return;
}
setI18n({
...createI18nTranslationFunctions({
"fallbackMessages": {
@ -176,10 +174,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
)
});
})();
return () => {
isMounted = false;
};
}, []);
return i18n ?? null;
@ -240,9 +234,9 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
})();
return doRenderMarkdown ? (
<ReactMarkdown allowDangerousHtml renderers={key === "termsText" ? undefined : { "paragraph": "span" }}>
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
{messageWithArgsInjectedIfAny}
</ReactMarkdown>
</Markdown>
) : (
messageWithArgsInjectedIfAny
);

View File

@ -0,0 +1,3 @@
import Markdown from "react-markdown";
export { Markdown };

View File

@ -4,7 +4,7 @@
"outDir": "../../dist/lib",
"rootDir": ".",
"module": "ES2020",
"target": "ES2020",
"target": "ES2017",
"lib": ["es2015", "DOM", "ES2019.Object"],
"moduleResolution": "node",
"jsx": "react",

View File

@ -304,13 +304,17 @@ export function useGetErrors(params: {
return { getErrors };
}
/**
* NOTE: The attributesWithPassword returned is actually augmented with
* artificial password related attributes only if kcContext.passwordRequired === true
*/
export function useFormValidationSlice(params: {
kcContext: {
messagesPerField: Pick<KcContextBase.Common["messagesPerField"], "existsError" | "get">;
profile: {
attributes: Attribute[];
};
passwordRequired: boolean;
passwordRequired?: boolean;
realm: { registrationEmailAsUsername: boolean };
};
/** NOTE: Try to avoid passing a new ref every render for better performances. */

View File

@ -14,6 +14,7 @@ generateKeycloakThemeResources({
"extraPages": ["my-custom-page.ftl"],
"extraThemeProperties": ["env=test"],
"isStandalone": true,
"urlPathname": "/keycloakify-demo-app/"
"urlPathname": "/keycloakify-demo-app/",
"isSilent": false
}
});

View File

@ -19,7 +19,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
}[e] + ".chunk.js"
}
n.u=function(e){return"static/js/" + e + "." + {
__webpack_require__.u=function(e){return"static/js/" + e + "." + {
147: "6c5cee76",
787: "8da10fcf",
922: "be170a73"
@ -54,10 +54,10 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
}[e] + ".chunk.js"
}
n[(function (){
var pd= Object.getOwnPropertyDescriptor(n, "p");
__webpack_require__[(function (){
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(n, "p", {
Object.defineProperty(__webpack_require__, "p", {
get: function() { return window.kcContext.url.resourcesPath; },
set: function (){}
});
@ -72,7 +72,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
}
t[(function (){
var pd= Object.getOwnPropertyDescriptor(n, "p");
var pd= Object.getOwnPropertyDescriptor(t, "p");
if( pd === undefined || pd.configurable ){
Object.defineProperty(t, "p", {
get: function() { return window.kcContext.url.resourcesPath; },
@ -103,23 +103,23 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
const fixedJsCodeExpected = `
function f() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : "") + a.p + "static/js/" + ({}[e] || e) + "." + {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
function sameAsF() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : "") + a.p + "static/js/" + ({}[e] || e) + "." + {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0"
}[e] + ".chunk.js"
}
n[(function (){
var pd= Object.getOwnPropertyDescriptor(n, "p");
__webpack_require__[(function (){
var pd= Object.getOwnPropertyDescriptor(__webpack_require__, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(n, "p", {
get: function() { return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : "") + p; },
Object.defineProperty(__webpack_require__, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}
@ -133,11 +133,11 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
}
t[(function (){
var pd= Object.getOwnPropertyDescriptor(n, "p");
var pd= Object.getOwnPropertyDescriptor(t, "p");
if( pd === undefined || pd.configurable ){
var p= "";
Object.defineProperty(t, "p", {
get: function() { return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : "") + p; },
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; }
});
}

View File

@ -8,6 +8,7 @@ export function setupSampleReactProject() {
downloadAndUnzip({
"url": "https://github.com/InseeFrLab/keycloakify/releases/download/v0.0.1/sample_build_dir_and_package_json.zip",
"destDirPath": sampleReactProjectDirPath,
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache")
"cacheDirPath": pathJoin(sampleReactProjectDirPath, "build_keycloak", ".cache"),
"isSilent": false
});
}

View File

@ -265,6 +265,11 @@
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519"
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":
version "17.0.45"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
@ -743,14 +748,14 @@ event-emitter@^0.3.5:
d "1"
es5-ext "~0.10.14"
evt@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.0.tgz#5052d3a685bba8f7e7be699b358c1781bd4037bf"
integrity sha512-1V8FnExwqzX2fufT793mHfCnQay078kRdAEDHzJz863pgJK9aeDiWXZFs5aKmzJfIgi+svevtApVnleZXz4Jeg==
evt@^2.4.4:
version "2.4.4"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.4.tgz#37d6e28ccb5b1bc91162fc3d5bcfbeb1ef3191cf"
integrity sha512-w/ZYdPCRdSfslOhcQHq7DuYoaU04YZKkFPyBwF8pYmOkRizivpbI0jZ8ffY/jITzbLo7RZ0wxN2dqyi62kyGwg==
dependencies:
minimal-polyfills "^2.2.2"
run-exclusive "^2.2.16"
tsafe "^1.0.1"
tsafe "^1.1.1"
execa@^5.1.1:
version "5.1.1"
@ -1188,6 +1193,11 @@ minimatch@^3.0.3, minimatch@^3.1.1:
dependencies:
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:
version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -1375,15 +1385,15 @@ please-upgrade-node@^3.2.0:
dependencies:
semver-compare "^1.0.0"
powerhooks@^0.20.15:
version "0.20.15"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.15.tgz#e5093a11f3ed1badbc09944acf5a3fb15ac3f1d7"
integrity sha512-28xNjPTtjPPP8vyi9SWXfn0U9hjxm/UzuarD2uofIXa/CPWmkCeTtikyLoR6yuiE4pOfvdDXVfU5im40GuzJaQ==
powerhooks@^0.20.20:
version "0.20.20"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.20.tgz#f9b2549710f5166f63d80e07c46c16d6da4c6f78"
integrity sha512-98Ymz0bjo5Ds9u273wYz1tdJ51sB1jcyjqGa08mRY5dKumewydA/+71zrFelfgkOLRRhVZ+mWynG6DZ7zOVjrQ==
dependencies:
evt "^2.4.0"
evt "^2.4.4"
memoizee "^0.4.15"
resize-observer-polyfill "^1.5.1"
tsafe "^1.0.1"
tsafe "^1.1.1"
prettier@^2.3.0:
version "2.7.1"
@ -1723,20 +1733,20 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
tsafe@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.0.1.tgz#c8c4eb2d75d1478418a4941307c5dd667fd76d23"
integrity sha512-FgJ1a4rE7YbmW5QIzpsfFl4tsAp0x74FH2bVE6qODb2U8jSrwTr5/ckIazeylme5zXndVbtgKm4BZdqmoGhiPw==
tsafe@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.1.1.tgz#8d6998c726f8c63c518e1d1e283bbcd282a2b9a9"
integrity sha512-Ogblm3uh0dVupcCcC4IT641rnSQ7CW9IO0q8yIncG8OBe4DDXEqGtUE8LWf7+0MK1qZGeWPWEqSxlLzY2xzREA==
tslib@^2.1.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tss-react@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.0.0.tgz#cdd9d4c4ae24de04c13b9deff59b50fdf6ce10ae"
integrity sha512-pPkOKWiWWPbKdQFnGGeHEgRceUwkjrv0eldVCAdBll3j6Y3Ys/xwqsnlWYwWOU3SMJygVRE/S4CsIYx6KPpOkA==
tss-react@^4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.3.3.tgz#fca93e91b93a2e52c40dc904c7067a2b1905a160"
integrity sha512-gXCocAaCDkLpMy0yYdr6AB9/FAetkiRsFnkoDYhjngMBni6ELyZStUwOmZ/LFdBGzKRFlrtext63J+UaaavgZg==
dependencies:
"@emotion/cache" "*"
"@emotion/serialize" "*"