diff --git a/.github/FUNDING.yaml b/.github/FUNDING.yaml
new file mode 100644
index 00000000..daf83ff5
--- /dev/null
+++ b/.github/FUNDING.yaml
@@ -0,0 +1,4 @@
+# These are supported funding model platforms
+
+github: [garronej]
+custom: ['https://www.ringerhq.com/experts/garronej']
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 4d84f3b5..4f26b082 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,11 +3,9 @@ on:
push:
branches:
- main
- - v6
pull_request:
branches:
- main
- - v6
jobs:
@@ -15,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps:
- - uses: actions/checkout@v2.3.4
- - uses: actions/setup-node@v2.1.3
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
- uses: bahmutov/npm-install@v1
- - name: If this step fails run 'npm run lint' and 'npm run format' then commit again.
+ - name: If this step fails run 'yarn format' then commit again.
run: |
PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then
@@ -26,22 +24,21 @@ jobs:
fi
$PACKAGE_MANAGER run format:check
test:
- runs-on: macos-10.15
+ runs-on: ${{ matrix.os }}
needs: test_lint
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
- node: [ '15', '14' ]
- name: Test with Node v${{ matrix.node }}
+ node: [ '14', '15' ,'16', '17' ]
+ os: [ windows-latest, ubuntu-latest ]
+ name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps:
- name: Tell if project is using npm or yarn
id: step1
- uses: garronej/ts-ci@v1.1.7
+ uses: garronej/ts-ci@v2.0.2
with:
action_name: tell_if_project_uses_npm_or_yarn
- - uses: actions/checkout@v2.3.4
- - uses: actions/setup-node@v2.1.3
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1
@@ -67,9 +64,9 @@ jobs:
from_version: ${{ steps.step1.outputs.from_version }}
to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
- is_release_beta: ${{steps.step1.outputs.is_release_beta }}
+ is_pre_release: ${{steps.step1.outputs.is_pre_release }}
steps:
- - uses: garronej/ts-ci@v1.1.7
+ - uses: garronej/ts-ci@v2.0.2
id: step1
with:
action_name: is_package_json_version_upgraded
@@ -77,13 +74,13 @@ jobs:
create_github_release:
runs-on: ubuntu-latest
- # We create a release only if the version have been upgraded and we are on a default branch
- # PR on the default branch can release beta but not real release
+ # We create a release only if the version have been upgraded and we are on the main branch
+ # or if we are on a branch of the repo that has an PR open on main.
if: |
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
(
github.event_name == 'push' ||
- needs.check_if_version_upgraded.outputs.is_release_beta == 'true'
+ needs.check_if_version_upgraded.outputs.is_pre_release == 'true'
)
needs:
- check_if_version_upgraded
@@ -95,7 +92,7 @@ jobs:
target_commitish: ${{ github.head_ref || github.ref }}
generate_release_notes: true
draft: false
- prerelease: ${{ needs.check_if_version_upgraded.outputs.is_release_beta == 'true' }}
+ prerelease: ${{ needs.check_if_version_upgraded.outputs.is_pre_release == 'true' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -105,12 +102,11 @@ jobs:
- create_github_release
- check_if_version_upgraded
steps:
- - uses: actions/checkout@v2.3.4
+ - uses: actions/checkout@v3
with:
ref: ${{ github.ref }}
- - uses: actions/setup-node@v2.1.3
+ - uses: actions/setup-node@v3
with:
- node-version: '15'
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: |
@@ -119,7 +115,7 @@ jobs:
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run build
- - run: npx -y -p denoify@1.0.2 enable_short_npm_import_path
+ - run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
env:
DRY_RUN: "0"
- name: Publishing on NPM
@@ -133,11 +129,11 @@ jobs:
false
fi
EXTRA_ARGS=""
- if [ "$IS_BETA" = "true" ]; then
- EXTRA_ARGS="--tag beta"
+ if [ "$IS_PRE_RELEASE" = "true" ]; then
+ EXTRA_ARGS="--tag next"
fi
npm publish $EXTRA_ARGS
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
- IS_BETA: ${{ needs.check_if_version_upgraded.outputs.is_release_beta }}
\ No newline at end of file
+ IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}
\ No newline at end of file
diff --git a/README.md b/README.md
index aabb1362..d1a38810 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- 🔏 Create Keycloak themes using React 🔏
+ 🔏 Create Keycloak themes using React 🔏
@@ -27,7 +27,14 @@
Home
-
Documentation
-
+
+ ---- Project starter / Demo setup ----
+
+ CSS Level customization
+ -
+ Component Level customization
+
+ ----
@@ -36,8 +43,40 @@
+> 🗣 V6 have been released 🎉
+> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
+> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
+
# Changelog highlights
+## 6.8.4
+
+- `@emotion/react` is no longer a peer dependency of Keycloakify.
+
+## 6.8.0
+
+- It is now possible to pass a custom `` component as a prop to `` and every
+ individual page (``, ``, ...) it enables to customize only the header and footer for
+ example without having to switch to a full-component level customization. [See issue](https://github.com/InseeFrLab/keycloakify/issues/191).
+
+## 6.7.0
+
+- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/185).
+
+## 6.6.0
+
+- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/184).
+
+## 6.5.0
+
+- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/InseeFrLab/keycloakify/pull/183).
+
+## 6.4.0
+
+- You can now optionally pass a `doFetchDefaultThemeResources: boolean` prop to every page component and the default ``
+ This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
+ You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
+
## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded.
@@ -45,7 +84,7 @@
- Real i18n API.
- Actual documentation for build options.
-Checkout the migration guide.
+Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0
diff --git a/package.json b/package.json
index eaeb88d1..c89d7d3e 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "6.0.0-beta.14",
+ "version": "6.8.10",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@@ -13,7 +13,8 @@
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl",
- "test": "yarn build:test && node dist_test/test/bin && node dist_test/test/lib",
+ "pretest": "yarn build:test",
+ "test": "node dist_test/test/bin && node dist_test/test/lib",
"generate-messages": "node dist/bin/generate-i18n-messages.js",
"link_in_test_app": "node dist/bin/link_in_test_app.js",
"_format": "prettier '**/*.{ts,tsx,json,md}'",
@@ -54,12 +55,12 @@
],
"homepage": "https://github.com/garronej/keycloakify",
"peerDependencies": {
- "@emotion/react": "^11.4.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
},
"devDependencies": {
- "@emotion/react": "^11.4.1",
+ "@babel/core": "^7.0.0",
"@types/memoizee": "^0.4.7",
+ "@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"copyfiles": "^2.4.1",
@@ -69,21 +70,24 @@
"properties-parser": "^0.3.1",
"react": "18.1.0",
"rimraf": "^3.0.2",
+ "@emotion/react": "^11.10.4",
"typescript": "^4.2.3"
},
"dependencies": {
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
- "evt": "^2.4.0",
+ "evt": "^2.4.13",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.2",
+ "minimist": "^1.2.6",
"path-browserify": "^1.0.1",
- "powerhooks": "^0.20.15",
+ "powerhooks": "^0.20.32",
"react-markdown": "^5.0.3",
+ "rfc4648": "^1.5.2",
"scripting-tools": "^0.19.13",
- "tsafe": "^1.0.1",
- "tss-react": "^4.1.1",
+ "tsafe": "^1.4.1",
+ "tss-react": "4.4.1-rc.0",
"zod": "^3.17.10"
}
}
diff --git a/renovate.json b/renovate.json
index 5d491bee..236da808 100644
--- a/renovate.json
+++ b/renovate.json
@@ -13,11 +13,11 @@
"packageRules": [
{
"packagePatterns": ["*"],
- "excludePackagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
+ "excludePackagePatterns": ["powerhooks", "tsafe", "evt"],
"enabled": false
},
{
- "packagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"],
+ "packagePatterns": ["powerhooks", "tsafe", "evt"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"automergeType": "branch",
diff --git a/src/bin/create-keycloak-email-directory.ts b/src/bin/create-keycloak-email-directory.ts
index 9d72d0bb..dfe70ae5 100644
--- a/src/bin/create-keycloak-email-directory.ts
+++ b/src/bin/create-keycloak-email-directory.ts
@@ -6,11 +6,15 @@ import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
+import { getCliOptions } from "./tools/cliOptions";
+import { getLogger } from "./tools/logger";
if (require.main === module) {
(async () => {
+ const { isSilent } = getCliOptions(process.argv.slice(2));
+ const logger = getLogger({ isSilent });
if (fs.existsSync(keycloakThemeEmailDirPath)) {
- console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
+ logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
@@ -21,7 +25,8 @@ if (require.main === module) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
- "destDirPath": builtinKeycloakThemeTmpDirPath
+ "destDirPath": builtinKeycloakThemeTmpDirPath,
+ isSilent
});
transformCodebase({
@@ -29,7 +34,7 @@ if (require.main === module) {
"destDirPath": keycloakThemeEmailDirPath
});
- console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
+ logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})();
diff --git a/src/bin/download-builtin-keycloak-theme.ts b/src/bin/download-builtin-keycloak-theme.ts
index 3aa7ede9..e98eaddc 100644
--- a/src/bin/download-builtin-keycloak-theme.ts
+++ b/src/bin/download-builtin-keycloak-theme.ts
@@ -4,31 +4,37 @@ import { keycloakThemeBuildingDirPath } from "./keycloakify";
import { join as pathJoin } from "path";
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
+import { getCliOptions } from "./tools/cliOptions";
+import { getLogger } from "./tools/logger";
-export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string }) {
- const { keycloakVersion, destDirPath } = params;
+export function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
+ const { keycloakVersion, destDirPath, isSilent } = params;
for (const ext of ["", "-community"]) {
downloadAndUnzip({
"destDirPath": destDirPath,
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
- "cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache")
+ "cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
+ isSilent
});
}
}
if (require.main === module) {
(async () => {
+ const { isSilent } = getCliOptions(process.argv.slice(2));
+ const logger = getLogger({ isSilent });
const { keycloakVersion } = await promptKeycloakVersion();
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
- console.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
+ logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
downloadBuiltinKeycloakTheme({
keycloakVersion,
- destDirPath
+ destDirPath,
+ isSilent
});
})();
}
diff --git a/src/bin/generate-i18n-messages.ts b/src/bin/generate-i18n-messages.ts
index 8ceabbf6..a0bfbeda 100644
--- a/src/bin/generate-i18n-messages.ts
+++ b/src/bin/generate-i18n-messages.ts
@@ -4,7 +4,8 @@ import { join as pathJoin, relative as pathRelative, dirname as pathDirname } fr
import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
-import { rmSync } from "fs";
+import { getCliOptions } from "./tools/cliOptions";
+import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
@@ -12,16 +13,20 @@ import { rmSync } from "fs";
//@ts-ignore
const propertiesParser = require("properties-parser");
+const { isSilent } = getCliOptions(process.argv.slice(2));
+const logger = getLogger({ isSilent });
+
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
- console.log({ keycloakVersion });
+ logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
- rmSync(tmpDirPath, {recursive: true, force: true});
+ fs.rmSync(tmpDirPath, {recursive: true, force: true});
downloadBuiltinKeycloakTheme({
keycloakVersion,
- "destDirPath": tmpDirPath
+ "destDirPath": tmpDirPath,
+ isSilent
});
type Dictionary = { [idiomId: string]: string };
@@ -48,7 +53,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
});
}
- rmSync(tmpDirPath, {recursive: true, force: true});
+ fs.rmSync(tmpDirPath, {recursive: true, force: true});
Object.keys(record).forEach(pageType => {
const recordForPageType = record[pageType];
@@ -75,7 +80,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
)
);
- console.log(`${filePath} wrote`);
+ logger.log(`${filePath} wrote`);
});
});
}
diff --git a/src/bin/keycloakify/BuildOptions.ts b/src/bin/keycloakify/BuildOptions.ts
index 45fabb76..c6565e62 100644
--- a/src/bin/keycloakify/BuildOptions.ts
+++ b/src/bin/keycloakify/BuildOptions.ts
@@ -11,7 +11,7 @@ type ParsedPackageJson = {
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
- isAppAndKeycloakServerSharingSameDomain?: boolean;
+ areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
@@ -23,7 +23,7 @@ const zParsedPackageJson = z.object({
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
- "isAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
+ "areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
@@ -35,6 +35,7 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
export namespace BuildOptions {
export type Common = {
+ isSilent: boolean;
version: string;
themeName: string;
extraPages?: string[];
@@ -56,11 +57,11 @@ export namespace BuildOptions {
};
export type SameDomain = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: true;
+ areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: false;
+ areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@@ -71,8 +72,9 @@ export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
+ isSilent: boolean;
}): BuildOptions {
- const { packageJson, CNAME, isExternalAssetsCliParamProvided } = params;
+ const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
@@ -130,7 +132,8 @@ export function readBuildOptions(params: {
})(),
"version": version,
extraPages,
- extraThemeProperties
+ extraThemeProperties,
+ isSilent
};
})();
@@ -140,10 +143,10 @@ export function readBuildOptions(params: {
"isStandalone": false
});
- if (parsedPackageJson.keycloakify?.isAppAndKeycloakServerSharingSameDomain) {
+ if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id({
...commonExternalAssets,
- "isAppAndKeycloakServerSharingSameDomain": true
+ "areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
@@ -155,14 +158,14 @@ export function readBuildOptions(params: {
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
- 'admin UI, you can set "keycloakify": { "isAppAndKeycloakServerSharingSameDomain": true }',
+ 'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id({
...commonExternalAssets,
- "isAppAndKeycloakServerSharingSameDomain": false,
+ "areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
diff --git a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
index 365d52cb..41caf3fb 100644
--- a/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
+++ b/src/bin/keycloakify/generateFtl/ftl_object_to_js_code_declaring_an_object.ftl
@@ -32,60 +32,82 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
"printIfExists": function (fieldName, x) {
<#if !messagesPerField?? >
return undefined;
+ <#else>
+ <#list fieldNames as fieldName>
+ if(fieldName === "${fieldName}" ){
+ <#attempt>
+ <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
+ return <#if messagesPerField.existsError('username', 'password')>x<#else>undefined#if>;
+ <#else>
+ return <#if messagesPerField.existsError('${fieldName}')>x<#else>undefined#if>;
+ #if>
+ <#recover>
+ #attempt>
+ }
+ #list>
+ throw new Error("There is no " + fieldName + " field");
#if>
- <#list fieldNames as fieldName>
- if(fieldName === "${fieldName}" ){
- <#attempt>
- return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
- <#recover>
- #attempt>
- }
- #list>
- throw new Error("There is no " + fieldName + " field");
},
"existsError": function (fieldName) {
<#if !messagesPerField?? >
return false;
+ <#else>
+ <#list fieldNames as fieldName>
+ if(fieldName === "${fieldName}" ){
+ <#attempt>
+ <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
+ return <#if messagesPerField.existsError('username', 'password')>true<#else>false#if>;
+ <#else>
+ return <#if messagesPerField.existsError('${fieldName}')>true<#else>false#if>;
+ #if>
+ <#recover>
+ #attempt>
+ }
+ #list>
+ throw new Error("There is no " + fieldName + " field");
#if>
- <#list fieldNames as fieldName>
- if(fieldName === "${fieldName}" ){
- <#attempt>
- return <#if messagesPerField.existsError('${fieldName}')>true<#else>false#if>;
- <#recover>
- #attempt>
- }
- #list>
- throw new Error("There is no " + fieldName + " field");
},
"get": function (fieldName) {
<#if !messagesPerField?? >
return '';
+ <#else>
+ <#list fieldNames as fieldName>
+ if(fieldName === "${fieldName}" ){
+ <#attempt>
+ <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
+ <#if messagesPerField.existsError('username', 'password')>
+ return 'Invalid username or password.';
+ #if>
+ <#else>
+ <#if messagesPerField.existsError('${fieldName}')>
+ return "${messagesPerField.get('${fieldName}')?no_esc}";
+ #if>
+ #if>
+ <#recover>
+ #attempt>
+ }
+ #list>
+ throw new Error("There is no " + fieldName + " field");
#if>
- <#list fieldNames as fieldName>
- if(fieldName === "${fieldName}" ){
- <#attempt>
- <#if messagesPerField.existsError('${fieldName}')>
- return "${messagesPerField.get('${fieldName}')?no_esc}";
- #if>
- <#recover>
- #attempt>
- }
- #list>
- throw new Error("There is no " + fieldName + " field");
},
"exists": function (fieldName) {
<#if !messagesPerField?? >
return false;
+ <#else>
+ <#list fieldNames as fieldName>
+ if(fieldName === "${fieldName}" ){
+ <#attempt>
+ <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
+ return <#if messagesPerField.exists('username', 'password')>true<#else>false#if>
+ <#else>
+ return <#if messagesPerField.exists('${fieldName}')>true<#else>false#if>;
+ #if>
+ <#recover>
+ #attempt>
+ }
+ #list>
+ throw new Error("There is no " + fieldName + " field");
#if>
- <#list fieldNames as fieldName>
- if(fieldName === "${fieldName}" ){
- <#attempt>
- return <#if messagesPerField.exists('${fieldName}')>true<#else>false#if>;
- <#recover>
- #attempt>
- }
- #list>
- throw new Error("There is no " + fieldName + " field");
}
};
@@ -272,6 +294,11 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
<#list object as array_item>
+ <#if !array_item??>
+ <#local out_seq += ["null,"]>
+ <#continue>
+ #if>
+
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1>
diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts
index f57f4990..d600bc60 100644
--- a/src/bin/keycloakify/generateFtl/generateFtl.ts
+++ b/src/bin/keycloakify/generateFtl/generateFtl.ts
@@ -13,6 +13,9 @@ import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [
"login.ftl",
+ "login-username.ftl",
+ "login-password.ftl",
+ "webauthn-authenticate.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
@@ -27,7 +30,9 @@ export const pageIds = [
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
- "logout-confirm.ftl"
+ "logout-confirm.ftl",
+ "update-user-profile.ftl",
+ "idp-review-user-profile.ftl"
] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@@ -46,11 +51,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: true;
+ areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: false;
+ areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@@ -76,7 +81,7 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
- if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
+ if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
diff --git a/src/bin/keycloakify/generateKeycloakThemeResources.ts b/src/bin/keycloakify/generateKeycloakThemeResources.ts
index 85374cfd..2f645fc2 100644
--- a/src/bin/keycloakify/generateKeycloakThemeResources.ts
+++ b/src/bin/keycloakify/generateKeycloakThemeResources.ts
@@ -11,6 +11,7 @@ import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
+import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@@ -19,6 +20,7 @@ export namespace BuildOptionsLike {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
+ isSilent: boolean;
};
export type Standalone = Common & {
@@ -34,11 +36,11 @@ export namespace BuildOptionsLike {
};
export type SameDomain = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: true;
+ areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
- isAppAndKeycloakServerSharingSameDomain: false;
+ areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
@@ -60,6 +62,7 @@ export function generateKeycloakThemeResources(params: {
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
+ const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record = {};
@@ -97,7 +100,7 @@ export function generateKeycloakThemeResources(params: {
}
if (/\.js?$/i.test(filePath)) {
- if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) {
+ if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
@@ -117,7 +120,7 @@ export function generateKeycloakThemeResources(params: {
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
- console.log(
+ logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
@@ -154,7 +157,8 @@ export function generateKeycloakThemeResources(params: {
downloadBuiltinKeycloakTheme({
keycloakVersion,
- "destDirPath": tmpDirPath
+ "destDirPath": tmpDirPath,
+ isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts
index d30f3f2e..ab893c18 100644
--- a/src/bin/keycloakify/keycloakify.ts
+++ b/src/bin/keycloakify/keycloakify.ts
@@ -5,6 +5,8 @@ import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import * as fs from "fs";
import { readBuildOptions } from "./BuildOptions";
+import { getLogger } from "../tools/logger";
+import { getCliOptions } from "../tools/cliOptions";
const reactProjectDirPath = process.cwd();
@@ -12,7 +14,9 @@ export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
export function main() {
- console.log("🔏 Building the keycloak theme...⌚");
+ const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
+ const logger = getLogger({ isSilent });
+ logger.log("🔏 Building the keycloak theme...⌚");
const buildOptions = readBuildOptions({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
@@ -25,7 +29,8 @@ export function main() {
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
- "isExternalAssetsCliParamProvided": process.argv[2]?.toLowerCase() === "--external-assets"
+ "isExternalAssetsCliParamProvided": hasExternalAssets,
+ "isSilent": isSilent
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
@@ -51,7 +56,7 @@ export function main() {
});
//We want, however, to test in a container running the latest Keycloak version
- const containerKeycloakVersion = "18.0.2";
+ const containerKeycloakVersion = "20.0.1";
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath,
@@ -59,7 +64,7 @@ export function main() {
buildOptions
});
- console.log(
+ logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
diff --git a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts b/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts
index d6c0c25b..ac0ffceb 100644
--- a/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts
+++ b/src/bin/keycloakify/replacers/replaceImportsFromStaticInJsCode.ts
@@ -57,7 +57,7 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
: `
var p= "";
Object.defineProperty(${n}, "p", {
- get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : p; },
+ get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
@@ -73,13 +73,13 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
- : `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group}) + "static/`
+ : `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
- : `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group2}) + ${group3},`
+ : `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };
diff --git a/src/bin/tools/cliOptions.ts b/src/bin/tools/cliOptions.ts
new file mode 100644
index 00000000..97f7c609
--- /dev/null
+++ b/src/bin/tools/cliOptions.ts
@@ -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
+ };
+};
diff --git a/src/bin/tools/downloadAndUnzip.ts b/src/bin/tools/downloadAndUnzip.ts
index d3aa0e67..8ff0a362 100644
--- a/src/bin/tools/downloadAndUnzip.ts
+++ b/src/bin/tools/downloadAndUnzip.ts
@@ -5,7 +5,13 @@ import { transformCodebase } from "./transformCodebase";
import * as crypto from "crypto";
/** assert url ends with .zip */
-export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string; cacheDirPath: string }) {
+export function downloadAndUnzip(params: {
+ isSilent: boolean;
+ url: string;
+ destDirPath: string;
+ pathOfDirToExtractInArchive?: string;
+ cacheDirPath: string;
+}) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const extractDirPath = pathJoin(
@@ -53,7 +59,7 @@ export function downloadAndUnzip(params: { url: string; destDirPath: string; pat
const zipFileBasename = pathBasename(url);
- execSync(`curl -L ${url} -o ${zipFileBasename}`, { "cwd": extractDirPath });
+ execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath
diff --git a/src/bin/tools/logger.ts b/src/bin/tools/logger.ts
new file mode 100644
index 00000000..638d93d5
--- /dev/null
+++ b/src/bin/tools/logger.ts
@@ -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);
+ }
+ };
+};
diff --git a/src/lib/components/Error.tsx b/src/lib/components/Error.tsx
index 04124c1b..06fcee6b 100644
--- a/src/lib/components/Error.tsx
+++ b/src/lib/components/Error.tsx
@@ -1,18 +1,27 @@
import React, { memo } from "react";
-import Template from "./Template";
+import DefaultTemplate from "./Template";
+import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
-const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Error; i18n: I18n } & KcProps) => {
+export type ErrorProps = KcProps & {
+ kcContext: KcContextBase.Error;
+ i18n: I18n;
+ doFetchDefaultThemeResources?: boolean;
+ Template?: (props: TemplateProps) => JSX.Element | null;
+};
+
+const Error = memo((props: ErrorProps) => {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
+
const { message, client } = kcContext;
const { msg } = i18n;
return (
JSX.Element | null;
+};
+
+const IdpReviewUserProfile = memo((props: IdpReviewUserProfileProps) => {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
+
+ const { msg, msgStr } = i18n;
+
+ const { url } = kcContext;
+
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
+
+ return (
+
+
+
+
+
+ }
+ />
+ );
+});
+
+export default IdpReviewUserProfile;
diff --git a/src/lib/components/Info.tsx b/src/lib/components/Info.tsx
index d6a297d3..851327c5 100644
--- a/src/lib/components/Info.tsx
+++ b/src/lib/components/Info.tsx
@@ -1,11 +1,21 @@
import React, { memo } from "react";
-import Template from "./Template";
+import DefaultTemplate from "./Template";
+import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n";
-const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Info; i18n: I18n } & KcProps) => {
+export type InfoProps = KcProps & {
+ kcContext: KcContextBase.Info;
+ i18n: I18n;
+ doFetchDefaultThemeResources?: boolean;
+ Template?: (props: TemplateProps) => JSX.Element | null;
+};
+
+const Info = memo((props: InfoProps) => {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
+
const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined);
@@ -14,8 +24,7 @@ const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Inf
return (
{messageHeader}> : <>{message.summary}>}
formNode={
diff --git a/src/lib/components/KcApp.tsx b/src/lib/components/KcApp.tsx
index 0cee851d..cf22d9c8 100644
--- a/src/lib/components/KcApp.tsx
+++ b/src/lib/components/KcApp.tsx
@@ -3,6 +3,8 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps";
import { __unsafe_useI18n as useI18n } from "../i18n";
import type { I18n } from "../i18n";
+import DefaultTemplate from "./Template";
+import type { TemplateProps } from "./Template";
const Login = lazy(() => import("./Login"));
const Register = lazy(() => import("./Register"));
@@ -13,6 +15,9 @@ const LoginResetPassword = lazy(() => import("./LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail"));
const Terms = lazy(() => import("./Terms"));
const LoginOtp = lazy(() => import("./LoginOtp"));
+const LoginPassword = lazy(() => import("./LoginPassword"));
+const LoginUsername = lazy(() => import("./LoginUsername"));
+const WebauthnAuthenticate = lazy(() => import("./WebauthnAuthenticate"));
const LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
@@ -20,8 +25,19 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
+const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
+const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
+
+export type KcAppProps = KcProps & {
+ kcContext: KcContextBase;
+ i18n?: I18n;
+ doFetchDefaultThemeResources?: boolean;
+ Template?: (props: TemplateProps) => JSX.Element | null;
+};
+
+const KcApp = memo((props_: KcAppProps) => {
+ const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
-const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
@@ -36,44 +52,54 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
return null;
}
- const props = { i18n, ...kcProps };
+ const commonProps = { i18n, Template, ...kcProps };
return (
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
- return ;
+ return ;
case "register.ftl":
- return ;
+ return ;
case "register-user-profile.ftl":
- return ;
+ return ;
case "info.ftl":
- return ;
+ return ;
case "error.ftl":
- return ;
+ return ;
case "login-reset-password.ftl":
- return ;
+ return ;
case "login-verify-email.ftl":
- return ;
+ return ;
case "terms.ftl":
- return ;
+ return ;
case "login-otp.ftl":
- return ;
+ return ;
+ case "login-username.ftl":
+ return ;
+ case "login-password.ftl":
+ return ;
+ case "webauthn-authenticate.ftl":
+ return ;
case "login-update-password.ftl":
- return ;
+ return ;
case "login-update-profile.ftl":
- return ;
+ return ;
case "login-idp-link-confirm.ftl":
- return ;
+ return ;
case "login-idp-link-email.ftl":
- return ;
+ return ;
case "login-page-expired.ftl":
- return ;
+ return ;
case "login-config-totp.ftl":
- return ;
+ return ;
case "logout-confirm.ftl":
- return ;
+ return ;
+ case "update-user-profile.ftl":
+ return ;
+ case "idp-review-user-profile.ftl":
+ return ;
}
})()}
diff --git a/src/lib/components/KcProps.ts b/src/lib/components/KcProps.ts
index c38cc5d1..03551616 100644
--- a/src/lib/components/KcProps.ts
+++ b/src/lib/components/KcProps.ts
@@ -84,6 +84,7 @@ export type KcProps = KcPropsGeneric<
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
+ | "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
@@ -105,12 +106,16 @@ export type KcProps = KcPropsGeneric<
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
+ | "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
+ | "kcSelectAuthListItemIconPropertyClass"
+ | "kcSelectAuthListItemIconClass"
+ | "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
@@ -138,6 +143,7 @@ export const defaultKcProps = {
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
+ "kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
@@ -173,6 +179,10 @@ export const defaultKcProps = {
// css classes for select-authenticator form
"kcSelectAuthListClass": ["list-group", "list-view-pf"],
"kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"],
+ "kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
+ "kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
+ "kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
+ "kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
"kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
diff --git a/src/lib/components/Login.tsx b/src/lib/components/Login.tsx
index 239b2ccb..a12a2c4c 100644
--- a/src/lib/components/Login.tsx
+++ b/src/lib/components/Login.tsx
@@ -1,19 +1,27 @@
import React, { useState, memo } from "react";
-import Template from "./Template";
+import DefaultTemplate from "./Template";
+import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
-import { useCssAndCx } from "../tools/useCssAndCx";
+import { clsx } from "../tools/clsx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
-const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Login; i18n: I18n } & KcProps) => {
+export type LoginProps = KcProps & {
+ kcContext: KcContextBase.Login;
+ i18n: I18n;
+ doFetchDefaultThemeResources?: boolean;
+ Template?: (props: TemplateProps) => JSX.Element | null;
+};
+
+const Login = memo((props: LoginProps) => {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
+
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n;
- const { cx } = useCssAndCx();
-
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback>(e => {
@@ -32,20 +40,21 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
return (
+