Compare commits

...

66 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
b7e75d8828 Bump beta version 2022-09-01 17:22:35 +02:00
30e20f4e7d Avoid redefining the properties 2022-09-01 17:22:15 +02:00
ce0ab8dccf Better linking script 2022-09-01 16:36:38 +02:00
5b20ab2f7c Upgrade to tss-react v4 2022-09-01 15:13:24 +02:00
daaaed43df Rename keycloakify-demo-app in keycloakify-starter 2022-09-01 14:58:59 +02:00
50 changed files with 2057 additions and 1544 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: push:
branches: branches:
- main - main
- v6
pull_request: pull_request:
branches: branches:
- main - main
- v6
jobs: jobs:
@ -47,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

View File

@ -2,7 +2,7 @@
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png"> <img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
</p> </p>
<p align="center"> <p align="center">
<i>🔏 Create Keycloak themes using React 🔏</i> <i>🔏 Create Keycloak themes using React 🔏</i>
<br> <br>
<br> <br>
<a href="https://github.com/garronej/keycloakify/actions"> <a href="https://github.com/garronej/keycloakify/actions">
@ -27,7 +27,14 @@
<a href="https://www.keycloakify.dev">Home</a> <a href="https://www.keycloakify.dev">Home</a>
- -
<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>
@ -36,8 +43,18 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png"> <img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p> </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 # 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 ## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded. - Bundle size drastically reduced, locals and component dynamically loaded.
@ -45,7 +62,7 @@
- Real i18n API. - Real i18n API.
- Actual documentation for build options. - Actual documentation for build options.
Checkout the migration guide. Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0 ## 5.8.0

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "6.0.0-beta.7", "version": "6.4.3",
"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.3.1", "evt": "^2.4.4",
"memoizee": "^0.4.15", "memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.1", "minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"powerhooks": "^0.20.10", "powerhooks": "^0.20.20",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"tsafe": "^0.10.1", "tsafe": "^1.1.1",
"tss-react": "^3.7.1", "tss-react": "^4.3.3",
"zod": "^3.17.10" "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 { 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 });
})(); })();

View File

@ -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
}); });
})(); })();
} }

View File

@ -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`);
}); });
}); });
} }

View File

@ -11,7 +11,7 @@ type ParsedPackageJson = {
keycloakify?: { keycloakify?: {
extraPages?: string[]; extraPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
isAppAndKeycloakServerSharingSameDomain?: boolean; areAppAndKeycloakServerSharingSameDomain?: boolean;
}; };
}; };
@ -23,7 +23,7 @@ const zParsedPackageJson = z.object({
.object({ .object({
"extraPages": z.array(z.string()).optional(), "extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(), "extraThemeProperties": z.array(z.string()).optional(),
"isAppAndKeycloakServerSharingSameDomain": z.boolean().optional() "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
}) })
.optional() .optional()
}); });
@ -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[];
@ -56,11 +57,11 @@ export namespace BuildOptions {
}; };
export type SameDomain = CommonExternalAssets & { export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true; areAppAndKeycloakServerSharingSameDomain: true;
}; };
export type DifferentDomains = CommonExternalAssets & { export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false; areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string; urlOrigin: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -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
}; };
})(); })();
@ -140,10 +143,10 @@ export function readBuildOptions(params: {
"isStandalone": false "isStandalone": false
}); });
if (parsedPackageJson.keycloakify?.isAppAndKeycloakServerSharingSameDomain) { if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({ return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets, ...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": true "areAppAndKeycloakServerSharingSameDomain": true
}); });
} else { } else {
assert( assert(
@ -155,14 +158,14 @@ export function readBuildOptions(params: {
"public/CNAME file.", "public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ", "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", "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" "in your package.json"
].join(" ") ].join(" ")
); );
return id<BuildOptions.ExternalAssets.DifferentDomains>({ return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets, ...commonExternalAssets,
"isAppAndKeycloakServerSharingSameDomain": false, "areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin, "urlOrigin": url.origin,
"urlPathname": url.pathname "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> <#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 rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1> <#local i = i + 1>

View File

@ -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;
@ -46,11 +48,11 @@ export namespace BuildOptionsLike {
}; };
export type SameDomain = CommonExternalAssets & { export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true; areAppAndKeycloakServerSharingSameDomain: true;
}; };
export type DifferentDomains = CommonExternalAssets & { export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false; areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string; urlOrigin: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -76,7 +78,7 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: { fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) { if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements; break fix_imports_statements;
} }

View File

@ -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 & {
@ -34,11 +36,11 @@ export namespace BuildOptionsLike {
}; };
export type SameDomain = CommonExternalAssets & { export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true; areAppAndKeycloakServerSharingSameDomain: true;
}; };
export type DifferentDomains = CommonExternalAssets & { export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false; areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string; urlOrigin: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -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> = {};
@ -97,7 +100,7 @@ export function generateKeycloakThemeResources(params: {
} }
if (/\.js?$/i.test(filePath)) { if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) { if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined; return undefined;
} }
@ -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");

View File

@ -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)} 🚀`,

View File

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

View File

@ -1,5 +1,6 @@
import { execSync } from "child_process"; import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path"; import { join as pathJoin, relative as pathRelative } from "path";
import { exclude } from "tsafe/exclude";
import * as fs from "fs"; import * as fs from "fs";
const keycloakifyDirPath = pathJoin(__dirname, "..", ".."); const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
@ -60,11 +61,32 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
}); });
}; };
const testAppNames = [process.argv[2] ?? "keycloakify-demo-app"] as const; const testAppPaths = (() => {
const arg = process.argv[2];
const getTestAppPath = (testAppName: typeof testAppNames[number]) => pathJoin(keycloakifyDirPath, "..", testAppName); const testAppNames = arg !== undefined ? [arg] : ["keycloakify-starter", "keycloakify-advanced-starter"];
testAppNames.forEach(testAppName => execSync("yarn install", { "cwd": getTestAppPath(testAppName) })); return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(keycloakifyDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter(exclude(undefined));
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ==="); console.log("=== Linking common dependencies ===");
@ -81,22 +103,24 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
); );
execYarnLink({ "cwd": localInstallPath }); execYarnLink({ "cwd": localInstallPath });
});
testAppNames.forEach(testAppName => commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({ execYarnLink({
"cwd": getTestAppPath(testAppName), "cwd": testAppPath,
"targetModuleName": commonThirdPartyDep "targetModuleName": commonThirdPartyDep
}) })
); )
}); );
console.log("=== Linking in house dependencies ==="); console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") }); execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
testAppNames.forEach(testAppName => testAppPaths.forEach(testAppPath =>
execYarnLink({ execYarnLink({
"cwd": getTestAppPath(testAppName), "cwd": testAppPath,
"targetModuleName": "keycloakify" "targetModuleName": "keycloakify"
}) })
); );

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"; 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
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,31 +4,37 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Error; i18n: I18n } & KcProps) => { const Error = memo(
const { message, client } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Error; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { message, client } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={msg("errorTitle")}
headerNode={msg("errorTitle")} formNode={
formNode={ <div id="kc-error-message">
<div id="kc-error-message"> <p className="instruction">{message.summary}</p>
<p className="instruction">{message.summary}</p> {client !== undefined && client.baseUrl !== undefined && (
{client !== undefined && client.baseUrl !== undefined && ( <p>
<p> <a id="backToApplication" href={client.baseUrl}>
<a id="backToApplication" href={client.baseUrl}> {msg("backToApplication")}
{msg("backToApplication")} </a>
</a> </p>
</p> )}
)} </div>
</div> }
} />
/> );
); }
}); );
export default Error; 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,47 +5,53 @@ import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Info; i18n: I18n } & KcProps) => { const Info = memo(
const { msgStr, msg } = i18n; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Info; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined); assert(kcContext.message !== undefined);
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext; const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>} formNode={
formNode={ <div id="kc-info-message">
<div id="kc-info-message"> <p className="instruction">
<p className="instruction"> {message.summary}
{message.summary}
{requiredActions !== undefined && ( {requiredActions !== undefined && (
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b> <b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
)} )}
</p>
{!skipLink && pageRedirectUri !== undefined ? (
<p>
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
</p> </p>
) : actionUri !== undefined ? ( {!skipLink && pageRedirectUri !== undefined ? (
<p>
<a href={actionUri}>{msg("proceedWithAction")}</a>
</p>
) : (
client.baseUrl !== undefined && (
<p> <p>
<a href={client.baseUrl}>{msg("backToApplication")}</a> <a href={pageRedirectUri}>{msg("backToApplication")}</a>
</p> </p>
) ) : actionUri !== undefined ? (
)} <p>
</div> <a href={actionUri}>{msg("proceedWithAction")}</a>
} </p>
/> ) : (
); client.baseUrl !== undefined && (
}); <p>
<a href={client.baseUrl}>{msg("backToApplication")}</a>
</p>
)
)}
</div>
}
/>
);
}
);
export default Info; export default Info;

View File

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

View File

@ -2,194 +2,202 @@ import React, { useState, memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react"; import type { FormEventHandler } from "react";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Login; i18n: I18n } & KcProps) => { const Login = memo(
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext; ({
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; const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => { const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
e.preventDefault(); e.preventDefault();
setIsLoginButtonDisabled(true); setIsLoginButtonDisabled(true);
const formElement = e.target as HTMLFormElement; const formElement = e.target as HTMLFormElement;
//NOTE: Even if we login with email Keycloak expect username and password in //NOTE: Even if we login with email Keycloak expect username and password in
//the POST request. //the POST request.
formElement.querySelector("input[name='email']")?.setAttribute("name", "username"); formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
formElement.submit(); formElement.submit();
}); });
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayInfo={social.displayInfo}
displayInfo={social.displayInfo} displayWide={realm.password && social.providers !== undefined}
displayWide={realm.password && social.providers !== undefined} headerNode={msg("doLogIn")}
headerNode={msg("doLogIn")} formNode={
formNode={ <div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}>
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}> <div
<div id="kc-form-wrapper"
id="kc-form-wrapper" className={cx(
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])} realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass]
> )}
{realm.password && ( >
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post"> {realm.password && (
<div className={cx(props.kcFormGroupClass)}> <form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
{(() => { <div className={cx(props.kcFormGroupClass)}>
const label = !realm.loginWithEmailAllowed {(() => {
? "username" const label = !realm.loginWithEmailAllowed
: realm.registrationEmailAsUsername ? "username"
? "email" : realm.registrationEmailAsUsername
: "usernameOrEmail"; ? "email"
: "usernameOrEmail";
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label; const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
return ( return (
<> <>
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}> <label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
{msg(label)} {msg(label)}
</label> </label>
<input
tabIndex={1}
id={autoCompleteHelper}
className={cx(props.kcInputClass)}
//NOTE: This is used by Google Chrome auto fill so we use it to tell
//the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects.
name={autoCompleteHelper}
defaultValue={login.username ?? ""}
type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off"
})}
/>
</>
);
})()}
</div>
<div className={cx(props.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}>
{msg("password")}
</label>
<input
tabIndex={2}
id="password"
className={cx(props.kcInputClass)}
name="password"
type="password"
autoComplete="off"
/>
</div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-options">
{realm.rememberMe && !usernameEditDisabled && (
<div className="checkbox">
<label>
<input <input
tabIndex={3} tabIndex={1}
id="rememberMe" id={autoCompleteHelper}
name="rememberMe" className={cx(props.kcInputClass)}
type="checkbox" //NOTE: This is used by Google Chrome auto fill so we use it to tell
{...(login.rememberMe //the browser how to pre fill the form but before submit we put it back
? { //to username because it is what keycloak expects.
"checked": true name={autoCompleteHelper}
} defaultValue={login.username ?? ""}
: {})} type="text"
{...(usernameEditDisabled
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off"
})}
/> />
{msg("rememberMe")} </>
</label> );
</div> })()}
)}
</div> </div>
<div className={cx(props.kcFormOptionsWrapperClass)}> <div className={cx(props.kcFormGroupClass)}>
{realm.resetPasswordAllowed && ( <label htmlFor="password" className={cx(props.kcLabelClass)}>
<span> {msg("password")}
<a tabIndex={5} href={url.loginResetCredentialsUrl}> </label>
{msg("doForgotPassword")} <input
</a> tabIndex={2}
</span> id="password"
)} className={cx(props.kcInputClass)}
name="password"
type="password"
autoComplete="off"
/>
</div> </div>
</div> <div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}> <div id="kc-form-options">
<input {realm.rememberMe && !usernameEditDisabled && (
type="hidden" <div className="checkbox">
id="id-hidden-input" <label>
name="credentialId" <input
{...(auth?.selectedCredential !== undefined tabIndex={3}
? { id="rememberMe"
"value": auth.selectedCredential name="rememberMe"
} type="checkbox"
: {})} {...(login.rememberMe
/> ? {
<input "checked": true
tabIndex={4} }
className={cx( : {})}
props.kcButtonClass, />
props.kcButtonPrimaryClass, {msg("rememberMe")}
props.kcButtonBlockClass, </label>
props.kcButtonLargeClass </div>
)} )}
name="login" </div>
id="kc-login" <div className={cx(props.kcFormOptionsWrapperClass)}>
type="submit" {realm.resetPasswordAllowed && (
value={msgStr("doLogIn")} <span>
disabled={isLoginButtonDisabled} <a tabIndex={5} href={url.loginResetCredentialsUrl}>
/> {msg("doForgotPassword")}
</div> </a>
</form> </span>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
<input
type="hidden"
id="id-hidden-input"
name="credentialId"
{...(auth?.selectedCredential !== undefined
? {
"value": auth.selectedCredential
}
: {})}
/>
<input
tabIndex={4}
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
disabled={isLoginButtonDisabled}
/>
</div>
</form>
)}
</div>
{realm.password && social.providers !== undefined && (
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}>
<ul
className={cx(
props.kcFormSocialAccountListClass,
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass
)}
>
{social.providers.map(p => (
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
<span>{p.displayName}</span>
</a>
</li>
))}
</ul>
</div>
)} )}
</div> </div>
{realm.password && social.providers !== undefined && ( }
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}> infoNode={
<ul realm.password &&
className={cx( realm.registrationAllowed &&
props.kcFormSocialAccountListClass, !registrationDisabled && (
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass <div id="kc-registration">
)} <span>
> {msg("noAccount")}
{social.providers.map(p => ( <a tabIndex={6} href={url.registrationUrl}>
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}> {msg("doRegister")}
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}> </a>
<span>{p.displayName}</span> </span>
</a>
</li>
))}
</ul>
</div> </div>
)} )
</div> }
} />
infoNode={ );
realm.password && }
realm.registrationAllowed && );
!registrationDisabled && (
<div id="kc-registration">
<span>
{msg("noAccount")}
<a tabIndex={6} href={url.registrationUrl}>
{msg("doRegister")}
</a>
</span>
</div>
)
}
/>
);
});
export default Login; export default Login;

View File

@ -2,185 +2,191 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => { const LoginConfigTotp = memo(
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = { const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1", HmacSHA1: "SHA1",
HmacSHA256: "SHA256", HmacSHA256: "SHA256",
HmacSHA512: "SHA512" HmacSHA512: "SHA512"
}; };
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("loginTotpTitle")}
headerNode={msg("loginTotpTitle")} formNode={
formNode={ <>
<> <ol id="kc-totp-settings">
<ol id="kc-totp-settings"> <li>
<li> <p>{msg("loginTotpStep1")}</p>
<p>{msg("loginTotpStep1")}</p>
<ul id="kc-totp-supported-apps"> <ul id="kc-totp-supported-apps">
{totp.policy.supportedApplications.map(app => ( {totp.policy.supportedApplications.map(app => (
<li>{app}</li> <li>{app}</li>
))} ))}
</ul> </ul>
</li> </li>
{mode && mode == "manual" ? ( {mode && mode == "manual" ? (
<> <>
<li>
<p>{msg("loginTotpManualStep2")}</p>
<p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
</p>
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("loginTotpScanBarcode")}
</a>
</p>
</li>
<li>
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (
<li> <li>
<p>{msg("loginTotpManualStep2")}</p> <p>{msg("loginTotpStep2")}</p>
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" />
<br />
<p> <p>
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span> <a href={totp.manualUrl} id="mode-manual">
</p> {msg("loginTotpUnableToScan")}
<p>
<a href={totp.qrUrl} id="mode-barcode">
{msg("loginTotpScanBarcode")}
</a> </a>
</p> </p>
</li> </li>
<li> )}
<p>{msg("loginTotpManualStep3")}</p>
<p>
<ul>
<li id="kc-totp-type">
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li>
<li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
</li>
<li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits}
</li>
{totp.policy.type === "totp" ? (
<li id="kc-totp-period">
{msg("loginTotpInterval")}: {totp.policy.period}
</li>
) : (
<li id="kc-totp-counter">
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
</li>
)}
</ul>
</p>
</li>
</>
) : (
<li> <li>
<p>{msg("loginTotpStep2")}</p> <p>{msg("loginTotpStep3")}</p>
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" /> <p>{msg("loginTotpStep3DeviceName")}</p>
<br />
<p>
<a href={totp.manualUrl} id="mode-manual">
{msg("loginTotpUnableToScan")}
</a>
</p>
</li> </li>
)} </ol>
<li>
<p>{msg("loginTotpStep3")}</p>
<p>{msg("loginTotpStep3DeviceName")}</p>
</li>
</ol>
<form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post"> <form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post">
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<label htmlFor="totp" className={cx(props.kcLabelClass)}> <label htmlFor="totp" className={cx(props.kcLabelClass)}>
{msg("authenticatorCode")} {msg("authenticatorCode")}
</label>{" "} </label>{" "}
<span className="required">*</span> <span className="required">*</span>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="totp" id="totp"
name="totp" name="totp"
autoComplete="off" autoComplete="off"
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
aria-invalid={messagesPerField.existsError("totp")} aria-invalid={messagesPerField.existsError("totp")}
/> />
{messagesPerField.existsError("totp") && ( {messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={cx(props.kcInputErrorMessageClass)} aria-live="polite"> <span id="input-error-otp-code" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("totp")} {messagesPerField.get("totp")}
</span> </span>
)} )}
</div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div> </div>
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
{mode && <input type="hidden" id="mode" value={mode} />}
</div>
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<label htmlFor="userLabel" className={cx(props.kcLabelClass)}> <label htmlFor="userLabel" className={cx(props.kcLabelClass)}>
{msg("loginTotpDeviceName")} {msg("loginTotpDeviceName")}
</label>{" "} </label>{" "}
{totp.otpCredentials.length >= 1 && <span className="required">*</span>} {totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={cx(props.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
)}
</div>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="userLabel"
name="userLabel"
autoComplete="off"
className={cx(props.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")}
/>
{messagesPerField.existsError("userLabel") && (
<span id="input-error-otp-label" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("userLabel")}
</span>
)}
</div>
</div>
{isAppInitiatedAction ? ( {isAppInitiatedAction ? (
<> <>
<input
type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
<button
type="submit"
className={cx(
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
id="cancelTOTPBtn"
name="cancel-aia"
value="true"
>
${msg("doCancel")}
</button>
</>
) : (
<input <input
type="submit" type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
id="saveTOTPBtn" id="saveTOTPBtn"
value={msgStr("doSubmit")} value={msgStr("doSubmit")}
/> />
<button )}
type="submit" </form>
className={cx( </>
props.kcButtonClass, }
props.kcButtonDefaultClass, />
props.kcButtonLargeClass, );
props.kcButtonLargeClass }
)} );
id="cancelTOTPBtn"
name="cancel-aia"
value="true"
>
${msg("doCancel")}
</button>
</>
) : (
<input
type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
id="saveTOTPBtn"
value={msgStr("doSubmit")}
/>
)}
</form>
</>
}
/>
);
});
export default LoginConfigTotp; export default LoginConfigTotp;

View File

@ -2,47 +2,53 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n } & KcProps) => { const LoginIdpLinkConfirm = memo(
const { url, idpAlias } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, idpAlias } = kcContext;
const { msg } = i18n; const { msg } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("confirmLinkIdpTitle")}
headerNode={msg("confirmLinkIdpTitle")} formNode={
formNode={ <form id="kc-register-form" action={url.loginAction} method="post">
<form id="kc-register-form" action={url.loginAction} method="post"> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcFormGroupClass)}> <button
<button type="submit"
type="submit" className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} name="submitAction"
name="submitAction" id="updateProfile"
id="updateProfile" value="updateProfile"
value="updateProfile" >
> {msg("confirmLinkIdpReviewProfile")}
{msg("confirmLinkIdpReviewProfile")} </button>
</button> <button
<button type="submit"
type="submit" className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} name="submitAction"
name="submitAction" id="linkAccount"
id="linkAccount" value="linkAccount"
value="linkAccount" >
> {msg("confirmLinkIdpContinue", idpAlias)}
{msg("confirmLinkIdpContinue", idpAlias)} </button>
</button> </div>
</div> </form>
</form> }
} />
/> );
); }
}); );
export default LoginIdpLinkConfirm; export default LoginIdpLinkConfirm;

View File

@ -4,31 +4,37 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n } & KcProps) => { const LoginIdpLinkEmail = memo(
const { url, realm, brokerContext, idpAlias } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("emailLinkIdpTitle", idpAlias)}
headerNode={msg("emailLinkIdpTitle", idpAlias)} formNode={
formNode={ <>
<> <p id="instruction1" className="instruction">
<p id="instruction1" className="instruction"> {msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)} </p>
</p> <p id="instruction2" className="instruction">
<p id="instruction2" className="instruction"> {msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")} </p>
</p> <p id="instruction3" className="instruction">
<p id="instruction3" className="instruction"> {msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")} </p>
</p> </>
</> }
} />
/> );
); }
}); );
export default LoginIdpLinkEmail; export default LoginIdpLinkEmail;

View File

@ -4,87 +4,98 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => { const LoginOtp = memo(
const { otpLogin, url } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginOtp; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { otpLogin, url } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
useEffect(() => { useEffect(() => {
let isCleanedUp = false; let isCleanedUp = false;
headInsert({ headInsert({
"type": "javascript", "type": "javascript",
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js") "src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
}).then(() => { }).then(() => {
if (isCleanedUp) return; if (isCleanedUp) return;
evaluateInlineScript(); evaluateInlineScript();
}); });
return () => { return () => {
isCleanedUp = true; isCleanedUp = true;
}; };
}, []); }, []);
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("doLogIn")}
headerNode={msg("doLogIn")} formNode={
formNode={ <form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> {otpLogin.userOtpCredentials.length > 1 && (
{otpLogin.userOtpCredentials.length > 1 && ( <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcInputWrapperClass)}>
<div className={cx(props.kcInputWrapperClass)}> {otpLogin.userOtpCredentials.map(otpCredential => (
{otpLogin.userOtpCredentials.map(otpCredential => ( <div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}>
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}> <input type="hidden" value="${otpCredential.id}" />
<input type="hidden" value="${otpCredential.id}" /> <div className={cx(props.kcSelectOTPListItemClass)}>
<div className={cx(props.kcSelectOTPListItemClass)}> <span className={cx(props.kcAuthenticatorOtpCircleClass)} />
<span className={cx(props.kcAuthenticatorOtpCircleClass)} /> <h2 className={cx(props.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2> </div>
</div> </div>
</div> ))}
))} </div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input id="otp" name="otp" autoComplete="off" type="text" className={cx(props.kcInputClass)} autoFocus />
</div> </div>
</div> </div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
{msg("loginOtpOneTime")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcFormGroupClass)}>
<input id="otp" name="otp" autoComplete="off" type="text" className={cx(props.kcInputClass)} autoFocus /> <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
</div> <div className={cx(props.kcFormOptionsWrapperClass)} />
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> <input
<div className={cx(props.kcFormOptionsWrapperClass)} /> className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div> </div>
</form>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> }
<input />
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} );
name="login" }
id="kc-login" );
type="submit"
value={msgStr("doLogIn")}
/>
</div>
</div>
</form>
}
/>
);
});
declare const $: any; declare const $: any;

View File

@ -4,35 +4,41 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n } & KcProps) => { const LoginPageExpired = memo(
const { url } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={msg("pageExpiredTitle")}
headerNode={msg("pageExpiredTitle")} formNode={
formNode={ <>
<> <p id="instruction1" className="instruction">
<p id="instruction1" className="instruction"> {msg("pageExpiredMsg1")}
{msg("pageExpiredMsg1")} <a id="loginRestartLink" href={url.loginRestartFlowUrl}>
<a id="loginRestartLink" href={url.loginRestartFlowUrl}> {msg("doClickHere")}
{msg("doClickHere")} </a>{" "}
</a>{" "} .<br />
.<br /> {msg("pageExpiredMsg2")}{" "}
{msg("pageExpiredMsg2")}{" "} <a id="loginContinueLink" href={url.loginAction}>
<a id="loginContinueLink" href={url.loginAction}> {msg("doClickHere")}
{msg("doClickHere")} </a>{" "}
</a>{" "} .
. </p>
</p> </>
</> }
} />
/> );
); }
}); );
export default LoginPageExpired; export default LoginPageExpired;

View File

@ -2,67 +2,78 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n } & KcProps) => { const LoginResetPassword = memo(
const { url, realm, auth } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, realm, auth } = kcContext;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={msg("emailForgotTitle")}
headerNode={msg("emailForgotTitle")} formNode={
formNode={ <form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcFormGroupClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass)}> <label htmlFor="username" className={cx(props.kcLabelClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}> {!realm.loginWithEmailAllowed
{!realm.loginWithEmailAllowed ? msg("username")
? msg("username") : !realm.registrationEmailAsUsername
: !realm.registrationEmailAsUsername ? msg("usernameOrEmail")
? msg("usernameOrEmail") : msg("email")}
: msg("email")} </label>
</label> </div>
</div> <div className={cx(props.kcInputWrapperClass)}>
<div className={cx(props.kcInputWrapperClass)}> <input
<input type="text"
type="text" id="username"
id="username" name="username"
name="username" className={cx(props.kcInputClass)}
className={cx(props.kcInputClass)} autoFocus
autoFocus defaultValue={auth !== undefined && auth.showUsername ? auth.attemptedUsername : undefined}
defaultValue={auth !== undefined && auth.showUsername ? auth.attemptedUsername : undefined} />
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div> </div>
</div> </div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={cx(
type="submit" props.kcButtonClass,
value={msgStr("doSubmit")} props.kcButtonPrimaryClass,
/> props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doSubmit")}
/>
</div>
</div> </div>
</div> </form>
</form> }
} infoNode={msg("emailInstruction")}
infoNode={msg("emailInstruction")} />
/> );
); }
}); );
export default LoginResetPassword; export default LoginResetPassword;

View File

@ -2,118 +2,124 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => { const LoginUpdatePassword = memo(
const { cx } = useCssAndCx(); ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext; const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("updatePasswordTitle")}
headerNode={msg("updatePasswordTitle")} formNode={
formNode={ <form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> <input
<input type="text"
type="text" id="username"
id="username" name="username"
name="username" value={username}
value={username} readOnly={true}
readOnly={true} autoComplete="username"
autoComplete="username" style={{ display: "none" }}
style={{ display: "none" }} />
/> <input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
<input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}> <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-new" className={cx(props.kcLabelClass)}> <label htmlFor="password-new" className={cx(props.kcLabelClass)}>
{msg("passwordNew")} {msg("passwordNew")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input <input
type="password" type="password"
id="password-new" id="password-new"
name="password-new" name="password-new"
autoFocus autoFocus
autoComplete="new-password" autoComplete="new-password"
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
/> />
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
{msg("passwordConfirm")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="password"
id="password-confirm"
name="password-confirm"
autoComplete="new-password"
className={cx(props.kcInputClass)}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
{isAppInitiatedAction && (
<div className="checkbox">
<label>
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
{msgStr("logoutOtherSessions")}
</label>
</div>
)}
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
{isAppInitiatedAction ? ( <div className={cx(props.kcLabelWrapperClass)}>
<> <label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
{msg("passwordConfirm")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="password"
id="password-confirm"
name="password-confirm"
autoComplete="new-password"
className={cx(props.kcInputClass)}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
{isAppInitiatedAction && (
<div className="checkbox">
<label>
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
{msgStr("logoutOtherSessions")}
</label>
</div>
)}
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
{isAppInitiatedAction ? (
<>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
<button
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit" type="submit"
defaultValue={msgStr("doSubmit")} defaultValue={msgStr("doSubmit")}
/> />
<button )}
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)} </div>
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
)}
</div> </div>
</div> </form>
</form> }
} />
/> );
); }
}); );
export default LoginUpdatePassword; export default LoginUpdatePassword;

View File

@ -2,121 +2,133 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n } & KcProps) => { const LoginUpdateProfile = memo(
const { cx } = useCssAndCx(); ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { cx } = useCssAndCx();
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext; const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("loginProfileTitle")}
headerNode={msg("loginProfileTitle")} formNode={
formNode={ <form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> {user.editUsernameAllowed && (
{user.editUsernameAllowed && ( <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}>
{msg("username")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="username"
name="username"
defaultValue={user.username ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
)}
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}> <label htmlFor="email" className={cx(props.kcLabelClass)}>
{msg("username")} {msg("email")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="email" name="email" defaultValue={user.email ?? ""} className={cx(props.kcInputClass)} />
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
{msg("firstName")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="username" id="firstName"
name="username" name="firstName"
defaultValue={user.username ?? ""} defaultValue={user.firstName ?? ""}
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
/> />
</div> </div>
</div> </div>
)}
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}> <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}> <label htmlFor="lastName" className={cx(props.kcLabelClass)}>
{msg("email")} {msg("lastName")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="email" name="email" defaultValue={user.email ?? ""} className={cx(props.kcInputClass)} /> <input
</div> type="text"
</div> id="lastName"
name="lastName"
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}> defaultValue={user.lastName ?? ""}
<div className={cx(props.kcLabelWrapperClass)}> className={cx(props.kcInputClass)}
<label htmlFor="firstName" className={cx(props.kcLabelClass)}> />
{msg("firstName")} </div>
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="firstName"
name="firstName"
defaultValue={user.firstName ?? ""}
className={cx(props.kcInputClass)}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>
{msg("lastName")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="text" id="lastName" name="lastName" defaultValue={user.lastName ?? ""} className={cx(props.kcInputClass)} />
</div>
</div>
<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)}> <div className={cx(props.kcFormGroupClass)}>
{isAppInitiatedAction ? ( <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<> <div className={cx(props.kcFormOptionsWrapperClass)} />
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
{isAppInitiatedAction ? (
<>
<input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
<button
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit" type="submit"
defaultValue={msgStr("doSubmit")} defaultValue={msgStr("doSubmit")}
/> />
<button )}
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)} </div>
type="submit"
name="cancel-aia"
value="true"
>
{msg("doCancel")}
</button>
</>
) : (
<input
className={cx(
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}
/>
)}
</div> </div>
</div> </form>
</form> }
} />
/> );
); }
}); );
export default LoginUpdateProfile; export default LoginUpdateProfile;

View File

@ -4,31 +4,37 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n } & KcProps) => { const LoginVerifyEmail = memo(
const { msg } = i18n; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msg } = i18n;
const { url, user } = kcContext; const { url, user } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={msg("emailVerifyTitle")}
headerNode={msg("emailVerifyTitle")} formNode={
formNode={ <>
<> <p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p> <p className="instruction">
<p className="instruction"> {msg("emailVerifyInstruction2")}
{msg("emailVerifyInstruction2")} <br />
<br /> <a href={url.loginAction}>{msg("doClickHere")}</a>
<a href={url.loginAction}>{msg("doClickHere")}</a> &nbsp;
&nbsp; {msg("emailVerifyInstruction3")}
{msg("emailVerifyInstruction3")} </p>
</p> </>
</> }
} />
/> );
); }
}); );
export default LoginVerifyEmail; export default LoginVerifyEmail;

View File

@ -1,62 +1,68 @@
import React, { memo } from "react"; import React, { memo } from "react";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n } & KcProps) => { const LogoutConfirm = memo(
const { url, client, logoutConfirm } = kcContext; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, client, logoutConfirm } = kcContext;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} displayMessage={false}
displayMessage={false} headerNode={msg("logoutConfirmTitle")}
headerNode={msg("logoutConfirmTitle")} formNode={
formNode={ <>
<> <div id="kc-logout-confirm" className="content-area">
<div id="kc-logout-confirm" className="content-area"> <p className="instruction">{msg("logoutConfirmHeader")}</p>
<p className="instruction">{msg("logoutConfirmHeader")}</p> <form className="form-actions" action={url.logoutConfirmAction} method="POST">
<form className="form-actions" action={url.logoutConfirmAction} method="POST"> <input type="hidden" name="session_code" value={logoutConfirm.code} />
<input type="hidden" name="session_code" value={logoutConfirm.code} /> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcFormGroupClass)}> <div id="kc-form-options">
<div id="kc-form-options"> <div className={cx(props.kcFormOptionsWrapperClass)}></div>
<div className={cx(props.kcFormOptionsWrapperClass)}></div> </div>
</div> <div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}> <input
<input tabIndex={4}
tabIndex={4} className={cx(
className={cx( props.kcButtonClass,
props.kcButtonClass, props.kcButtonPrimaryClass,
props.kcButtonPrimaryClass, props.kcButtonBlockClass,
props.kcButtonBlockClass, props.kcButtonLargeClass
props.kcButtonLargeClass )}
)} name="confirmLogout"
name="confirmLogout" id="kc-logout"
id="kc-logout" type="submit"
type="submit" value={msgStr("doLogout")}
value={msgStr("doLogout")} />
/> </div>
</div> </div>
</form>
<div id="kc-info-message">
{!logoutConfirm.skipLink && client.baseUrl && (
<p>
<a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
</p>
)}
</div> </div>
</form>
<div id="kc-info-message">
{!logoutConfirm.skipLink && client.baseUrl && (
<p>
<a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
</p>
)}
</div> </div>
</div> </>
</> }
} />
/> );
); }
}); );
export default LogoutConfirm; export default LogoutConfirm;

View File

@ -2,157 +2,168 @@ import React, { memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Register; i18n: I18n } & KcProps) => { const Register = memo(
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext; ({
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; const { msg, msgStr } = i18n;
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
doFetchDefaultThemeResources={true} headerNode={msg("registerTitle")}
headerNode={msg("registerTitle")} formNode={
formNode={ <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post"> <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
{msg("firstName")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="firstName"
className={cx(props.kcInputClass)}
name="firstName"
defaultValue={register.formData.firstName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>
{msg("lastName")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="lastName"
className={cx(props.kcInputClass)}
name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}>
{msg("email")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="email"
className={cx(props.kcInputClass)}
name="email"
defaultValue={register.formData.email ?? ""}
autoComplete="email"
/>
</div>
</div>
{!realm.registrationEmailAsUsername && (
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="username" className={cx(props.kcLabelClass)}> <label htmlFor="firstName" className={cx(props.kcLabelClass)}>
{msg("username")} {msg("firstName")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="username" id="firstName"
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
name="username" name="firstName"
defaultValue={register.formData.username ?? ""} defaultValue={register.formData.firstName ?? ""}
autoComplete="username"
/> />
</div> </div>
</div> </div>
)}
{passwordRequired && ( <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
<> <div className={cx(props.kcLabelWrapperClass)}>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}> <label htmlFor="lastName" className={cx(props.kcLabelClass)}>
{msg("lastName")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="lastName"
className={cx(props.kcInputClass)}
name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="email" className={cx(props.kcLabelClass)}>
{msg("email")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
type="text"
id="email"
className={cx(props.kcInputClass)}
name="email"
defaultValue={register.formData.email ?? ""}
autoComplete="email"
/>
</div>
</div>
{!realm.registrationEmailAsUsername && (
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}> <label htmlFor="username" className={cx(props.kcLabelClass)}>
{msg("password")} {msg("username")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input <input
type="password" type="text"
id="password" id="username"
className={cx(props.kcInputClass)} className={cx(props.kcInputClass)}
name="password" name="username"
autoComplete="new-password" defaultValue={register.formData.username ?? ""}
autoComplete="username"
/> />
</div> </div>
</div> </div>
)}
<div {passwordRequired && (
className={cx( <>
props.kcFormGroupClass, <div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass) <div className={cx(props.kcLabelWrapperClass)}>
)} <label htmlFor="password" className={cx(props.kcLabelClass)}>
> {msg("password")}
<div className={cx(props.kcLabelWrapperClass)}> </label>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}> </div>
{msg("passwordConfirm")} <div className={cx(props.kcInputWrapperClass)}>
</label> <input
type="password"
id="password"
className={cx(props.kcInputClass)}
name="password"
autoComplete="new-password"
/>
</div>
</div> </div>
<div
className={cx(
props.kcFormGroupClass,
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass)
)}
>
<div className={cx(props.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
{msg("passwordConfirm")}
</label>
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
</div>
</div>
</>
)}
{recaptchaRequired && (
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}> <div className={cx(props.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" /> <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div> </div>
</div> </div>
</> )}
)} <div className={cx(props.kcFormGroupClass)}>
{recaptchaRequired && ( <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className="form-group"> <div className={cx(props.kcFormOptionsWrapperClass)}>
<div className={cx(props.kcInputWrapperClass)}> <span>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div> <a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div> </div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={cx(
type="submit" props.kcButtonClass,
value={msgStr("doRegister")} props.kcButtonPrimaryClass,
/> props.kcButtonBlockClass,
props.kcButtonLargeClass
)}
type="submit"
value={msgStr("doRegister")}
/>
</div>
</div> </div>
</div> </form>
</form> }
} />
/> );
); }
}); );
export default Register; export default Register;

View File

@ -1,220 +1,78 @@
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 "tss-react"; 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(
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext; ({
const { msg, msgStr } = i18n;
const { cx, css } = useCssAndCx();
const props = useMemo(
() => ({
...props_,
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 }))
}),
[cx, css]
);
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, ...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">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
{recaptchaRequired && (
<div className="form-group">
<div className={cx(props.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div>
</div>
)}
<div className={cx(props.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</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("doRegister")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
});
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, kcContext,
i18n i18n,
}); doFetchDefaultThemeResources = true,
...props_
}: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
useEffect(() => { const { msg, msgStr } = i18n;
onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]);
const onChangeFactory = useCallbackFactory( const { cx, css } = useCssAndCx();
(
[name]: [string],
[
{
target: { value }
}
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
) =>
formValidationReducer({
"action": "update value",
name,
"newValue": value
})
);
const onBlurFactory = useCallbackFactory(([name]: [string]) => const props = useMemo(
formValidationReducer({ () => ({
"action": "focus lost", ...props_,
name "kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 }))
}) }),
); [cx, css]
);
let currentGroup = ""; const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return ( return (
<> <Template
{attributesWithPassword.map((attribute, i) => { {...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute; displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true}
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name]; headerNode={msg("registerTitle")}
formNode={
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass); <form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} />
return ( {recaptchaRequired && (
<Fragment key={i}> <div className="form-group">
{group !== currentGroup && (currentGroup = group) !== "" && ( <div className={cx(props.kcInputWrapperClass)}>
<div className={formGroupClassName}> <div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
<div className={cx(props.kcContentWrapperClass)}>
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
{advancedMsg(groupDisplayHeader) || currentGroup}
</label>
</div> </div>
{groupDisplayDescription !== "" && (
<div className={cx(props.kcLabelWrapperClass)}>
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
{advancedMsg(groupDisplayDescription)}
</label>
</div>
)}
</div> </div>
)} )}
<div className={formGroupClassName}> <div className={cx(props.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}> <div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}> <div className={cx(props.kcFormOptionsWrapperClass)}>
{advancedMsg(attribute.displayName ?? "")} <span>
</label> <a href={url.loginUrl}>{msg("backToLogin")}</a>
{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> </span>
)} </div>
</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("doRegister")}
disabled={!isFomSubmittable}
/>
</div> </div>
</div> </div>
</Fragment> </form>
); }
})} />
</> );
); }
}); );
export default RegisterUserProfile; export default RegisterUserProfile;

View File

@ -7,7 +7,7 @@ import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { KcTemplateProps } from "./KcProps"; import type { KcTemplateProps } from "./KcProps";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export type TemplateProps = { export type TemplateProps = {

View File

@ -2,7 +2,7 @@ import React, { useEffect, memo } from "react";
import Template from "./Template"; import Template from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react"; import { useCssAndCx } from "../tools/useCssAndCx";
import { Evt } from "evt"; import { Evt } from "evt";
import { useRerenderOnStateChange } from "evt/hooks"; import { useRerenderOnStateChange } from "evt/hooks";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -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);
@ -53,55 +54,61 @@ export function useDownloadTerms(params: {
}, []); }, []);
} }
const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Terms; i18n: I18n } & KcProps) => { const Terms = memo(
const { msg, msgStr } = i18n; ({
kcContext,
i18n,
doFetchDefaultThemeResources = true,
...props
}: { kcContext: KcContextBase.Terms; i18n: I18n; doFetchDefaultThemeResources?: boolean } & KcProps) => {
const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown); useRerenderOnStateChange(evtTermMarkdown);
const { cx } = useCssAndCx(); const { cx } = useCssAndCx();
const { url } = kcContext; const { url } = kcContext;
if (evtTermMarkdown.state === undefined) { if (evtTermMarkdown.state === undefined) {
return null; return null;
}
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...props }}
displayMessage={false}
headerNode={msg("termsTitle")}
formNode={
<>
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
<form className="form-actions" action={url.loginAction} method="POST">
<input
className={cx(
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass
)}
name="accept"
id="kc-accept"
type="submit"
value={msgStr("doAccept")}
/>
<input
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
name="cancel"
id="kc-decline"
type="submit"
value={msgStr("doDecline")}
/>
</form>
<div className="clearfix" />
</>
}
/>
);
} }
);
return (
<Template
{...{ kcContext, i18n, ...props }}
doFetchDefaultThemeResources={true}
displayMessage={false}
headerNode={msg("termsTitle")}
formNode={
<>
<div id="kc-terms-text">{evtTermMarkdown.state}</div>
<form className="form-actions" action={url.loginAction} method="POST">
<input
className={cx(
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass
)}
name="accept"
id="kc-accept"
type="submit"
value={msgStr("doAccept")}
/>
<input
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
name="cancel"
id="kc-decline"
type="submit"
value={msgStr("doDecline")}
/>
</form>
<div className="clearfix" />
</>
}
/>
);
});
export default Terms; 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.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 = {

View File

@ -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;
@ -82,14 +90,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute; id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
}); });
partialAttributes.forEach(partialAttribute => { partialAttributes
const { name } = partialAttribute; .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"); assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any); id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any; id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
}); });
} }
} }

View File

@ -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,
...(() => { attributes,
const attributes: Attribute[] = [ attributesByName
{
"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;
})()
} }
}) })
]; ];
@ -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
}
}) })
]; ];

View File

@ -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 } 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";
@ -76,12 +76,14 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined); const [i18n, setI18n] = useState<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
const refHasStartedFetching = useRef(false);
useEffect(() => { useEffect(() => {
if (doSkip) { if (doSkip || refHasStartedFetching.current) {
return; return;
} }
let isMounted = true; refHasStartedFetching.current = true;
(async () => { (async () => {
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {}; 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)); ]).then(modules => modules.map(module => module.default));
if (!isMounted) {
return;
}
setI18n({ setI18n({
...createI18nTranslationFunctions({ ...createI18nTranslationFunctions({
"fallbackMessages": { "fallbackMessages": {
@ -176,10 +174,6 @@ export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params:
) )
}); });
})(); })();
return () => {
isMounted = false;
};
}, []); }, []);
return i18n ?? null; return i18n ?? null;
@ -240,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
); );

View File

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

View File

@ -0,0 +1,11 @@
import { createMakeStyles } from "tss-react";
const { useStyles } = createMakeStyles({
"useTheme": () => ({})
});
export function useCssAndCx() {
const { css, cx } = useStyles();
return { css, cx };
}

View File

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

View File

@ -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. */

View File

@ -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
} }
}); });

View File

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

View File

@ -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
}); });
} }

140
yarn.lock
View File

@ -21,6 +21,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f"
integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w==
"@babel/helper-string-parser@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz#181f22d28ebe1b3857fa575f5c290b1aaf659b56"
integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw==
"@babel/helper-validator-identifier@^7.18.6": "@babel/helper-validator-identifier@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
@ -50,17 +55,18 @@
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/types@^7.18.6": "@babel/types@^7.18.6":
version "7.18.9" version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.9.tgz#7148d64ba133d8d73a41b3172ac4b83a1452205f" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
integrity sha512-WwMLAg2MvJmt/rKEVQBBhIVffMmnilX4oe0sRe7iPOHIGsqpruFHHdrfj4O1CMMtgMtCU4oPafZjDPCRgO57Wg== integrity sha512-ePqfTihzW0W6XAU+aMw2ykilisStJfDnsejDCXRchCcMJ4O0+8DhPXf2YUbZ6wjBlsEmZwLK/sPweWtu8hcJYQ==
dependencies: dependencies:
"@babel/helper-string-parser" "^7.18.10"
"@babel/helper-validator-identifier" "^7.18.6" "@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0" to-fast-properties "^2.0.0"
"@emotion/babel-plugin@^11.10.0": "@emotion/babel-plugin@^11.10.0":
version "11.10.0" version "11.10.2"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.0.tgz#ae545b8faa6b42d3a50ec86b70b758296f3c4467" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.2.tgz#879db80ba622b3f6076917a1e6f648b1c7d008c7"
integrity sha512-xVnpDAAbtxL1dsuSelU5A7BnY/lftws0wUexNJZTPsvX/1tM4GZJbclgODhvW4E+NH7E5VFcH0bBn30NvniPJA== integrity sha512-xNQ57njWTFVfPAc3cjfuaPdsgLp5QOSuRsj9MA6ndEhH/AzuZM86qIQzt6rq+aGBwj3n5/TkLmU5lhAfdRmogA==
dependencies: dependencies:
"@babel/helper-module-imports" "^7.16.7" "@babel/helper-module-imports" "^7.16.7"
"@babel/plugin-syntax-jsx" "^7.17.12" "@babel/plugin-syntax-jsx" "^7.17.12"
@ -76,9 +82,9 @@
stylis "4.0.13" stylis "4.0.13"
"@emotion/cache@*", "@emotion/cache@^11.10.0": "@emotion/cache@*", "@emotion/cache@^11.10.0":
version "11.10.0" version "11.10.3"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.0.tgz#9eb9d3245c2c25ae31028d5ff0929987824c77da" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87"
integrity sha512-3FoUWnDbHWg/pXGCvL46jvpOSJP0xvRZLY8khUcUHGOBcp0S/MCIk+osp84/dby2Ctahw/Ev4KTHWkY3i0g39g== integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==
dependencies: dependencies:
"@emotion/memoize" "^0.8.0" "@emotion/memoize" "^0.8.0"
"@emotion/sheet" "^1.2.0" "@emotion/sheet" "^1.2.0"
@ -97,14 +103,15 @@
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/react@^11.4.1": "@emotion/react@^11.4.1":
version "11.10.0" version "11.10.4"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.0.tgz#53c577f063f26493f68a05188fb87528d912ff2e" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.4.tgz#9dc6bccbda5d70ff68fdb204746c0e8b13a79199"
integrity sha512-K6z9zlHxxBXwN8TcpwBKcEsBsOw4JWCCmR+BeeOWgqp8GIU1yA2Odd41bwdAAr0ssbQrbJbVnndvv7oiv1bZeQ== integrity sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==
dependencies: dependencies:
"@babel/runtime" "^7.18.3" "@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.10.0" "@emotion/babel-plugin" "^11.10.0"
"@emotion/cache" "^11.10.0" "@emotion/cache" "^11.10.0"
"@emotion/serialize" "^1.1.0" "@emotion/serialize" "^1.1.0"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.0"
"@emotion/utils" "^1.2.0" "@emotion/utils" "^1.2.0"
"@emotion/weak-memoize" "^0.3.0" "@emotion/weak-memoize" "^0.3.0"
hoist-non-react-statics "^3.3.1" hoist-non-react-statics "^3.3.1"
@ -130,6 +137,11 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.0.tgz#a4a36e9cbdc6903737cd20d38033241e1b8833db"
integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw== integrity sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==
"@emotion/use-insertion-effect-with-fallbacks@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
"@emotion/utils@*", "@emotion/utils@^1.2.0": "@emotion/utils@*", "@emotion/utils@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
@ -253,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"
@ -661,9 +678,9 @@ enquirer@^2.3.6:
ansi-colors "^4.1.1" ansi-colors "^4.1.1"
entities@^4.2.0, entities@^4.3.0: entities@^4.2.0, entities@^4.3.0:
version "4.3.1" version "4.4.0"
resolved "https://registry.yarnpkg.com/entities/-/entities-4.3.1.tgz#c34062a94c865c322f9d67b4384e4169bcede6a4" resolved "https://registry.yarnpkg.com/entities/-/entities-4.4.0.tgz#97bdaba170339446495e653cfd2db78962900174"
integrity sha512-o4q/dYJlmyjP2zfnaWDUC6A3BQFmVTX+tZPezK7k0GLSU9QYCauscf5Y+qcEPzKL+EixVouYDgLQK5H9GrLpkg== integrity sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==
error-ex@^1.3.1: error-ex@^1.3.1:
version "1.3.2" version "1.3.2"
@ -673,9 +690,9 @@ error-ex@^1.3.1:
is-arrayish "^0.2.1" is-arrayish "^0.2.1"
es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46: es5-ext@^0.10.35, es5-ext@^0.10.46, es5-ext@^0.10.50, es5-ext@^0.10.53, es5-ext@~0.10.14, es5-ext@~0.10.2, es5-ext@~0.10.46:
version "0.10.61" version "0.10.62"
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.61.tgz#311de37949ef86b6b0dcea894d1ffedb909d3269" resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
integrity sha512-yFhIqQAzu2Ca2I4SE2Au3rxVfmohU9Y7wqGR+s7+H7krk26NXhIRAZDgqd6xqjCEFUomDEA3/Bo/7fKmIkW1kA== integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
dependencies: dependencies:
es6-iterator "^2.0.3" es6-iterator "^2.0.3"
es6-symbol "^3.1.3" es6-symbol "^3.1.3"
@ -731,14 +748,14 @@ event-emitter@^0.3.5:
d "1" d "1"
es5-ext "~0.10.14" es5-ext "~0.10.14"
evt@^2.3.1: evt@^2.4.4:
version "2.3.1" version "2.4.4"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.3.1.tgz#988fc6fc255db8999240e918afe63ba6c325db99" resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.4.tgz#37d6e28ccb5b1bc91162fc3d5bcfbeb1ef3191cf"
integrity sha512-+MU1aA0as6hOnGxzQOw9hV/xiKIB1vAY90S+WD6zMzvvhQHlY4aPHk2b8WpWsVs3XErDzlhGzCESVCAuH9kUiA== integrity sha512-w/ZYdPCRdSfslOhcQHq7DuYoaU04YZKkFPyBwF8pYmOkRizivpbI0jZ8ffY/jITzbLo7RZ0wxN2dqyi62kyGwg==
dependencies: dependencies:
minimal-polyfills "^2.2.1" minimal-polyfills "^2.2.2"
run-exclusive "^2.2.16" run-exclusive "^2.2.16"
tsafe "^0.10.1" tsafe "^1.1.1"
execa@^5.1.1: execa@^5.1.1:
version "5.1.1" version "5.1.1"
@ -756,11 +773,11 @@ execa@^5.1.1:
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
ext@^1.1.2: ext@^1.1.2:
version "1.6.0" version "1.7.0"
resolved "https://registry.yarnpkg.com/ext/-/ext-1.6.0.tgz#3871d50641e874cc172e2b53f919842d19db4c52" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f"
integrity sha512-sdBImtzkq2HpkdRLtlLWDa6w4DX22ijZLKx8BMPUuKe1c5lbN6xwQDQCxSfxBQnHZ13ls/FH0MQZx/q/gr6FQg== integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
dependencies: dependencies:
type "^2.5.0" type "^2.7.2"
extend@^3.0.0: extend@^3.0.0:
version "3.0.2" version "3.0.2"
@ -945,9 +962,9 @@ is-buffer@^2.0.0:
integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
is-core-module@^2.9.0: is-core-module@^2.9.0:
version "2.9.0" version "2.10.0"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed"
integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==
dependencies: dependencies:
has "^1.0.3" has "^1.0.3"
@ -1164,10 +1181,10 @@ mimic-fn@^2.1.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimal-polyfills@^2.2.1: minimal-polyfills@^2.2.1, minimal-polyfills@^2.2.2:
version "2.2.1" version "2.2.2"
resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.2.1.tgz#7249d7ece666d3b4e1ec1c1b8f949eb9d44e2308" resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.2.2.tgz#6b06a004acce420eb91cf94698f5e6e7f2518378"
integrity sha512-WLmHQrsZob4rVYf8yHapZPNJZ3sspGa/sN8abuSD59b0FifDEE7HMfLUi24z7mPZqTpBXy4Svp+iGvAmclCmXg== integrity sha512-eEOUq/LH/DbLWihrxUP050Wi7H/N/I2dQT98Ep6SqOpmIbk4sXOI4wqalve66QoZa+6oljbZWU6I6T4dehQGmw==
minimatch@^3.0.3, minimatch@^3.1.1: minimatch@^3.0.3, minimatch@^3.1.1:
version "3.1.2" version "3.1.2"
@ -1176,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"
@ -1363,15 +1385,15 @@ please-upgrade-node@^3.2.0:
dependencies: dependencies:
semver-compare "^1.0.0" semver-compare "^1.0.0"
powerhooks@^0.20.10: powerhooks@^0.20.20:
version "0.20.10" version "0.20.20"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.10.tgz#c30ba493ea324806100d9f5df889dea19f3a73ff" resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.20.tgz#f9b2549710f5166f63d80e07c46c16d6da4c6f78"
integrity sha512-RiQVBgxLavWFVb3hKmzluykvgeVYgVyGtITlnIjVrtfql3jpCHzuVVTq/49oXp18BNEOKCQXzKfFeb42f1v4Pg== integrity sha512-98Ymz0bjo5Ds9u273wYz1tdJ51sB1jcyjqGa08mRY5dKumewydA/+71zrFelfgkOLRRhVZ+mWynG6DZ7zOVjrQ==
dependencies: dependencies:
evt "^2.3.1" evt "^2.4.4"
memoizee "^0.4.15" memoizee "^0.4.15"
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
tsafe "^0.10.1" tsafe "^1.1.1"
prettier@^2.3.0: prettier@^2.3.0:
version "2.7.1" version "2.7.1"
@ -1711,20 +1733,20 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
tsafe@^0.10.1: tsafe@^1.1.1:
version "0.10.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-0.10.1.tgz#8f100b901e4467c43c0484f56a063f4276683ce0" resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.1.1.tgz#8d6998c726f8c63c518e1d1e283bbcd282a2b9a9"
integrity sha512-S+LrpSjoH5Pah201KS0MxtJn88HVtKf4ZxUoQuW/Hnl4IK6ALu9Qwjed7RbohDeHn+iMuug4c5Mk/z1Cq2G3nw== integrity sha512-Ogblm3uh0dVupcCcC4IT641rnSQ7CW9IO0q8yIncG8OBe4DDXEqGtUE8LWf7+0MK1qZGeWPWEqSxlLzY2xzREA==
tslib@^2.1.0: tslib@^2.1.0:
version "2.4.0" version "2.4.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@^3.7.1: tss-react@^4.3.3:
version "3.7.1" version "4.3.3"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-3.7.1.tgz#119647731490f9e7e62c7f6a38a78df981929a4b" resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.3.3.tgz#fca93e91b93a2e52c40dc904c7067a2b1905a160"
integrity sha512-dfWUoxBlKZfIG9UC1A2h02OmcE/Ni0itCmmZu94E9g+KyBhKMHKcsKvUm0bNlRqTmYjXiCgPJDmj5fyc8CSrLg== integrity sha512-gXCocAaCDkLpMy0yYdr6AB9/FAetkiRsFnkoDYhjngMBni6ELyZStUwOmZ/LFdBGzKRFlrtext63J+UaaavgZg==
dependencies: dependencies:
"@emotion/cache" "*" "@emotion/cache" "*"
"@emotion/serialize" "*" "@emotion/serialize" "*"
@ -1740,15 +1762,15 @@ type@^1.0.1:
resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0"
integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==
type@^2.5.0: type@^2.7.2:
version "2.6.1" version "2.7.2"
resolved "https://registry.yarnpkg.com/type/-/type-2.6.1.tgz#808f389ec777205cc3cd97c1c88ec1a913105aae" resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0"
integrity sha512-OvgH5rB0XM+iDZGQ1Eg/o7IZn0XYJFVrN/9FQ4OWIYILyJJgVP2s1hLTOFn6UOZoDUI/HctGa0PFlE2n2HW3NQ== integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==
typescript@^4.2.3: typescript@^4.2.3:
version "4.7.4" version "4.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
unified@^9.0.0: unified@^9.0.0:
version "9.2.2" version "9.2.2"
@ -1916,6 +1938,6 @@ yocto-queue@^0.1.0:
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.17.10: zod@^3.17.10:
version "3.17.10" version "3.18.0"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.10.tgz#8716a05e6869df6faaa878a44ffe3c79e615defb" resolved "https://registry.yarnpkg.com/zod/-/zod-3.18.0.tgz#2eed58b3cafb8d9a67aa2fee69279702f584f3bc"
integrity sha512-IHXnQYQuOOOL/XgHhgl8YjNxBHi3xX0mVcHmqsvJgcxKkEczPshoWdxqyFwsARpf41E0v9U95WUROqsHHxt0UQ== integrity sha512-gwTm8RfUCe8l9rDwN5r2A17DkAa8Ez4Yl4yXqc5VqeGaXaJahzYYXbTwvhroZi0SNBqTwh/bKm2N0mpCzuw4bA==