Compare commits
146 Commits
v6.0.0-bet
...
v6.8.15
Author | SHA1 | Date | |
---|---|---|---|
dcec058a22 | |||
2bdc6b156b | |||
84ca9e6b81 | |||
11cb0fd2db | |||
3f620ffb6f | |||
a4d2de23a1 | |||
016b15b437 | |||
6fb936798e | |||
a692b87843 | |||
19663885a4 | |||
49b87777f9 | |||
d4523bb1e6 | |||
e3200899e2 | |||
36c7a1ab9e | |||
c54fbd5eca | |||
bbe828071e | |||
23f6c7db00 | |||
b1ea9e7a71 | |||
fb71d0e272 | |||
fa72a29999 | |||
af77b31d54 | |||
8280dace26 | |||
ecaf1c7b7c | |||
8702ec29a8 | |||
d8206434bc | |||
c71c2a8710 | |||
e55b881017 | |||
ab906ec417 | |||
0b1ff529f7 | |||
85a6835748 | |||
259271bc0f | |||
b7bc0f178b | |||
688455d0aa | |||
3c96d2ea42 | |||
ab81481e5a | |||
a429ad5dcf | |||
5e1c5b510b | |||
9e63183f4b | |||
b1e740f026 | |||
ce4ea55438 | |||
18ab7cd22f | |||
8807743daf | |||
aad50377ff | |||
4b3ae58ea7 | |||
ce2c68ecc9 | |||
0c155a7a2e | |||
afddfe8b58 | |||
5fa0915271 | |||
6a0a170b17 | |||
4dde5b6e45 | |||
4b93a1cb9e | |||
e3a0639a0c | |||
4d3220820b | |||
a4ac9fb0f3 | |||
1ff79ecf07 | |||
1166b16420 | |||
213224942f | |||
ff16e66275 | |||
3c338e983f | |||
2c11ba6520 | |||
9a21656706 | |||
e96ee5ba53 | |||
b421633a8a | |||
e2e0d62560 | |||
c71fb06940 | |||
e2171af99c | |||
8cebf049d4 | |||
ef139ed1cc | |||
d717de006a | |||
a44f091878 | |||
1b37ba5339 | |||
bbaa90e997 | |||
86e6c4a419 | |||
4159883791 | |||
d8b00da3a1 | |||
a24945bc1b | |||
158759493f | |||
36e32d6ddc | |||
84908e2ec0 | |||
a2dc51d811 | |||
fb3b0e2c29 | |||
1a3e4c68bb | |||
11b2342da0 | |||
80d4a808d3 | |||
da4146eb59 | |||
a0be35db8b | |||
14db9cd523 | |||
0c315385dd | |||
c0a0eb02fb | |||
ee407c32ad | |||
9262d21829 | |||
a13f710325 | |||
eac1a6036f | |||
987f3d7586 | |||
875322669c | |||
33a264b3d0 | |||
c059eff170 | |||
b4a22fc9dd | |||
6d1cbdc463 | |||
2bfbba4daf | |||
21ffe82bde | |||
8e6f597027 | |||
16c5065560 | |||
c4b985f1a4 | |||
042747c7d2 | |||
e4a46f31de | |||
6d9e62d2b4 | |||
9caaa507b1 | |||
5c7d3c5b44 | |||
8bac57d87a | |||
b8d759cd63 | |||
da72e3e5ac | |||
2afd36fee0 | |||
b7e75d8828 | |||
30e20f4e7d | |||
ce0ab8dccf | |||
5b20ab2f7c | |||
daaaed43df | |||
3a4bd791ad | |||
eecddd7f6b | |||
a34eaa136e | |||
53be8b5e96 | |||
f0ae5ea908 | |||
9910556a8b | |||
5997416e1b | |||
9a9fc56f85 | |||
2a5e919f29 | |||
8031d51e15 | |||
56ce9c0d0d | |||
8cd584cbd5 | |||
f5b87f4669 | |||
a1a65c5529 | |||
832434095e | |||
b85f1ef351 | |||
8bee5d788e | |||
0752d857e2 | |||
07e4056694 | |||
0eb4ab85b3 | |||
69ef47daf8 | |||
6eaa1f69ac | |||
5aab75fae0 | |||
7407c98005 | |||
dcd4322e44 | |||
81a4d46b08 | |||
e85895ab55 | |||
095bdb16ba |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,3 +1,3 @@
|
||||
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
||||
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
||||
src/bin/keycloakify/index.ts -linguist-detectable
|
||||
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
||||
|
4
.github/FUNDING.yaml
vendored
Normal file
4
.github/FUNDING.yaml
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [garronej]
|
||||
custom: ['https://www.ringerhq.com/experts/garronej']
|
46
.github/workflows/ci.yaml
vendored
46
.github/workflows/ci.yaml
vendored
@ -13,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
|
||||
@ -24,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.4
|
||||
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
|
||||
@ -65,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.4
|
||||
- uses: garronej/ts-ci@v2.0.2
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
@ -75,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
|
||||
@ -93,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 }}
|
||||
|
||||
@ -103,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: |
|
||||
@ -117,7 +115,7 @@ jobs:
|
||||
PACKAGE_MANAGER=yarn
|
||||
fi
|
||||
$PACKAGE_MANAGER run build
|
||||
- run: npx -y -p denoify@0.6.5 denoify_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
|
||||
@ -131,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 }}
|
||||
IS_PRE_RELEASE: ${{ needs.check_if_version_upgraded.outputs.is_pre_release }}
|
@ -5,7 +5,7 @@
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"quoteProps": "preserve",
|
||||
"trailingComma": "all",
|
||||
"trailingComma": "none",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
52
README.md
52
README.md
@ -2,7 +2,7 @@
|
||||
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
|
||||
</p>
|
||||
<p align="center">
|
||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
||||
<i>🔏 Create Keycloak themes using React 🔏</i>
|
||||
<br>
|
||||
<br>
|
||||
<a href="https://github.com/garronej/keycloakify/actions">
|
||||
@ -27,7 +27,14 @@
|
||||
<a href="https://www.keycloakify.dev">Home</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>
|
||||
|
||||
@ -36,8 +43,49 @@
|
||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||
</p>
|
||||
|
||||
> 🗣 V6 have been released 🎉
|
||||
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
|
||||
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
|
||||
|
||||
# Changelog highlights
|
||||
|
||||
## 6.8.4
|
||||
|
||||
- `@emotion/react` is no longer a peer dependency of Keycloakify.
|
||||
|
||||
## 6.8.0
|
||||
|
||||
- It is now possible to pass a custom `<Template />` component as a prop to `<KcApp />` and every
|
||||
individual page (`<Login />`, `<RegisterUserProfile />`, ...) 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 `<KcApp />`
|
||||
This enables you to prevent the default CSS and JS that comes with the builtin Keycloak theme to be downloaded.
|
||||
You'll get [a black slate](https://user-images.githubusercontent.com/6702424/192619083-4baa5df4-4a21-4ec7-8e28-d200d1208299.png).
|
||||
|
||||
## 6.0.0
|
||||
|
||||
- Bundle size drastically reduced, locals and component dynamically loaded.
|
||||
- First print much quicker, use of React.lazy() everywhere.
|
||||
- Real i18n API.
|
||||
- Actual documentation for build options.
|
||||
|
||||
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
|
||||
|
||||
## 5.8.0
|
||||
|
||||
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/InseeFrLab/keycloakify/issues/141)
|
||||
|
25
package.json
25
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "6.0.0-beta.2",
|
||||
"version": "6.8.15",
|
||||
"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}'",
|
||||
@ -21,7 +22,7 @@
|
||||
"format:check": "yarn _format --list-different"
|
||||
},
|
||||
"bin": {
|
||||
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
|
||||
"keycloakify": "dist/bin/keycloakify/index.js",
|
||||
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
|
||||
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
|
||||
},
|
||||
@ -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,20 +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.3.1",
|
||||
"evt": "^2.4.13",
|
||||
"memoizee": "^0.4.15",
|
||||
"minimal-polyfills": "^2.2.1",
|
||||
"minimal-polyfills": "^2.2.2",
|
||||
"minimist": "^1.2.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"powerhooks": "^0.20.10",
|
||||
"powerhooks": "^0.22.0",
|
||||
"react-markdown": "^5.0.3",
|
||||
"rfc4648": "^1.5.2",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"tsafe": "^0.10.1",
|
||||
"tss-react": "^3.7.1"
|
||||
"tsafe": "^1.4.1",
|
||||
"tss-react": "4.4.1-rc.0",
|
||||
"zod": "^3.17.10"
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -1,139 +0,0 @@
|
||||
import cheerio from "cheerio";
|
||||
import { replaceImportsFromStaticInJsCode, replaceImportsInInlineCssCode, generateCssCodeToDefineGlobals } from "../replaceImportFromStatic";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
|
||||
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
|
||||
export const pageIds = [
|
||||
"login.ftl",
|
||||
"register.ftl",
|
||||
"register-user-profile.ftl",
|
||||
"info.ftl",
|
||||
"error.ftl",
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-idp-link-email.ftl",
|
||||
"login-page-expired.ftl",
|
||||
"login-config-totp.ftl",
|
||||
"logout-confirm.ftl",
|
||||
] as const;
|
||||
|
||||
export type PageId = typeof pageIds[number];
|
||||
|
||||
export function generateFtlFilesCodeFactory(params: {
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
indexHtmlCode: string;
|
||||
urlPathname: string;
|
||||
urlOrigin: undefined | string;
|
||||
}) {
|
||||
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
$("script:not([src])").each((...[, element]) => {
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": $(element).html()!,
|
||||
urlOrigin,
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
});
|
||||
|
||||
$("style").each((...[, element]) => {
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
"cssCode": $(element).html()!,
|
||||
"urlPathname": params.urlPathname,
|
||||
urlOrigin,
|
||||
});
|
||||
|
||||
$(element).text(fixedCssCode);
|
||||
});
|
||||
|
||||
(
|
||||
[
|
||||
["link", "href"],
|
||||
["script", "src"],
|
||||
] as const
|
||||
).forEach(([selector, attrName]) =>
|
||||
$(selector).each((...[, element]) => {
|
||||
const href = $(element).attr(attrName);
|
||||
|
||||
if (href === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(element).attr(
|
||||
attrName,
|
||||
urlOrigin !== undefined
|
||||
? href.replace(/^\//, `${urlOrigin}/`)
|
||||
: href.replace(new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/"),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const replaceValueBySearchValue = {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||
"<#if scripts??>",
|
||||
" <#list scripts as script>",
|
||||
' <script src="${script}" type="text/javascript"></script>',
|
||||
" </#list>",
|
||||
"</#if>",
|
||||
].join("\n"),
|
||||
};
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
...(Object.keys(cssGlobalsToDefine).length === 0
|
||||
? []
|
||||
: [
|
||||
"",
|
||||
"<style>",
|
||||
generateCssCodeToDefineGlobals({
|
||||
cssGlobalsToDefine,
|
||||
urlPathname,
|
||||
}).cssCodeToPrependInHead,
|
||||
"</style>",
|
||||
"",
|
||||
]),
|
||||
"<script>",
|
||||
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||
"</script>",
|
||||
"",
|
||||
objectKeys(replaceValueBySearchValue)[1],
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(params: { pageId: string }): {
|
||||
ftlCode: string;
|
||||
} {
|
||||
const { pageId } = params;
|
||||
|
||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||
|
||||
let ftlCode = $.html();
|
||||
|
||||
Object.entries({
|
||||
...replaceValueBySearchValue,
|
||||
//If updated, don't forget to change in the ftl script as well.
|
||||
"PAGE_ID_xIgLsPgGId9D8e": pageId,
|
||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||
|
||||
return { ftlCode };
|
||||
}
|
||||
|
||||
return { generateFtlFilesCode };
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
export * from "./build-keycloak-theme";
|
||||
import { main } from "./build-keycloak-theme";
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
@ -1,116 +0,0 @@
|
||||
import * as crypto from "crypto";
|
||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
||||
|
||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOrigin: undefined | string }): { fixedJsCode: string } {
|
||||
/*
|
||||
NOTE:
|
||||
|
||||
When we have urlOrigin defined it means that
|
||||
we are building with --external-assets
|
||||
so we have to make sur that the fixed js code will run
|
||||
inside and outside keycloak.
|
||||
|
||||
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||
will always run in keycloak context.
|
||||
*/
|
||||
|
||||
const { jsCode, urlOrigin } = params;
|
||||
|
||||
const fixedJsCode = jsCode
|
||||
.replace(
|
||||
/([a-zA-Z]+)\.([a-zA-Z]+)=function\(([a-zA-Z]+)\){return"static\/js\/"/g,
|
||||
(...[, n, u, e]) => `
|
||||
${n}[(function(){
|
||||
${
|
||||
urlOrigin === undefined
|
||||
? `
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||
set: function (){}
|
||||
});
|
||||
`
|
||||
: `
|
||||
var p= "";
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + p; },
|
||||
set: function (value){ p = value;}
|
||||
});
|
||||
`
|
||||
}
|
||||
return "${u}";
|
||||
})()] = function(${e}) { return "${urlOrigin === undefined ? "/build/" : ""}static/js/"`,
|
||||
)
|
||||
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||
urlOrigin === undefined
|
||||
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||
: `("${ftlValuesGlobalName}" in window ? "${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]) =>
|
||||
urlOrigin === undefined
|
||||
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
}
|
||||
|
||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPathname: string; urlOrigin: undefined | string }): {
|
||||
fixedCssCode: string;
|
||||
} {
|
||||
const { cssCode, urlPathname, urlOrigin } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
}
|
||||
|
||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
fixedCssCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
} {
|
||||
const { cssCode } = params;
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`)),
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; urlPathname: string }): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
const { cssGlobalsToDefine, urlPathname } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName =>
|
||||
[
|
||||
`--${cssVariableName}:`,
|
||||
cssGlobalsToDefine[cssVariableName].replace(
|
||||
new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"),
|
||||
"url(${url.resourcesPath}/build/",
|
||||
),
|
||||
].join(" "),
|
||||
)
|
||||
.map(line => ` ${line};`),
|
||||
"}",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
@ -1,16 +1,20 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||
import { keycloakThemeEmailDirPath } from "./build-keycloak-theme";
|
||||
import { keycloakThemeEmailDirPath } from "./keycloakify";
|
||||
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);
|
||||
}
|
||||
@ -22,14 +26,15 @@ if (require.main === module) {
|
||||
downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||
"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 });
|
||||
})();
|
||||
|
@ -1,33 +1,40 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
|
||||
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"),
|
||||
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,
|
||||
isSilent
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
@ -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 { rm_rf, rm_r } from "./tools/rm";
|
||||
import { getCliOptions } from "./tools/cliOptions";
|
||||
import { getLogger } from "./tools/logger";
|
||||
|
||||
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||
// update the version array for generating for newer version.
|
||||
@ -12,16 +13,20 @@ import { rm_rf, rm_r } from "./tools/rm";
|
||||
//@ts-ignore
|
||||
const propertiesParser = require("properties-parser");
|
||||
|
||||
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||
const logger = getLogger({ isSilent });
|
||||
|
||||
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
||||
console.log({ keycloakVersion });
|
||||
logger.log(JSON.stringify({ keycloakVersion }));
|
||||
|
||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||
|
||||
rm_rf(tmpDirPath);
|
||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
|
||||
downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
type Dictionary = { [idiomId: string]: string };
|
||||
@ -42,13 +47,13 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
||||
([key, value]: any) => [key, value.replace(/''/g, "'")],
|
||||
),
|
||||
([key, value]: any) => [key, value.replace(/''/g, "'")]
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
rm_r(tmpDirPath);
|
||||
fs.rmSync(tmpDirPath, { recursive: true, force: true });
|
||||
|
||||
Object.keys(record).forEach(pageType => {
|
||||
const recordForPageType = record[pageType];
|
||||
@ -69,13 +74,13 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
||||
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||
"",
|
||||
"export default messages;",
|
||||
"/* spell-checker: enable */",
|
||||
"/* spell-checker: enable */"
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
console.log(`${filePath} wrote`);
|
||||
logger.log(`${filePath} wrote`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
180
src/bin/keycloakify/BuildOptions.ts
Normal file
180
src/bin/keycloakify/BuildOptions.ts
Normal file
@ -0,0 +1,180 @@
|
||||
import { z } from "zod";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import { id } from "tsafe/id";
|
||||
import { parse as urlParse } from "url";
|
||||
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
homepage?: string;
|
||||
keycloakify?: {
|
||||
extraPages?: string[];
|
||||
extraThemeProperties?: string[];
|
||||
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
const zParsedPackageJson = z.object({
|
||||
"name": z.string(),
|
||||
"version": z.string(),
|
||||
"homepage": z.string().optional(),
|
||||
"keycloakify": z
|
||||
.object({
|
||||
"extraPages": z.array(z.string()).optional(),
|
||||
"extraThemeProperties": z.array(z.string()).optional(),
|
||||
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
|
||||
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||
|
||||
export namespace BuildOptions {
|
||||
export type Common = {
|
||||
isSilent: boolean;
|
||||
version: string;
|
||||
themeName: string;
|
||||
extraPages?: string[];
|
||||
extraThemeProperties?: string[];
|
||||
//NOTE: Only for the pom.xml file, questionable utility...
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||
|
||||
export namespace ExternalAssets {
|
||||
export type CommonExternalAssets = Common & {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function readBuildOptions(params: {
|
||||
packageJson: string;
|
||||
CNAME: string | undefined;
|
||||
isExternalAssetsCliParamProvided: boolean;
|
||||
isSilent: boolean;
|
||||
}): BuildOptions {
|
||||
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
|
||||
|
||||
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
||||
|
||||
const url = (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
let url: URL | undefined = undefined;
|
||||
|
||||
if (homepage !== undefined) {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
if (CNAME !== undefined) {
|
||||
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||
}
|
||||
|
||||
if (url === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
"origin": url.origin,
|
||||
"pathname": (() => {
|
||||
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||
|
||||
return out === "/" ? undefined : out;
|
||||
})()
|
||||
};
|
||||
})();
|
||||
|
||||
const common: BuildOptions.Common = (() => {
|
||||
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||
|
||||
const { extraPages, extraThemeProperties } = keycloakify ?? {};
|
||||
|
||||
const themeName = name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
|
||||
return {
|
||||
themeName,
|
||||
"groupId": (() => {
|
||||
const fallbackGroupId = `${themeName}.keycloak`;
|
||||
|
||||
return (
|
||||
(!homepage
|
||||
? fallbackGroupId
|
||||
: urlParse(homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"version": version,
|
||||
extraPages,
|
||||
extraThemeProperties,
|
||||
isSilent
|
||||
};
|
||||
})();
|
||||
|
||||
if (isExternalAssetsCliParamProvided) {
|
||||
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
|
||||
...common,
|
||||
"isStandalone": false
|
||||
});
|
||||
|
||||
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
|
||||
return id<BuildOptions.ExternalAssets.SameDomain>({
|
||||
...commonExternalAssets,
|
||||
"areAppAndKeycloakServerSharingSameDomain": true
|
||||
});
|
||||
} else {
|
||||
assert(
|
||||
url !== undefined,
|
||||
[
|
||||
"Can't compile in external assets mode if we don't know where",
|
||||
"the app will be hosted.",
|
||||
"You should provide a homepage field in the package.json (or create a",
|
||||
"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": { "areAppAndKeycloakServerSharingSameDomain": true }',
|
||||
"in your package.json"
|
||||
].join(" ")
|
||||
);
|
||||
|
||||
return id<BuildOptions.ExternalAssets.DifferentDomains>({
|
||||
...commonExternalAssets,
|
||||
"areAppAndKeycloakServerSharingSameDomain": false,
|
||||
"urlOrigin": url.origin,
|
||||
"urlPathname": url.pathname
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return id<BuildOptions.Standalone>({
|
||||
...common,
|
||||
"isStandalone": true,
|
||||
"urlPathname": url?.pathname
|
||||
});
|
||||
}
|
@ -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') || messagesPerField.exists('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>
|
190
src/bin/keycloakify/generateFtl/generateFtl.ts
Normal file
190
src/bin/keycloakify/generateFtl/generateFtl.ts
Normal file
@ -0,0 +1,190 @@
|
||||
import cheerio from "cheerio";
|
||||
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
|
||||
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
||||
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { objectKeys } from "tsafe/objectKeys";
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
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",
|
||||
"error.ftl",
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-idp-link-email.ftl",
|
||||
"login-page-expired.ftl",
|
||||
"login-config-totp.ftl",
|
||||
"logout-confirm.ftl",
|
||||
"update-user-profile.ftl",
|
||||
"idp-review-user-profile.ftl"
|
||||
] as const;
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Standalone = {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||
|
||||
export namespace ExternalAssets {
|
||||
export type CommonExternalAssets = {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export type PageId = typeof pageIds[number];
|
||||
|
||||
export function generateFtlFilesCodeFactory(params: {
|
||||
indexHtmlCode: string;
|
||||
//NOTE: Expected to be an empty object if external assets mode is enabled.
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
fix_imports_statements: {
|
||||
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||
break fix_imports_statements;
|
||||
}
|
||||
|
||||
$("script:not([src])").each((...[, element]) => {
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": $(element).html()!,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
$(element).text(fixedJsCode);
|
||||
});
|
||||
|
||||
$("style").each((...[, element]) => {
|
||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||
"cssCode": $(element).html()!,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
$(element).text(fixedCssCode);
|
||||
});
|
||||
|
||||
(
|
||||
[
|
||||
["link", "href"],
|
||||
["script", "src"]
|
||||
] as const
|
||||
).forEach(([selector, attrName]) =>
|
||||
$(selector).each((...[, element]) => {
|
||||
const href = $(element).attr(attrName);
|
||||
|
||||
if (href === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(element).attr(
|
||||
attrName,
|
||||
buildOptions.isStandalone
|
||||
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
if (Object.keys(cssGlobalsToDefine).length !== 0) {
|
||||
$("head").prepend(
|
||||
[
|
||||
"",
|
||||
"<style>",
|
||||
generateCssCodeToDefineGlobals({
|
||||
cssGlobalsToDefine,
|
||||
buildOptions
|
||||
}).cssCodeToPrependInHead,
|
||||
"</style>",
|
||||
""
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||
const replaceValueBySearchValue = {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||
"<#if scripts??>",
|
||||
" <#list scripts as script>",
|
||||
' <script src="${script}" type="text/javascript"></script>',
|
||||
" </#list>",
|
||||
"</#if>"
|
||||
].join("\n")
|
||||
};
|
||||
|
||||
$("head").prepend(
|
||||
[
|
||||
"<script>",
|
||||
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||
"</script>",
|
||||
"",
|
||||
objectKeys(replaceValueBySearchValue)[1]
|
||||
].join("\n")
|
||||
);
|
||||
|
||||
const partiallyFixedIndexHtmlCode = $.html();
|
||||
|
||||
function generateFtlFilesCode(params: { pageId: string }): {
|
||||
ftlCode: string;
|
||||
} {
|
||||
const { pageId } = params;
|
||||
|
||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||
|
||||
let ftlCode = $.html();
|
||||
|
||||
Object.entries({
|
||||
...replaceValueBySearchValue,
|
||||
//If updated, don't forget to change in the ftl script as well.
|
||||
"PAGE_ID_xIgLsPgGId9D8e": pageId
|
||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||
|
||||
return { ftlCode };
|
||||
}
|
||||
|
||||
return { generateFtlFilesCode };
|
||||
}
|
@ -1,37 +1,39 @@
|
||||
import * as url from "url";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { BuildOptions } from "./BuildOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
themeName: string;
|
||||
groupId: string;
|
||||
};
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function generateJavaStackFiles(params: {
|
||||
version: string;
|
||||
themeName: string;
|
||||
homepage?: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
doBundleEmailTemplate: boolean;
|
||||
doBundlesEmailTemplate: boolean;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): {
|
||||
jarFilePath: string;
|
||||
} {
|
||||
const { themeName, version, homepage, keycloakThemeBuildingDirPath, doBundleEmailTemplate } = params;
|
||||
const {
|
||||
version,
|
||||
buildOptions: { groupId, themeName },
|
||||
keycloakThemeBuildingDirPath,
|
||||
doBundlesEmailTemplate
|
||||
} = params;
|
||||
|
||||
{
|
||||
const { pomFileCode } = (function generatePomFileCode(): {
|
||||
pomFileCode: string;
|
||||
} {
|
||||
const groupId = (() => {
|
||||
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
|
||||
|
||||
return (
|
||||
(!homepage
|
||||
? fallbackGroupId
|
||||
: url
|
||||
.parse(homepage)
|
||||
.host?.replace(/:[0-9]+$/, "")
|
||||
?.split(".")
|
||||
.reverse()
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})();
|
||||
|
||||
const artefactId = `${themeName}-keycloak-theme`;
|
||||
|
||||
const pomFileCode = [
|
||||
@ -45,7 +47,7 @@ export function generateJavaStackFiles(params: {
|
||||
` <version>${version}</version>`,
|
||||
` <name>${artefactId}</name>`,
|
||||
` <description />`,
|
||||
`</project>`,
|
||||
`</project>`
|
||||
].join("\n");
|
||||
|
||||
return { pomFileCode };
|
||||
@ -69,19 +71,19 @@ export function generateJavaStackFiles(params: {
|
||||
"themes": [
|
||||
{
|
||||
"name": themeName,
|
||||
"types": ["login", ...(doBundleEmailTemplate ? ["email"] : [])],
|
||||
},
|
||||
],
|
||||
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])]
|
||||
}
|
||||
]
|
||||
},
|
||||
null,
|
||||
2,
|
||||
2
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`),
|
||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`)
|
||||
};
|
||||
}
|
@ -1,116 +1,150 @@
|
||||
import { transformCodebase } from "../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, basename as pathBasename } from "path";
|
||||
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
|
||||
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
|
||||
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
|
||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
||||
import * as child_process from "child_process";
|
||||
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
|
||||
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;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
themeName: string;
|
||||
extraPages?: string[];
|
||||
extraThemeProperties?: string[];
|
||||
isSilent: boolean;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||
|
||||
export namespace ExternalAssets {
|
||||
export type CommonExternalAssets = Common & {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function generateKeycloakThemeResources(params: {
|
||||
themeName: string;
|
||||
reactAppBuildDirPath: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
keycloakThemeEmailDirPath: string;
|
||||
urlPathname: string;
|
||||
//If urlOrigin is not undefined then it means --externals-assets
|
||||
urlOrigin: undefined | string;
|
||||
extraPagesId: string[];
|
||||
extraThemeProperties: string[];
|
||||
keycloakVersion: string;
|
||||
}): { doBundleEmailTemplate: boolean } {
|
||||
const {
|
||||
themeName,
|
||||
reactAppBuildDirPath,
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakThemeEmailDirPath,
|
||||
urlPathname,
|
||||
urlOrigin,
|
||||
extraPagesId,
|
||||
extraThemeProperties,
|
||||
keycloakVersion,
|
||||
} = params;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}): { doBundlesEmailTemplate: boolean } {
|
||||
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
||||
|
||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
||||
const logger = getLogger({ isSilent: buildOptions.isSilent });
|
||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
|
||||
|
||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
transformCodebase({
|
||||
"destDirPath": urlOrigin === undefined ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||
"srcDirPath": reactAppBuildDirPath,
|
||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||
if (
|
||||
urlOrigin === undefined &&
|
||||
buildOptions.isStandalone &&
|
||||
isInside({
|
||||
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
|
||||
filePath,
|
||||
filePath
|
||||
})
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
|
||||
if (/\.css?$/i.test(filePath)) {
|
||||
if (!buildOptions.isStandalone) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||
"cssCode": sourceCode.toString("utf8"),
|
||||
"cssCode": sourceCode.toString("utf8")
|
||||
});
|
||||
|
||||
allCssGlobalsToDefine = {
|
||||
...allCssGlobalsToDefine,
|
||||
...cssGlobalsToDefine,
|
||||
...cssGlobalsToDefine
|
||||
};
|
||||
|
||||
return {
|
||||
"modifiedSourceCode": Buffer.from(fixedCssCode, "utf8"),
|
||||
};
|
||||
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||
}
|
||||
|
||||
if (/\.js?$/i.test(filePath)) {
|
||||
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||
"jsCode": sourceCode.toString("utf8"),
|
||||
urlOrigin,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
return {
|
||||
"modifiedSourceCode": Buffer.from(fixedJsCode, "utf8"),
|
||||
};
|
||||
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||
}
|
||||
|
||||
return urlOrigin === undefined ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||
},
|
||||
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||
}
|
||||
});
|
||||
|
||||
let doBundleEmailTemplate: boolean;
|
||||
let doBundlesEmailTemplate: boolean;
|
||||
|
||||
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 👈`,
|
||||
].join("\n"),
|
||||
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
|
||||
].join("\n")
|
||||
);
|
||||
doBundleEmailTemplate = false;
|
||||
doBundlesEmailTemplate = false;
|
||||
break email;
|
||||
}
|
||||
|
||||
doBundleEmailTemplate = true;
|
||||
doBundlesEmailTemplate = true;
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": keycloakThemeEmailDirPath,
|
||||
"destDirPath": pathJoin(themeDirPath, "..", "email"),
|
||||
"destDirPath": pathJoin(themeDirPath, "..", "email")
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||
urlPathname,
|
||||
urlOrigin,
|
||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||
"buildOptions": buildOptions
|
||||
});
|
||||
|
||||
[...pageIds, ...extraPagesId].forEach(pageId => {
|
||||
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
|
||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||
|
||||
fs.mkdirSync(themeDirPath, { "recursive": true });
|
||||
@ -124,25 +158,26 @@ export function generateKeycloakThemeResources(params: {
|
||||
downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
isSilent: buildOptions.isSilent
|
||||
});
|
||||
|
||||
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
|
||||
"destDirPath": themeResourcesDirPath,
|
||||
"destDirPath": themeResourcesDirPath
|
||||
});
|
||||
|
||||
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath)),
|
||||
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": themeResourcesDirPath,
|
||||
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath),
|
||||
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
|
||||
});
|
||||
|
||||
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
|
||||
@ -150,8 +185,8 @@ export function generateKeycloakThemeResources(params: {
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
|
||||
Buffer.from(
|
||||
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" "),
|
||||
),
|
||||
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" ")
|
||||
)
|
||||
);
|
||||
|
||||
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
@ -161,8 +196,8 @@ export function generateKeycloakThemeResources(params: {
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeDirPath, "theme.properties"),
|
||||
Buffer.from("parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
|
||||
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
|
||||
);
|
||||
|
||||
return { doBundleEmailTemplate };
|
||||
return { doBundlesEmailTemplate };
|
||||
}
|
@ -1,19 +1,40 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
import type { BuildOptions } from "./BuildOptions";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
themeName: string;
|
||||
};
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||
|
||||
const containerName = "keycloak-testing-container";
|
||||
|
||||
/** Files for being able to run a hot reload keycloak container */
|
||||
export function generateStartKeycloakTestingContainer(params: { keycloakVersion: string; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
||||
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
|
||||
export function generateStartKeycloakTestingContainer(params: {
|
||||
keycloakVersion: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
buildOptions: BuildOptionsLike;
|
||||
}) {
|
||||
const {
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakVersion,
|
||||
buildOptions: { themeName }
|
||||
} = params;
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
||||
Buffer.from(
|
||||
[
|
||||
"#!/bin/bash",
|
||||
"#!/usr/bin/env bash",
|
||||
"",
|
||||
`docker rm ${containerName} || true`,
|
||||
"",
|
||||
@ -31,14 +52,14 @@ export function generateStartKeycloakTestingContainer(params: { keycloakVersion:
|
||||
"main",
|
||||
"resources",
|
||||
"theme",
|
||||
themeName,
|
||||
themeName
|
||||
)}:/opt/keycloak/themes/${themeName}:rw \\`,
|
||||
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
||||
` start-dev`,
|
||||
"",
|
||||
""
|
||||
].join("\n"),
|
||||
"utf8",
|
||||
"utf8"
|
||||
),
|
||||
{ "mode": 0o755 },
|
||||
{ "mode": 0o755 }
|
||||
);
|
||||
}
|
8
src/bin/keycloakify/index.ts
Normal file
8
src/bin/keycloakify/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
export * from "./keycloakify";
|
||||
import { main } from "./keycloakify";
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
@ -3,104 +3,68 @@ import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||
import * as child_process from "child_process";
|
||||
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||
import { URL } from "url";
|
||||
import * as fs from "fs";
|
||||
|
||||
type ParsedPackageJson = {
|
||||
name: string;
|
||||
version: string;
|
||||
homepage?: string;
|
||||
};
|
||||
import { readBuildOptions } from "./BuildOptions";
|
||||
import { getLogger } from "../tools/logger";
|
||||
import { getCliOptions } from "../tools/cliOptions";
|
||||
|
||||
const reactProjectDirPath = process.cwd();
|
||||
|
||||
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
||||
|
||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
||||
|
||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
||||
|
||||
function sanitizeThemeName(name: string) {
|
||||
return name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
}
|
||||
|
||||
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 extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
|
||||
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
|
||||
const themeName = sanitizeThemeName(parsedPackageJson.name);
|
||||
const buildOptions = readBuildOptions({
|
||||
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
|
||||
"CNAME": (() => {
|
||||
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
||||
|
||||
const { doBundleEmailTemplate } = generateKeycloakThemeResources({
|
||||
if (!fs.existsSync(cnameFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||
})(),
|
||||
"isExternalAssetsCliParamProvided": hasExternalAssets,
|
||||
"isSilent": isSilent
|
||||
});
|
||||
|
||||
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakThemeEmailDirPath,
|
||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||
themeName,
|
||||
...(() => {
|
||||
const url = (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
|
||||
if (homepage !== undefined) {
|
||||
return new URL(homepage);
|
||||
}
|
||||
|
||||
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
||||
|
||||
if (fs.existsSync(cnameFilePath)) {
|
||||
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})();
|
||||
|
||||
return {
|
||||
"urlPathname": url === undefined ? "/" : url.pathname.replace(/([^/])$/, "$1/"),
|
||||
"urlOrigin": !doUseExternalAssets
|
||||
? undefined
|
||||
: (() => {
|
||||
if (url === undefined) {
|
||||
console.error("ERROR: You must specify 'homepage' in your package.json");
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
return url.origin;
|
||||
})(),
|
||||
};
|
||||
})(),
|
||||
extraPagesId,
|
||||
extraThemeProperties,
|
||||
buildOptions,
|
||||
//We have to leave it at that otherwise we break our default theme.
|
||||
//Problem is that we can't guarantee that the the old resources
|
||||
//will still be available on the newer keycloak version.
|
||||
"keycloakVersion": "11.0.3",
|
||||
"keycloakVersion": "11.0.3"
|
||||
});
|
||||
|
||||
const { jarFilePath } = generateJavaStackFiles({
|
||||
"version": parsedPackageJson.version,
|
||||
themeName,
|
||||
"homepage": parsedPackageJson.homepage,
|
||||
"version": buildOptions.version,
|
||||
keycloakThemeBuildingDirPath,
|
||||
doBundleEmailTemplate,
|
||||
doBundlesEmailTemplate,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
child_process.execSync("mvn package", {
|
||||
"cwd": keycloakThemeBuildingDirPath,
|
||||
"cwd": keycloakThemeBuildingDirPath
|
||||
});
|
||||
|
||||
//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,
|
||||
themeName,
|
||||
"keycloakVersion": containerKeycloakVersion,
|
||||
buildOptions
|
||||
});
|
||||
|
||||
console.log(
|
||||
logger.log(
|
||||
[
|
||||
"",
|
||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||
@ -145,11 +109,11 @@ export function main() {
|
||||
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||
'- Create a realm named "myrealm"',
|
||||
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
||||
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
|
||||
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
|
||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||
"",
|
||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
||||
"",
|
||||
].join("\n"),
|
||||
""
|
||||
].join("\n")
|
||||
);
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Standalone = {
|
||||
isStandalone: true;
|
||||
};
|
||||
|
||||
export type ExternalAssets = {
|
||||
isStandalone: false;
|
||||
urlOrigin: string;
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
|
||||
/*
|
||||
NOTE:
|
||||
|
||||
When we have urlOrigin defined it means that
|
||||
we are building with --external-assets
|
||||
so we have to make sur that the fixed js code will run
|
||||
inside and outside keycloak.
|
||||
|
||||
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||
will always run in keycloak context.
|
||||
*/
|
||||
|
||||
const { jsCode, buildOptions } = params;
|
||||
|
||||
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||
(...[, n, u, e]) => `
|
||||
${n}[(function(){
|
||||
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||
if( pd === undefined || pd.configurable ){
|
||||
${
|
||||
buildOptions.isStandalone
|
||||
? `
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||
set: function (){}
|
||||
});
|
||||
`
|
||||
: `
|
||||
var p= "";
|
||||
Object.defineProperty(${n}, "p", {
|
||||
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
|
||||
set: function (value){ p = value;}
|
||||
});
|
||||
`
|
||||
}
|
||||
}
|
||||
return "${u}";
|
||||
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
|
||||
];
|
||||
|
||||
const fixedJsCode = jsCode
|
||||
.replace(...getReplaceArgs("js"))
|
||||
.replace(...getReplaceArgs("css"))
|
||||
.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/`
|
||||
)
|
||||
//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},`
|
||||
);
|
||||
|
||||
return { fixedJsCode };
|
||||
}
|
64
src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
Normal file
64
src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import * as crypto from "crypto";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export type BuildOptionsLike = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||
fixedCssCode: string;
|
||||
cssGlobalsToDefine: Record<string, string>;
|
||||
} {
|
||||
const { cssCode } = params;
|
||||
|
||||
const cssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
|
||||
);
|
||||
|
||||
let fixedCssCode = cssCode;
|
||||
|
||||
Object.keys(cssGlobalsToDefine).forEach(
|
||||
cssVariableName =>
|
||||
//NOTE: split/join pattern ~ replace all
|
||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
|
||||
);
|
||||
|
||||
return { fixedCssCode, cssGlobalsToDefine };
|
||||
}
|
||||
|
||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
|
||||
cssCodeToPrependInHead: string;
|
||||
} {
|
||||
const { cssGlobalsToDefine, buildOptions } = params;
|
||||
|
||||
return {
|
||||
"cssCodeToPrependInHead": [
|
||||
":root {",
|
||||
...Object.keys(cssGlobalsToDefine)
|
||||
.map(cssVariableName =>
|
||||
[
|
||||
`--${cssVariableName}:`,
|
||||
cssGlobalsToDefine[cssVariableName].replace(
|
||||
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||
"url(${url.resourcesPath}/build/"
|
||||
)
|
||||
].join(" ")
|
||||
)
|
||||
.map(line => ` ${line};`),
|
||||
"}"
|
||||
].join("\n")
|
||||
};
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { is } from "tsafe/is";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
};
|
||||
|
||||
export type ExternalAssets = Common & {
|
||||
isStandalone: false;
|
||||
urlOrigin: string;
|
||||
};
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||
fixedCssCode: string;
|
||||
} {
|
||||
const { cssCode, buildOptions } = params;
|
||||
|
||||
const fixedCssCode = cssCode.replace(
|
||||
buildOptions.urlPathname === undefined
|
||||
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||
(...[, group]) =>
|
||||
`url(${
|
||||
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||
})`
|
||||
);
|
||||
|
||||
return { fixedCssCode };
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { execSync } from "child_process";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { exclude } from "tsafe/exclude";
|
||||
import * as fs from "fs";
|
||||
|
||||
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
|
||||
@ -14,14 +15,14 @@ fs.writeFileSync(
|
||||
return {
|
||||
...packageJsonParsed,
|
||||
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
|
||||
"types": packageJsonParsed["types"].replace(/^dist\//, ""),
|
||||
"types": packageJsonParsed["types"].replace(/^dist\//, "")
|
||||
};
|
||||
})(),
|
||||
null,
|
||||
2,
|
||||
2
|
||||
),
|
||||
"utf8",
|
||||
),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
|
||||
const commonThirdPartyDeps = (() => {
|
||||
@ -33,16 +34,17 @@ const commonThirdPartyDeps = (() => {
|
||||
.map(namespaceModuleName =>
|
||||
fs
|
||||
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`),
|
||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||
)
|
||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||
...standaloneModuleNames,
|
||||
...standaloneModuleNames
|
||||
];
|
||||
})();
|
||||
|
||||
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
|
||||
|
||||
execSync(["rm -rf", "mkdir"].map(cmd => `${cmd} ${yarnHomeDirPath}`).join(" && "));
|
||||
fs.rmSync(yarnHomeDirPath, { "recursive": true, "force": true });
|
||||
fs.mkdirSync(yarnHomeDirPath);
|
||||
|
||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
const { targetModuleName, cwd } = params;
|
||||
@ -55,16 +57,37 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||
cwd,
|
||||
"env": {
|
||||
...process.env,
|
||||
"HOME": yarnHomeDirPath,
|
||||
},
|
||||
"HOME": yarnHomeDirPath
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 ===");
|
||||
|
||||
@ -77,26 +100,28 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||
|
||||
const localInstallPath = pathJoin(
|
||||
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])],
|
||||
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
||||
);
|
||||
|
||||
execYarnLink({ "cwd": localInstallPath });
|
||||
|
||||
testAppNames.forEach(testAppName =>
|
||||
execYarnLink({
|
||||
"cwd": getTestAppPath(testAppName),
|
||||
"targetModuleName": commonThirdPartyDep,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": commonThirdPartyDep
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
console.log("=== Linking in house dependencies ===");
|
||||
|
||||
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
|
||||
|
||||
testAppNames.forEach(testAppName =>
|
||||
testAppPaths.forEach(testAppPath =>
|
||||
execYarnLink({
|
||||
"cwd": getTestAppPath(testAppName),
|
||||
"targetModuleName": "keycloakify",
|
||||
}),
|
||||
"cwd": testAppPath,
|
||||
"targetModuleName": "keycloakify"
|
||||
})
|
||||
);
|
||||
|
@ -24,9 +24,9 @@ export async function promptKeycloakVersion() {
|
||||
"count": 10,
|
||||
"doIgnoreBeta": true,
|
||||
"owner": "keycloak",
|
||||
"repo": "keycloak",
|
||||
"repo": "keycloak"
|
||||
}).then(arr => arr.map(({ tag }) => tag))),
|
||||
"11.0.3",
|
||||
"11.0.3"
|
||||
];
|
||||
|
||||
if (process.env["GITHUB_ACTIONS"] === "true") {
|
||||
@ -34,7 +34,7 @@ export async function promptKeycloakVersion() {
|
||||
}
|
||||
|
||||
const { value: keycloakVersion } = await cliSelect<string>({
|
||||
"values": tags,
|
||||
"values": tags
|
||||
}).catch(() => {
|
||||
console.log("Aborting");
|
||||
|
||||
|
@ -20,7 +20,7 @@ export namespace NpmModuleVersion {
|
||||
...(() => {
|
||||
const str = match[4];
|
||||
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
|
||||
})(),
|
||||
})()
|
||||
};
|
||||
}
|
||||
|
||||
|
15
src/bin/tools/cliOptions.ts
Normal file
15
src/bin/tools/cliOptions.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import parseArgv from "minimist";
|
||||
|
||||
export type CliOptions = {
|
||||
isSilent: boolean;
|
||||
hasExternalAssets: boolean;
|
||||
};
|
||||
|
||||
export const getCliOptions = (processArgv: string[]): CliOptions => {
|
||||
const argv = parseArgv(processArgv);
|
||||
|
||||
return {
|
||||
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||
};
|
||||
};
|
@ -2,31 +2,79 @@ import { basename as pathBasename, join as pathJoin } from "path";
|
||||
import { execSync } from "child_process";
|
||||
import * as fs from "fs";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
import { rm_rf, rm, rm_r } from "./rm";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
/** assert url ends with .zip */
|
||||
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||
export function downloadAndUnzip(params: {
|
||||
isSilent: boolean;
|
||||
url: string;
|
||||
destDirPath: string;
|
||||
pathOfDirToExtractInArchive?: string;
|
||||
cacheDirPath: string;
|
||||
}) {
|
||||
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
|
||||
const zipFilePath = pathBasename(url);
|
||||
const extractDirPath = pathJoin(
|
||||
cacheDirPath,
|
||||
`_${crypto.createHash("sha256").update(JSON.stringify({ url, pathOfDirToExtractInArchive })).digest("hex").substring(0, 15)}`
|
||||
);
|
||||
|
||||
rm_rf(tmpDirPath);
|
||||
fs.mkdirSync(cacheDirPath, { "recursive": true });
|
||||
|
||||
fs.mkdirSync(tmpDirPath, { "recursive": true });
|
||||
const { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath } = (() => {
|
||||
const filePath = pathJoin(cacheDirPath, "isSuccessByExtractDirPath.json");
|
||||
|
||||
execSync(`curl -L ${url} -o ${zipFilePath}`, { "cwd": tmpDirPath });
|
||||
type IsSuccessByExtractDirPath = Record<string, boolean | undefined>;
|
||||
|
||||
execSync(`unzip -o ${zipFilePath}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
|
||||
"cwd": tmpDirPath,
|
||||
});
|
||||
function readIsSuccessByExtractDirPath(): IsSuccessByExtractDirPath {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
rm(pathBasename(url), { "cwd": tmpDirPath });
|
||||
return JSON.parse(fs.readFileSync(filePath).toString("utf8"));
|
||||
}
|
||||
|
||||
function writeIsSuccessByExtractDirPath(isSuccessByExtractDirPath: IsSuccessByExtractDirPath): void {
|
||||
fs.writeFileSync(filePath, Buffer.from(JSON.stringify(isSuccessByExtractDirPath, null, 2), "utf8"));
|
||||
}
|
||||
|
||||
return { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath };
|
||||
})();
|
||||
|
||||
downloadAndUnzip: {
|
||||
const isSuccessByExtractDirPath = readIsSuccessByExtractDirPath();
|
||||
|
||||
if (isSuccessByExtractDirPath[extractDirPath]) {
|
||||
break downloadAndUnzip;
|
||||
}
|
||||
|
||||
writeIsSuccessByExtractDirPath({
|
||||
...isSuccessByExtractDirPath,
|
||||
[extractDirPath]: false
|
||||
});
|
||||
|
||||
fs.rmSync(extractDirPath, { "recursive": true, "force": true });
|
||||
|
||||
fs.mkdirSync(extractDirPath);
|
||||
|
||||
const zipFileBasename = pathBasename(url);
|
||||
|
||||
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
|
||||
|
||||
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
|
||||
"cwd": extractDirPath
|
||||
});
|
||||
|
||||
fs.rmSync(pathJoin(extractDirPath, zipFileBasename), { "recursive": true, "force": true });
|
||||
|
||||
writeIsSuccessByExtractDirPath({
|
||||
...isSuccessByExtractDirPath,
|
||||
[extractDirPath]: true
|
||||
});
|
||||
}
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathOfDirToExtractInArchive === undefined ? tmpDirPath : pathJoin(tmpDirPath, pathOfDirToExtractInArchive),
|
||||
destDirPath,
|
||||
"srcDirPath": pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive),
|
||||
destDirPath
|
||||
});
|
||||
|
||||
rm_r(tmpDirPath);
|
||||
}
|
||||
|
@ -5,6 +5,6 @@ import * as fs from "fs";
|
||||
|
||||
Object.entries<string>(JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["bin"]).forEach(([, scriptPath]) =>
|
||||
child_process.execSync(`chmod +x ${scriptPath}`, {
|
||||
"cwd": getProjectRoot(),
|
||||
}),
|
||||
"cwd": getProjectRoot()
|
||||
})
|
||||
);
|
||||
|
27
src/bin/tools/logger.ts
Normal file
27
src/bin/tools/logger.ts
Normal file
@ -0,0 +1,27 @@
|
||||
type LoggerOpts = {
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
type Logger = {
|
||||
log: (message: string, opts?: LoggerOpts) => void;
|
||||
warn: (message: string) => void;
|
||||
error: (message: string) => void;
|
||||
};
|
||||
|
||||
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
|
||||
return {
|
||||
log: (message, { force } = {}) => {
|
||||
if (isSilent && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(message);
|
||||
},
|
||||
warn: message => {
|
||||
console.warn(message);
|
||||
},
|
||||
error: message => {
|
||||
console.error(message);
|
||||
}
|
||||
};
|
||||
};
|
@ -19,7 +19,7 @@ export function listTagsFactory(params: { octokit: Octokit }) {
|
||||
owner,
|
||||
repo,
|
||||
per_page,
|
||||
"page": page++,
|
||||
"page": page++
|
||||
});
|
||||
|
||||
for (const branch of resp.data.map(({ name }) => name)) {
|
||||
|
@ -1,31 +0,0 @@
|
||||
import { execSync } from "child_process";
|
||||
|
||||
function rmInternal(params: { pathToRemove: string; args: string | undefined; cwd: string | undefined }) {
|
||||
const { pathToRemove, args, cwd } = params;
|
||||
|
||||
execSync(`rm ${args ? `-${args} ` : ""}${pathToRemove.replace(/ /g, "\\ ")}`, cwd !== undefined ? { cwd } : undefined);
|
||||
}
|
||||
|
||||
export function rm(pathToRemove: string, options?: { cwd: string }) {
|
||||
rmInternal({
|
||||
pathToRemove,
|
||||
"args": undefined,
|
||||
"cwd": options?.cwd,
|
||||
});
|
||||
}
|
||||
|
||||
export function rm_r(pathToRemove: string, options?: { cwd: string }) {
|
||||
rmInternal({
|
||||
pathToRemove,
|
||||
"args": "r",
|
||||
"cwd": options?.cwd,
|
||||
});
|
||||
}
|
||||
|
||||
export function rm_rf(pathToRemove: string, options?: { cwd: string }) {
|
||||
rmInternal({
|
||||
pathToRemove,
|
||||
"args": "rf",
|
||||
"cwd": options?.cwd,
|
||||
});
|
||||
}
|
@ -16,8 +16,8 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
||||
srcDirPath,
|
||||
destDirPath,
|
||||
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
|
||||
"modifiedSourceCode": sourceCode,
|
||||
})),
|
||||
"modifiedSourceCode": sourceCode
|
||||
}))
|
||||
} = params;
|
||||
|
||||
for (const file_relative_path of crawl(srcDirPath)) {
|
||||
@ -25,7 +25,7 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
||||
|
||||
const transformSourceCodeResult = transformSourceCode({
|
||||
"sourceCode": fs.readFileSync(filePath),
|
||||
"filePath": path.join(srcDirPath, file_relative_path),
|
||||
"filePath": path.join(srcDirPath, file_relative_path)
|
||||
});
|
||||
|
||||
if (transformSourceCodeResult === undefined) {
|
||||
@ -33,14 +33,14 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
|
||||
"recursive": true,
|
||||
"recursive": true
|
||||
});
|
||||
|
||||
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
|
||||
modifiedSourceCode,
|
||||
modifiedSourceCode
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("errorTitle")}
|
||||
formNode={
|
||||
|
58
src/lib/components/IdpReviewUserProfile.tsx
Normal file
58
src/lib/components/IdpReviewUserProfile.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||
|
||||
export type IdpReviewUserProfileProps = KcProps & {
|
||||
kcContext: KcContextBase.IdpReviewUserProfile;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => 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 (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("loginIdpReviewProfileTitle")}
|
||||
formNode={
|
||||
<form id="kc-idp-review-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
disabled={!isFomSubmittable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default IdpReviewUserProfile;
|
@ -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 (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
||||
formNode={
|
||||
|
@ -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,13 +25,24 @@ 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, ...props }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
|
||||
const i18n = (function useClosure() {
|
||||
const i18n = useI18n({
|
||||
kcContext,
|
||||
"extraMessages": {},
|
||||
"doSkip": userProvidedI18n !== undefined,
|
||||
"doSkip": userProvidedI18n !== undefined
|
||||
});
|
||||
|
||||
return userProvidedI18n ?? i18n;
|
||||
@ -36,42 +52,54 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps = { i18n, Template, ...kcProps };
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl":
|
||||
return <Login {...{ kcContext, i18n, ...props }} />;
|
||||
return <Login {...{ kcContext, ...commonProps }} />;
|
||||
case "register.ftl":
|
||||
return <Register {...{ kcContext, i18n, ...props }} />;
|
||||
return <Register {...{ kcContext, ...commonProps }} />;
|
||||
case "register-user-profile.ftl":
|
||||
return <RegisterUserProfile {...{ kcContext, i18n, ...props }} />;
|
||||
return <RegisterUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "info.ftl":
|
||||
return <Info {...{ kcContext, i18n, ...props }} />;
|
||||
return <Info {...{ kcContext, ...commonProps }} />;
|
||||
case "error.ftl":
|
||||
return <Error {...{ kcContext, i18n, ...props }} />;
|
||||
return <Error {...{ kcContext, ...commonProps }} />;
|
||||
case "login-reset-password.ftl":
|
||||
return <LoginResetPassword {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginResetPassword {...{ kcContext, ...commonProps }} />;
|
||||
case "login-verify-email.ftl":
|
||||
return <LoginVerifyEmail {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginVerifyEmail {...{ kcContext, ...commonProps }} />;
|
||||
case "terms.ftl":
|
||||
return <Terms {...{ kcContext, i18n, ...props }} />;
|
||||
return <Terms {...{ kcContext, ...commonProps }} />;
|
||||
case "login-otp.ftl":
|
||||
return <LoginOtp {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginOtp {...{ kcContext, ...commonProps }} />;
|
||||
case "login-username.ftl":
|
||||
return <LoginUsername {...{ kcContext, ...commonProps }} />;
|
||||
case "login-password.ftl":
|
||||
return <LoginPassword {...{ kcContext, ...commonProps }} />;
|
||||
case "webauthn-authenticate.ftl":
|
||||
return <WebauthnAuthenticate {...{ kcContext, ...commonProps }} />;
|
||||
case "login-update-password.ftl":
|
||||
return <LoginUpdatePassword {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginUpdatePassword {...{ kcContext, ...commonProps }} />;
|
||||
case "login-update-profile.ftl":
|
||||
return <LoginUpdateProfile {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginUpdateProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "login-idp-link-confirm.ftl":
|
||||
return <LoginIdpLinkConfirm {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginIdpLinkConfirm {...{ kcContext, ...commonProps }} />;
|
||||
case "login-idp-link-email.ftl":
|
||||
return <LoginIdpLinkEmail {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginIdpLinkEmail {...{ kcContext, ...commonProps }} />;
|
||||
case "login-page-expired.ftl":
|
||||
return <LoginPageExpired {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginPageExpired {...{ kcContext, ...commonProps }} />;
|
||||
case "login-config-totp.ftl":
|
||||
return <LoginConfigTotp {...{ kcContext, i18n, ...props }} />;
|
||||
return <LoginConfigTotp {...{ kcContext, ...commonProps }} />;
|
||||
case "logout-confirm.ftl":
|
||||
return <LogoutConfirm {...{ kcContext, i18n, ...props }} />;
|
||||
return <LogoutConfirm {...{ kcContext, ...commonProps }} />;
|
||||
case "update-user-profile.ftl":
|
||||
return <UpdateUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "idp-review-user-profile.ftl":
|
||||
return <IdpReviewUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
|
@ -37,7 +37,7 @@ export const defaultKcTemplateProps = {
|
||||
"stylesCommon": [
|
||||
"node_modules/patternfly/dist/css/patternfly.min.css",
|
||||
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
||||
"lib/zocial/zocial.css",
|
||||
"lib/zocial/zocial.css"
|
||||
],
|
||||
"styles": ["css/login.css"],
|
||||
"scripts": [],
|
||||
@ -60,7 +60,7 @@ export const defaultKcTemplateProps = {
|
||||
"kcFormGroupClass": ["form-group"],
|
||||
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
||||
"kcSignUpClass": ["login-pf-signup"],
|
||||
"kcInfoAreaWrapperClass": [],
|
||||
"kcInfoAreaWrapperClass": []
|
||||
} as const;
|
||||
|
||||
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
|
||||
@ -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"],
|
||||
@ -192,7 +202,7 @@ export const defaultKcProps = {
|
||||
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
|
||||
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
|
||||
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
|
||||
"kcFormOptionsWrapperClass": [],
|
||||
"kcFormOptionsWrapperClass": []
|
||||
} as const;
|
||||
|
||||
assert<typeof defaultKcProps extends KcProps ? true : false>();
|
||||
|
@ -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 "tss-react";
|
||||
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<FormEventHandler<HTMLFormElement>>(e => {
|
||||
@ -32,20 +40,21 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayInfo={social.displayInfo}
|
||||
displayWide={realm.password && social.providers !== undefined}
|
||||
headerNode={msg("doLogIn")}
|
||||
formNode={
|
||||
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}>
|
||||
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
|
||||
<div
|
||||
id="kc-form-wrapper"
|
||||
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}
|
||||
className={clsx(
|
||||
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
|
||||
)}
|
||||
>
|
||||
{realm.password && (
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
{(() => {
|
||||
const label = !realm.loginWithEmailAllowed
|
||||
? "username"
|
||||
@ -57,13 +66,13 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
|
||||
<label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg(label)}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id={autoCompleteHelper}
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.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.
|
||||
@ -74,27 +83,27 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
? { "disabled": true }
|
||||
: {
|
||||
"autoFocus": true,
|
||||
"autoComplete": "off",
|
||||
"autoComplete": "off"
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={2}
|
||||
id="password"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="password"
|
||||
type="password"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||
<div id="kc-form-options">
|
||||
{realm.rememberMe && !usernameEditDisabled && (
|
||||
<div className="checkbox">
|
||||
@ -106,7 +115,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
type="checkbox"
|
||||
{...(login.rememberMe
|
||||
? {
|
||||
"checked": true,
|
||||
"checked": true
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
@ -115,7 +124,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||
{realm.resetPasswordAllowed && (
|
||||
<span>
|
||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
||||
@ -125,24 +134,24 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<input
|
||||
type="hidden"
|
||||
id="id-hidden-input"
|
||||
name="credentialId"
|
||||
{...(auth?.selectedCredential !== undefined
|
||||
? {
|
||||
"value": auth.selectedCredential,
|
||||
"value": auth.selectedCredential
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonBlockClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="login"
|
||||
id="kc-login"
|
||||
@ -155,16 +164,16 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
|
||||
)}
|
||||
</div>
|
||||
{realm.password && social.providers !== undefined && (
|
||||
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}>
|
||||
<div id="kc-social-providers" className={clsx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
|
||||
<ul
|
||||
className={cx(
|
||||
props.kcFormSocialAccountListClass,
|
||||
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass,
|
||||
className={clsx(
|
||||
kcProps.kcFormSocialAccountListClass,
|
||||
social.providers.length > 4 && kcProps.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)}>
|
||||
<li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}>
|
||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
|
||||
<span>{p.displayName}</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -1,27 +1,34 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => {
|
||||
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
||||
export type LoginConfigTotpProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginConfigTotp;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
const LoginConfigTotp = memo((props: LoginConfigTotpProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
|
||||
HmacSHA1: "SHA1",
|
||||
HmacSHA256: "SHA256",
|
||||
HmacSHA512: "SHA512",
|
||||
"HmacSHA1": "SHA1",
|
||||
"HmacSHA256": "SHA256",
|
||||
"HmacSHA512": "SHA512"
|
||||
};
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("loginTotpTitle")}
|
||||
formNode={
|
||||
<>
|
||||
@ -93,26 +100,26 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post">
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<label htmlFor="totp" className={cx(props.kcLabelClass)}>
|
||||
<form action={url.loginAction} className={clsx(kcProps.kcFormClass)} id="kc-totp-settings-form" method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<label htmlFor="totp" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("authenticatorCode")}
|
||||
</label>{" "}
|
||||
<span className="required">*</span>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="totp"
|
||||
name="totp"
|
||||
autoComplete="off"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
aria-invalid={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={clsx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
|
||||
{messagesPerField.get("totp")}
|
||||
</span>
|
||||
)}
|
||||
@ -121,24 +128,24 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
|
||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
||||
</div>
|
||||
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<label htmlFor="userLabel" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<label htmlFor="userLabel" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("loginTotpDeviceName")}
|
||||
</label>{" "}
|
||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="userLabel"
|
||||
name="userLabel"
|
||||
autoComplete="off"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
||||
/>
|
||||
{messagesPerField.existsError("userLabel") && (
|
||||
<span id="input-error-otp-label" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
||||
<span id="input-error-otp-label" className={clsx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
|
||||
{messagesPerField.get("userLabel")}
|
||||
</span>
|
||||
)}
|
||||
@ -149,17 +156,17 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
|
||||
<>
|
||||
<input
|
||||
type="submit"
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonDefaultClass,
|
||||
props.kcButtonLargeClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonLargeClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
id="cancelTOTPBtn"
|
||||
name="cancel-aia"
|
||||
@ -171,7 +178,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
|
||||
) : (
|
||||
<input
|
||||
type="submit"
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
id="saveTOTPBtn"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
|
@ -1,28 +1,40 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n } & KcProps) => {
|
||||
export type LoginIdpLinkConfirmProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginIdpLinkConfirm;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginIdpLinkConfirm = memo((props: LoginIdpLinkConfirmProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, idpAlias } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("confirmLinkIdpTitle")}
|
||||
formNode={
|
||||
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<button
|
||||
type="submit"
|
||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="submitAction"
|
||||
id="updateProfile"
|
||||
value="updateProfile"
|
||||
@ -31,7 +43,12 @@ const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonDefaultClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="submitAction"
|
||||
id="linkAccount"
|
||||
value="linkAccount"
|
||||
|
@ -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 LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n } & KcProps) => {
|
||||
export type LoginIdpLinkEmailProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginIdpLinkEmail;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginIdpLinkEmail = memo((props: LoginIdpLinkEmailProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, realm, brokerContext, idpAlias } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("emailLinkIdpTitle", idpAlias)}
|
||||
formNode={
|
||||
<>
|
||||
|
@ -1,16 +1,24 @@
|
||||
import React, { useEffect, 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 { headInsert } from "../tools/headInsert";
|
||||
import { pathJoin } from "../../bin/tools/pathJoin";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => {
|
||||
const { otpLogin, url } = kcContext;
|
||||
export type LoginOtpProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginOtp;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
const LoginOtp = memo((props: LoginOtpProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { otpLogin, url } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
@ -19,7 +27,7 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
|
||||
|
||||
headInsert({
|
||||
"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(() => {
|
||||
if (isCleanedUp) return;
|
||||
|
||||
@ -33,46 +41,50 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("doLogIn")}
|
||||
formNode={
|
||||
<form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||
<form id="kc-otp-login-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
{otpLogin.userOtpCredentials.length > 1 && (
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
{otpLogin.userOtpCredentials.map(otpCredential => (
|
||||
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}>
|
||||
<div key={otpCredential.id} className={clsx(kcProps.kcSelectOTPListClass)}>
|
||||
<input type="hidden" value="${otpCredential.id}" />
|
||||
<div className={cx(props.kcSelectOTPListItemClass)}>
|
||||
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
|
||||
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
|
||||
<div className={clsx(kcProps.kcSelectOTPListItemClass)}>
|
||||
<span className={clsx(kcProps.kcAuthenticatorOtpCircleClass)} />
|
||||
<h2 className={clsx(kcProps.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="otp" className={clsx(kcProps.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 className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input id="otp" name="otp" autoComplete="off" type="text" className={clsx(kcProps.kcInputClass)} autoFocus />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="login"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
|
@ -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 LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n } & KcProps) => {
|
||||
export type LoginPageExpired = KcProps & {
|
||||
kcContext: KcContextBase.LoginPageExpired;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginPageExpired = memo((props: LoginPageExpired) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("pageExpiredTitle")}
|
||||
formNode={
|
||||
|
97
src/lib/components/LoginPassword.tsx
Normal file
97
src/lib/components/LoginPassword.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import type { FormEventHandler } from "react";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export type LoginPasswordProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginPassword;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginPassword = memo((props: LoginPasswordProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { realm, url, login } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||
|
||||
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoginButtonDisabled(true);
|
||||
|
||||
const formElement = e.target as HTMLFormElement;
|
||||
|
||||
formElement.submit();
|
||||
});
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("doLogIn")}
|
||||
formNode={
|
||||
<div id="kc-form">
|
||||
<div id="kc-form-wrapper">
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<hr />
|
||||
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={2}
|
||||
id="password"
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="password"
|
||||
type="password"
|
||||
autoFocus={true}
|
||||
autoComplete="on"
|
||||
defaultValue={login.password ?? ""}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||
<div id="kc-form-options" />
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||
{realm.resetPasswordAllowed && (
|
||||
<span>
|
||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
||||
{msg("doForgotPassword")}
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="login"
|
||||
id="kc-login"
|
||||
type="submit"
|
||||
value={msgStr("doLogIn")}
|
||||
disabled={isLoginButtonDisabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default LoginPassword;
|
@ -1,28 +1,35 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n } & KcProps) => {
|
||||
export type LoginResetPasswordProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginResetPassword;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginResetPassword = memo((props: LoginResetPasswordProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, realm, auth } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("emailForgotTitle")}
|
||||
formNode={
|
||||
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
||||
<form id="kc-reset-password-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
|
||||
{!realm.loginWithEmailAllowed
|
||||
? msg("username")
|
||||
: !realm.registrationEmailAsUsername
|
||||
@ -30,29 +37,34 @@ const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
|
||||
: msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
autoFocus
|
||||
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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.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={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
|
@ -1,12 +1,20 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => {
|
||||
const { cx } = useCssAndCx();
|
||||
export type LoginUpdatePasswordProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginUpdatePassword;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginUpdatePassword = memo((props: LoginUpdatePasswordProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
@ -14,11 +22,10 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("updatePasswordTitle")}
|
||||
formNode={
|
||||
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||
<form id="kc-passwd-update-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
@ -30,44 +37,46 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
|
||||
/>
|
||||
<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.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-new" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-new" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("passwordNew")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="password"
|
||||
id="password-new"
|
||||
name="password-new"
|
||||
autoFocus
|
||||
autoComplete="new-password"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.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)}>
|
||||
<div
|
||||
className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass))}
|
||||
>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="password"
|
||||
id="password-confirm"
|
||||
name="password-confirm"
|
||||
autoComplete="new-password"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
|
||||
{isAppInitiatedAction && (
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
@ -79,16 +88,16 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
{isAppInitiatedAction ? (
|
||||
<>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
defaultValue={msgStr("doSubmit")}
|
||||
/>
|
||||
<button
|
||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
name="cancel-aia"
|
||||
value="true"
|
||||
@ -98,11 +107,11 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonBlockClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
defaultValue={msgStr("doSubmit")}
|
||||
|
@ -1,12 +1,20 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n } & KcProps) => {
|
||||
const { cx } = useCssAndCx();
|
||||
export type LoginUpdateProfile = KcProps & {
|
||||
kcContext: KcContextBase.LoginUpdateProfile;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginUpdateProfile = memo((props: LoginUpdateProfile) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
@ -14,84 +22,89 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("loginProfileTitle")}
|
||||
formNode={
|
||||
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
||||
<form id="kc-update-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
{user.editUsernameAllowed && (
|
||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
defaultValue={user.username ?? ""}
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="email" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="email" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input type="text" id="email" name="email" defaultValue={user.email ?? ""} className={cx(props.kcInputClass)} />
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input type="text" id="email" name="email" defaultValue={user.email ?? ""} className={clsx(kcProps.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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
defaultValue={user.firstName ?? ""}
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="lastName" className={clsx(kcProps.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 className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
defaultValue={user.lastName ?? ""}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
{isAppInitiatedAction ? (
|
||||
<>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
defaultValue={msgStr("doSubmit")}
|
||||
/>
|
||||
<button
|
||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
name="cancel-aia"
|
||||
value="true"
|
||||
@ -101,11 +114,11 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonBlockClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
defaultValue={msgStr("doSubmit")}
|
||||
|
169
src/lib/components/LoginUsername.tsx
Normal file
169
src/lib/components/LoginUsername.tsx
Normal file
@ -0,0 +1,169 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import type { FormEventHandler } from "react";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export type LoginUsernameProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginUsername;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginUsername = memo((props: LoginUsernameProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||
|
||||
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoginButtonDisabled(true);
|
||||
|
||||
const formElement = e.target as HTMLFormElement;
|
||||
|
||||
//NOTE: Even if we login with email Keycloak expect username and password in
|
||||
//the POST request.
|
||||
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
|
||||
|
||||
formElement.submit();
|
||||
});
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayInfo={social.displayInfo}
|
||||
displayWide={realm.password && social.providers !== undefined}
|
||||
headerNode={msg("doLogIn")}
|
||||
formNode={
|
||||
<div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
|
||||
<div
|
||||
id="kc-form-wrapper"
|
||||
className={clsx(
|
||||
realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
|
||||
)}
|
||||
>
|
||||
{realm.password && (
|
||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
{!usernameHidden &&
|
||||
(() => {
|
||||
const label = !realm.loginWithEmailAllowed
|
||||
? "username"
|
||||
: realm.registrationEmailAsUsername
|
||||
? "email"
|
||||
: "usernameOrEmail";
|
||||
|
||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||
|
||||
return (
|
||||
<>
|
||||
<label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg(label)}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id={autoCompleteHelper}
|
||||
className={clsx(kcProps.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"
|
||||
autoFocus={true}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
|
||||
<div id="kc-form-options">
|
||||
{realm.rememberMe && !usernameHidden && (
|
||||
<div className="checkbox">
|
||||
<label>
|
||||
<input
|
||||
tabIndex={3}
|
||||
id="rememberMe"
|
||||
name="rememberMe"
|
||||
type="checkbox"
|
||||
{...(login.rememberMe
|
||||
? {
|
||||
"checked": true
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
{msg("rememberMe")}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.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={clsx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
|
||||
<ul
|
||||
className={clsx(
|
||||
kcProps.kcFormSocialAccountListClass,
|
||||
social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
|
||||
)}
|
||||
>
|
||||
{social.providers.map(p => (
|
||||
<li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}>
|
||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
|
||||
<span>{p.displayName}</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</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 LoginUsername;
|
@ -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 LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n } & KcProps) => {
|
||||
export type LoginVerifyEmailProps = KcProps & {
|
||||
kcContext: KcContextBase.LoginVerifyEmail;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const LoginVerifyEmail = memo((props: LoginVerifyEmailProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
const { url, user } = kcContext;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("emailVerifyTitle")}
|
||||
formNode={
|
||||
|
@ -1,21 +1,28 @@
|
||||
import React, { memo } from "react";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import Template from "./Template";
|
||||
import { clsx } from "../tools/clsx";
|
||||
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 LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n } & KcProps) => {
|
||||
const { url, client, logoutConfirm } = kcContext;
|
||||
export type LogoutConfirmProps = KcProps & {
|
||||
kcContext: KcContextBase.LogoutConfirm;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
const LogoutConfirm = memo((props: LogoutConfirmProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, client, logoutConfirm } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("logoutConfirmTitle")}
|
||||
formNode={
|
||||
@ -24,18 +31,18 @@ const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContex
|
||||
<p className="instruction">{msg("logoutConfirmHeader")}</p>
|
||||
<form className="form-actions" action={url.logoutConfirmAction} method="POST">
|
||||
<input type="hidden" name="session_code" value={logoutConfirm.code} />
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options">
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)}></div>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
|
||||
</div>
|
||||
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<input
|
||||
tabIndex={4}
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonBlockClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="confirmLogout"
|
||||
id="kc-logout"
|
||||
|
@ -1,69 +1,76 @@
|
||||
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 { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Register; i18n: I18n } & KcProps) => {
|
||||
export type RegisterProps = KcProps & {
|
||||
kcContext: KcContextBase.Register;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const Register = memo((props: RegisterProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("registerTitle")}
|
||||
formNode={
|
||||
<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.kcLabelWrapperClass)}>
|
||||
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
|
||||
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("firstName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="firstName"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="lastName" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("lastName")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="lastName"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="email" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("email")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="email"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="email"
|
||||
defaultValue={register.formData.email ?? ""}
|
||||
autoComplete="email"
|
||||
@ -71,17 +78,17 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
|
||||
</div>
|
||||
</div>
|
||||
{!realm.registrationEmailAsUsername && (
|
||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("username")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="username"
|
||||
defaultValue={register.formData.username ?? ""}
|
||||
autoComplete="username"
|
||||
@ -91,17 +98,19 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
|
||||
)}
|
||||
{passwordRequired && (
|
||||
<>
|
||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
||||
<div
|
||||
className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}
|
||||
>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("password")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
className={cx(props.kcInputClass)}
|
||||
className={clsx(kcProps.kcInputClass)}
|
||||
name="password"
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
@ -109,41 +118,46 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
props.kcFormGroupClass,
|
||||
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass),
|
||||
className={clsx(
|
||||
kcProps.kcFormGroupClass,
|
||||
messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass)
|
||||
)}
|
||||
>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
|
||||
<div className={clsx(kcProps.kcLabelWrapperClass)}>
|
||||
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
|
||||
{msg("passwordConfirm")}
|
||||
</label>
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<input type="password" id="password-confirm" className={clsx(kcProps.kcInputClass)} name="password-confirm" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcInputWrapperClass)}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.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={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
|
@ -1,59 +1,61 @@
|
||||
import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
|
||||
import Template from "./Template";
|
||||
import React, { memo, useState } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import type { ReactComponent } from "../tools/ReactComponent";
|
||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
||||
import { useFormValidationSlice } from "../useFormValidationSlice";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||
|
||||
export type RegisterUserProfileProps = KcProps & {
|
||||
kcContext: KcContextBase.RegisterUserProfile;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const RegisterUserProfile = memo((props: RegisterUserProfileProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps) => {
|
||||
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 }}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
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} />
|
||||
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
|
||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||
{recaptchaRequired && (
|
||||
<div className="form-group">
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
<div className={clsx(kcProps.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)}>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.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={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
value={msgStr("doRegister")}
|
||||
disabled={!isFomSubmittable}
|
||||
@ -66,155 +68,4 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
|
||||
);
|
||||
});
|
||||
|
||||
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile; i18n: I18n } & KcProps &
|
||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
||||
};
|
||||
|
||||
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, i18n, ...props }: UserProfileFormFieldsProps) => {
|
||||
const { cx, css } = useCssAndCx();
|
||||
|
||||
const { advancedMsg } = i18n;
|
||||
|
||||
const {
|
||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
||||
formValidationReducer,
|
||||
attributesWithPassword,
|
||||
} = useFormValidationSlice({
|
||||
kcContext,
|
||||
i18n,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onIsFormSubmittableValueChange(isFormSubmittable);
|
||||
}, [isFormSubmittable]);
|
||||
|
||||
const onChangeFactory = useCallbackFactory(
|
||||
(
|
||||
[name]: [string],
|
||||
[
|
||||
{
|
||||
target: { value },
|
||||
},
|
||||
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>],
|
||||
) =>
|
||||
formValidationReducer({
|
||||
"action": "update value",
|
||||
name,
|
||||
"newValue": value,
|
||||
}),
|
||||
);
|
||||
|
||||
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
|
||||
formValidationReducer({
|
||||
"action": "focus lost",
|
||||
name,
|
||||
}),
|
||||
);
|
||||
|
||||
let currentGroup = "";
|
||||
|
||||
return (
|
||||
<>
|
||||
{attributesWithPassword.map((attribute, i) => {
|
||||
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
|
||||
|
||||
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
|
||||
|
||||
const formGroupClassName = cx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<label id={`header-${group}`} className={cx(props.kcFormGroupHeader)}>
|
||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label id={`description-${group}`} className={`${cx(props.kcLabelClass)}`}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={formGroupClassName}>
|
||||
<div className={cx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor={attribute.name} className={cx(props.kcLabelClass)}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={cx(props.kcInputWrapperClass)}>
|
||||
{(() => {
|
||||
const { options } = attribute.validators;
|
||||
|
||||
if (options !== undefined) {
|
||||
return (
|
||||
<select
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
onChange={onChangeFactory(attribute.name)}
|
||||
onBlur={onBlurFactory(attribute.name)}
|
||||
value={value}
|
||||
>
|
||||
{options.options.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{option}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
type={(() => {
|
||||
switch (attribute.name) {
|
||||
case "password-confirm":
|
||||
case "password":
|
||||
return "password";
|
||||
default:
|
||||
return "text";
|
||||
}
|
||||
})()}
|
||||
id={attribute.name}
|
||||
name={attribute.name}
|
||||
value={value}
|
||||
onChange={onChangeFactory(attribute.name)}
|
||||
className={cx(props.kcInputClass)}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
onBlur={onBlurFactory(attribute.name)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{displayableErrors.length !== 0 && (
|
||||
<span
|
||||
id={`input-error-${attribute.name}`}
|
||||
className={cx(
|
||||
props.kcInputErrorMessageClass,
|
||||
css({
|
||||
"position": displayableErrors.length === 1 ? "absolute" : undefined,
|
||||
"& > span": { "display": "block" },
|
||||
}),
|
||||
)}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export default RegisterUserProfile;
|
||||
|
@ -7,7 +7,7 @@ import { headInsert } from "../tools/headInsert";
|
||||
import { pathJoin } from "../../bin/tools/pathJoin";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import type { KcTemplateProps } from "./KcProps";
|
||||
import { useCssAndCx } from "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export type TemplateProps = {
|
||||
@ -39,15 +39,9 @@ const Template = memo((props: TemplateProps) => {
|
||||
infoNode = null,
|
||||
kcContext,
|
||||
i18n,
|
||||
doFetchDefaultThemeResources,
|
||||
doFetchDefaultThemeResources
|
||||
} = props;
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Rendering this page with react using keycloakify");
|
||||
}, []);
|
||||
|
||||
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||
|
||||
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag));
|
||||
@ -72,16 +66,16 @@ const Template = memo((props: TemplateProps) => {
|
||||
Promise.all(
|
||||
[
|
||||
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
|
||||
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath)),
|
||||
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
|
||||
]
|
||||
.reverse()
|
||||
.map(href =>
|
||||
headInsert({
|
||||
"type": "css",
|
||||
href,
|
||||
"position": "prepend",
|
||||
}),
|
||||
),
|
||||
"position": "prepend"
|
||||
})
|
||||
)
|
||||
).then(() => {
|
||||
if (isUnmounted) {
|
||||
return;
|
||||
@ -93,14 +87,14 @@ const Template = memo((props: TemplateProps) => {
|
||||
toArr(props.scripts).forEach(relativePath =>
|
||||
headInsert({
|
||||
"type": "javascript",
|
||||
"src": pathJoin(url.resourcesPath, relativePath),
|
||||
}),
|
||||
"src": pathJoin(url.resourcesPath, relativePath)
|
||||
})
|
||||
);
|
||||
|
||||
if (props.kcHtmlClass !== undefined) {
|
||||
const htmlClassList = document.getElementsByTagName("html")[0].classList;
|
||||
|
||||
const tokens = cx(props.kcHtmlClass).split(" ");
|
||||
const tokens = clsx(props.kcHtmlClass).split(" ");
|
||||
|
||||
htmlClassList.add(...tokens);
|
||||
|
||||
@ -119,18 +113,18 @@ const Template = memo((props: TemplateProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cx(props.kcLoginClass)}>
|
||||
<div id="kc-header" className={cx(props.kcHeaderClass)}>
|
||||
<div id="kc-header-wrapper" className={cx(props.kcHeaderWrapperClass)}>
|
||||
<div className={clsx(props.kcLoginClass)}>
|
||||
<div id="kc-header" className={clsx(props.kcHeaderClass)}>
|
||||
<div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}>
|
||||
{msg("loginTitleHtml", realm.displayNameHtml)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
|
||||
<header className={cx(props.kcFormHeaderClass)}>
|
||||
<div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
|
||||
<header className={clsx(props.kcFormHeaderClass)}>
|
||||
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||
<div id="kc-locale">
|
||||
<div id="kc-locale-wrapper" className={cx(props.kcLocaleWrapperClass)}>
|
||||
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}>
|
||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||
<a href="#" id="kc-current-locale-link">
|
||||
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||
@ -150,8 +144,8 @@ const Template = memo((props: TemplateProps) => {
|
||||
)}
|
||||
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
|
||||
displayRequiredFields ? (
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<div className={clsx(props.kcContentWrapperClass)}>
|
||||
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span>
|
||||
{msg("requiredFields")}
|
||||
@ -165,20 +159,20 @@ const Template = memo((props: TemplateProps) => {
|
||||
<h1 id="kc-page-title">{headerNode}</h1>
|
||||
)
|
||||
) : displayRequiredFields ? (
|
||||
<div className={cx(props.kcContentWrapperClass)}>
|
||||
<div className={cx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<div className={clsx(props.kcContentWrapperClass)}>
|
||||
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
|
||||
<span className="subtitle">
|
||||
<span className="required">*</span> {msg("requiredFields")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col-md-10">
|
||||
{showUsernameNode}
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={cx(props.kcResetFlowIcon)}></i>
|
||||
<i className={clsx(props.kcResetFlowIcon)}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
@ -189,12 +183,12 @@ const Template = memo((props: TemplateProps) => {
|
||||
) : (
|
||||
<>
|
||||
{showUsernameNode}
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<div id="kc-username">
|
||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||
<div className="kc-login-tooltip">
|
||||
<i className={cx(props.kcResetFlowIcon)}></i>
|
||||
<i className={clsx(props.kcResetFlowIcon)}></i>
|
||||
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
|
||||
</div>
|
||||
</a>
|
||||
@ -207,15 +201,15 @@ const Template = memo((props: TemplateProps) => {
|
||||
<div id="kc-content-wrapper">
|
||||
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
|
||||
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
|
||||
<div className={cx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className={cx(props.kcFeedbackSuccessIcon)}></span>}
|
||||
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>}
|
||||
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>}
|
||||
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>}
|
||||
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||
{message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>}
|
||||
{message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>}
|
||||
{message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>}
|
||||
{message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>}
|
||||
<span
|
||||
className="kc-feedback-text"
|
||||
dangerouslySetInnerHTML={{
|
||||
"__html": message.summary,
|
||||
"__html": message.summary
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@ -226,10 +220,10 @@ const Template = memo((props: TemplateProps) => {
|
||||
id="kc-select-try-another-way-form"
|
||||
action={url.loginAction}
|
||||
method="post"
|
||||
className={cx(displayWide && props.kcContentWrapperClass)}
|
||||
className={clsx(displayWide && props.kcContentWrapperClass)}
|
||||
>
|
||||
<div className={cx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
|
||||
<div className={cx(props.kcFormGroupClass)}>
|
||||
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
|
||||
<div className={clsx(props.kcFormGroupClass)}>
|
||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>
|
||||
{msg("doTryAnotherWay")}
|
||||
@ -239,8 +233,8 @@ const Template = memo((props: TemplateProps) => {
|
||||
</form>
|
||||
)}
|
||||
{displayInfo && (
|
||||
<div id="kc-info" className={cx(props.kcSignUpClass)}>
|
||||
<div id="kc-info-wrapper" className={cx(props.kcInfoAreaWrapperClass)}>
|
||||
<div id="kc-info" className={clsx(props.kcSignUpClass)}>
|
||||
<div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}>
|
||||
{infoNode}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,8 +1,9 @@
|
||||
import React, { useEffect, 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 "tss-react";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import { Evt } from "evt";
|
||||
import { useRerenderOnStateChange } from "evt/hooks";
|
||||
import { assert } from "tsafe/assert";
|
||||
@ -11,16 +12,19 @@ import type { I18n } from "../i18n";
|
||||
import memoize from "memoizee";
|
||||
import { useConst } from "powerhooks/useConst";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
import { Markdown } from "../tools/Markdown";
|
||||
import type { Extends } from "tsafe";
|
||||
|
||||
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
|
||||
|
||||
export type KcContextLike = {
|
||||
pageId: KcContextBase["pageId"];
|
||||
locale?: {
|
||||
currentLanguageTag: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<KcContextBase extends KcContextLike ? true : false>();
|
||||
assert<Extends<KcContextBase, KcContextLike>>();
|
||||
|
||||
/** Allow to avoid bundling the terms and download it on demand*/
|
||||
export function useDownloadTerms(params: {
|
||||
@ -35,26 +39,37 @@ export function useDownloadTerms(params: {
|
||||
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
|
||||
|
||||
const downloadTermMarkdownMemoized = useConst(() =>
|
||||
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }), { "promise": true }),
|
||||
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }), { "promise": true })
|
||||
);
|
||||
|
||||
return { downloadTermMarkdownMemoized };
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
if (kcContext.pageId !== "terms.ftl") {
|
||||
return;
|
||||
}
|
||||
|
||||
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
|
||||
thermMarkdown => (evtTermMarkdown.state = thermMarkdown),
|
||||
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
|
||||
);
|
||||
}, []);
|
||||
}
|
||||
|
||||
const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Terms; i18n: I18n } & KcProps) => {
|
||||
export type TermsProps = KcProps & {
|
||||
kcContext: KcContextBase.Terms;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const Terms = memo((props: TermsProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
useRerenderOnStateChange(evtTermMarkdown);
|
||||
|
||||
const { cx } = useCssAndCx();
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
if (evtTermMarkdown.state === undefined) {
|
||||
@ -63,21 +78,20 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, ...props }}
|
||||
doFetchDefaultThemeResources={true}
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
displayMessage={false}
|
||||
headerNode={msg("termsTitle")}
|
||||
formNode={
|
||||
<>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state}</div>
|
||||
<div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
|
||||
<form className="form-actions" action={url.loginAction} method="POST">
|
||||
<input
|
||||
className={cx(
|
||||
props.kcButtonClass,
|
||||
props.kcButtonClass,
|
||||
props.kcButtonClass,
|
||||
props.kcButtonPrimaryClass,
|
||||
props.kcButtonLargeClass,
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
name="accept"
|
||||
id="kc-accept"
|
||||
@ -85,7 +99,7 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
|
||||
value={msgStr("doAccept")}
|
||||
/>
|
||||
<input
|
||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||
name="cancel"
|
||||
id="kc-decline"
|
||||
type="submit"
|
||||
|
78
src/lib/components/UpdateUserProfile.tsx
Normal file
78
src/lib/components/UpdateUserProfile.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import React, { useState, memo } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n } from "../i18n";
|
||||
import { UserProfileFormFields } from "./shared/UserProfileCommons";
|
||||
|
||||
export type UpdateUserProfileProps = KcProps & {
|
||||
kcContext: KcContextBase.UpdateUserProfile;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const UpdateUserProfile = memo((props: UpdateUserProfileProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { url, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("loginProfileTitle")}
|
||||
formNode={
|
||||
<form id="kc-update-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
|
||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
|
||||
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
|
||||
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
|
||||
</div>
|
||||
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
{isAppInitiatedAction ? (
|
||||
<>
|
||||
<input
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
value={msgStr("doSubmit")}
|
||||
/>
|
||||
<button
|
||||
className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
|
||||
type="submit"
|
||||
name="cancel-aia"
|
||||
value="true"
|
||||
formNoValidate
|
||||
>
|
||||
{msg("doCancel")}
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<input
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
type="submit"
|
||||
defaultValue={msgStr("doSubmit")}
|
||||
disabled={!isFomSubmittable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default UpdateUserProfile;
|
203
src/lib/components/WebauthnAuthenticate.tsx
Normal file
203
src/lib/components/WebauthnAuthenticate.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import React, { useRef, useState, memo } from "react";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { TemplateProps } from "./Template";
|
||||
import type { KcProps } from "./KcProps";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { clsx } from "../tools/clsx";
|
||||
import type { I18n, MessageKeyBase } from "../i18n";
|
||||
import { base64url } from "rfc4648";
|
||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||
|
||||
export type WebauthnAuthenticateProps = KcProps & {
|
||||
kcContext: KcContextBase.WebauthnAuthenticate;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template?: (props: TemplateProps) => JSX.Element | null;
|
||||
};
|
||||
|
||||
const WebauthnAuthenticate = memo((props: WebauthnAuthenticateProps) => {
|
||||
const { kcContext, i18n, doFetchDefaultThemeResources = true, Template = DefaultTemplate, ...kcProps } = props;
|
||||
|
||||
const { url } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext;
|
||||
const createTimeout = Number(kcContext.createTimeout);
|
||||
const isUserIdentified = kcContext.isUserIdentified == "true";
|
||||
|
||||
const webAuthnAuthenticate = useConstCallback(async () => {
|
||||
if (!isUserIdentified) {
|
||||
return;
|
||||
}
|
||||
const allowCredentials = authenticators.authenticators.map(
|
||||
authenticator =>
|
||||
({
|
||||
id: base64url.parse(authenticator.credentialId, { loose: true }),
|
||||
type: "public-key"
|
||||
} as PublicKeyCredentialDescriptor)
|
||||
);
|
||||
// Check if WebAuthn is supported by this browser
|
||||
if (!window.PublicKeyCredential) {
|
||||
setError(msgStr("webauthn-unsupported-browser-text"));
|
||||
submitForm();
|
||||
return;
|
||||
}
|
||||
|
||||
const publicKey: PublicKeyCredentialRequestOptions = {
|
||||
rpId,
|
||||
challenge: base64url.parse(challenge, { loose: true })
|
||||
};
|
||||
|
||||
if (createTimeout !== 0) {
|
||||
publicKey.timeout = createTimeout * 1000;
|
||||
}
|
||||
|
||||
if (allowCredentials.length) {
|
||||
publicKey.allowCredentials = allowCredentials;
|
||||
}
|
||||
|
||||
if (userVerification !== "not specified") {
|
||||
publicKey.userVerification = userVerification;
|
||||
}
|
||||
|
||||
try {
|
||||
const resultRaw = await navigator.credentials.get({ publicKey });
|
||||
if (!resultRaw || resultRaw.type != "public-key") return;
|
||||
const result = resultRaw as PublicKeyCredential;
|
||||
if (!("authenticatorData" in result.response)) return;
|
||||
const response = result.response as AuthenticatorAssertionResponse;
|
||||
const clientDataJSON = response.clientDataJSON;
|
||||
const authenticatorData = response.authenticatorData;
|
||||
const signature = response.signature;
|
||||
|
||||
setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { pad: false }));
|
||||
setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { pad: false }));
|
||||
setSignature(base64url.stringify(new Uint8Array(signature), { pad: false }));
|
||||
setCredentialId(result.id);
|
||||
setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { pad: false }));
|
||||
submitForm();
|
||||
} catch (err) {
|
||||
setError(String(err));
|
||||
submitForm();
|
||||
}
|
||||
});
|
||||
|
||||
const webAuthForm = useRef<HTMLFormElement>(null);
|
||||
const submitForm = useConstCallback(() => {
|
||||
webAuthForm.current!.submit();
|
||||
});
|
||||
|
||||
const [clientDataJSON, setClientDataJSON] = useState("");
|
||||
const [authenticatorData, setAuthenticatorData] = useState("");
|
||||
const [signature, setSignature] = useState("");
|
||||
const [credentialId, setCredentialId] = useState("");
|
||||
const [userHandle, setUserHandle] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
|
||||
return (
|
||||
<Template
|
||||
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
|
||||
headerNode={msg("webauthn-login-title")}
|
||||
formNode={
|
||||
<div id="kc-form-webauthn" className={clsx(kcProps.kcFormClass)}>
|
||||
<form id="webauth" action={url.loginAction} ref={webAuthForm} method="post">
|
||||
<input type="hidden" id="clientDataJSON" name="clientDataJSON" value={clientDataJSON} />
|
||||
<input type="hidden" id="authenticatorData" name="authenticatorData" value={authenticatorData} />
|
||||
<input type="hidden" id="signature" name="signature" value={signature} />
|
||||
<input type="hidden" id="credentialId" name="credentialId" value={credentialId} />
|
||||
<input type="hidden" id="userHandle" name="userHandle" value={userHandle} />
|
||||
<input type="hidden" id="error" name="error" value={error} />
|
||||
</form>
|
||||
<div className={clsx(kcProps.kcFormGroupClass)}>
|
||||
{authenticators &&
|
||||
(() => (
|
||||
<form id="authn_select" className={clsx(kcProps.kcFormClass)}>
|
||||
{authenticators.authenticators.map(authenticator => (
|
||||
<input
|
||||
type="hidden"
|
||||
name="authn_use_chk"
|
||||
value={authenticator.credentialId}
|
||||
key={authenticator.credentialId}
|
||||
/>
|
||||
))}
|
||||
</form>
|
||||
))()}
|
||||
{authenticators &&
|
||||
shouldDisplayAuthenticators &&
|
||||
(() => (
|
||||
<>
|
||||
{authenticators.authenticators.length > 1 && (
|
||||
<p className={clsx(kcProps.kcSelectAuthListItemTitle)}>{msg("webauthn-available-authenticators")}</p>
|
||||
)}
|
||||
<div className={clsx(kcProps.kcFormClass)}>
|
||||
{authenticators.authenticators.map(authenticator => (
|
||||
<div id="kc-webauthn-authenticator" className={clsx(kcProps.kcSelectAuthListItemClass)}>
|
||||
<div className={clsx(kcProps.kcSelectAuthListItemIconClass)}>
|
||||
<i
|
||||
className={clsx(
|
||||
kcProps[authenticator.transports.iconClass] ?? kcProps.kcWebAuthnDefaultIcon,
|
||||
kcProps.kcSelectAuthListItemIconPropertyClass
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcSelectAuthListItemBodyClass)}>
|
||||
<div
|
||||
id="kc-webauthn-authenticator-label"
|
||||
className={clsx(kcProps.kcSelectAuthListItemHeadingClass)}
|
||||
>
|
||||
{authenticator.label}
|
||||
</div>
|
||||
|
||||
{authenticator.transports && authenticator.transports.displayNameProperties.length && (
|
||||
<div
|
||||
id="kc-webauthn-authenticator-transport"
|
||||
className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}
|
||||
>
|
||||
{authenticator.transports.displayNameProperties.map(
|
||||
(transport: MessageKeyBase, index: number) => (
|
||||
<>
|
||||
<span>{msg(transport)}</span>
|
||||
{index < authenticator.transports.displayNameProperties.length - 1 && (
|
||||
<span>{", "}</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}>
|
||||
<span id="kc-webauthn-authenticator-created-label">{msg("webauthn-createdAt-label")}</span>
|
||||
<span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={clsx(kcProps.kcSelectAuthListItemFillClass)} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
))()}
|
||||
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
|
||||
<input
|
||||
id="authenticateWebAuthnButton"
|
||||
type="button"
|
||||
onClick={webAuthnAuthenticate}
|
||||
autoFocus={true}
|
||||
value={msgStr("webauthn-doAuthenticate")}
|
||||
className={clsx(
|
||||
kcProps.kcButtonClass,
|
||||
kcProps.kcButtonPrimaryClass,
|
||||
kcProps.kcButtonBlockClass,
|
||||
kcProps.kcButtonLargeClass
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default WebauthnAuthenticate;
|
173
src/lib/components/shared/UserProfileCommons.tsx
Normal file
173
src/lib/components/shared/UserProfileCommons.tsx
Normal file
@ -0,0 +1,173 @@
|
||||
import React, { memo, useEffect, Fragment } from "react";
|
||||
import type { KcProps } from "../KcProps";
|
||||
import type { Attribute } from "../../getKcContext/KcContextBase";
|
||||
import { clsx } from "../../tools/clsx";
|
||||
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 { 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 = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
|
||||
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{group !== currentGroup && (currentGroup = group) !== "" && (
|
||||
<div className={formGroupClassName}>
|
||||
<div className={clsx(props.kcContentWrapperClass)}>
|
||||
<label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
|
||||
{advancedMsg(groupDisplayHeader) || currentGroup}
|
||||
</label>
|
||||
</div>
|
||||
{groupDisplayDescription !== "" && (
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
|
||||
{advancedMsg(groupDisplayDescription)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{BeforeField && <BeforeField attribute={attribute} />}
|
||||
|
||||
<div className={formGroupClassName}>
|
||||
<div className={clsx(props.kcLabelWrapperClass)}>
|
||||
<label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
|
||||
{advancedMsg(attribute.displayName ?? "")}
|
||||
</label>
|
||||
{attribute.required && <>*</>}
|
||||
</div>
|
||||
<div className={clsx(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={clsx(props.kcInputClass)}
|
||||
aria-invalid={displayableErrors.length !== 0}
|
||||
disabled={attribute.readOnly}
|
||||
autoComplete={attribute.autocomplete}
|
||||
onBlur={onBlurFactory(attribute.name)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
{displayableErrors.length !== 0 &&
|
||||
(() => {
|
||||
const divId = `input-error-${attribute.name}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`#${divId} > span: { display: block; }`}</style>
|
||||
<span
|
||||
id={divId}
|
||||
className={clsx(props.kcInputErrorMessageClass)}
|
||||
style={{
|
||||
"position": displayableErrors.length === 1 ? "absolute" : undefined
|
||||
}}
|
||||
aria-live="polite"
|
||||
>
|
||||
{displayableErrors.map(({ errorMessage }) => errorMessage)}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{AfterField && <AfterField attribute={attribute} />}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
@ -1,7 +1,8 @@
|
||||
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
|
||||
import type { PageId } from "../../bin/keycloakify/generateFtl";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { Equals } from "tsafe";
|
||||
import type { MessageKeyBase } from "../i18n";
|
||||
import type { KcTemplateClassKey } from "../components/KcProps";
|
||||
|
||||
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
|
||||
|
||||
@ -19,13 +20,28 @@ export type KcContextBase =
|
||||
| KcContextBase.LoginVerifyEmail
|
||||
| KcContextBase.Terms
|
||||
| KcContextBase.LoginOtp
|
||||
| KcContextBase.LoginUsername
|
||||
| KcContextBase.WebauthnAuthenticate
|
||||
| KcContextBase.LoginPassword
|
||||
| KcContextBase.LoginUpdatePassword
|
||||
| KcContextBase.LoginUpdateProfile
|
||||
| KcContextBase.LoginIdpLinkConfirm
|
||||
| KcContextBase.LoginIdpLinkEmail
|
||||
| KcContextBase.LoginPageExpired
|
||||
| KcContextBase.LoginConfigTotp
|
||||
| KcContextBase.LogoutConfirm;
|
||||
| KcContextBase.LogoutConfirm
|
||||
| KcContextBase.UpdateUserProfile
|
||||
| KcContextBase.IdpReviewUserProfile;
|
||||
|
||||
export type WebauthnAuthenticator = {
|
||||
credentialId: string;
|
||||
transports: {
|
||||
iconClass: KcTemplateClassKey;
|
||||
displayNameProperties: MessageKeyBase[];
|
||||
};
|
||||
label: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export declare namespace KcContextBase {
|
||||
export type Common = {
|
||||
@ -196,6 +212,77 @@ export declare namespace KcContextBase {
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginUsername = Common & {
|
||||
pageId: "login-username.ftl";
|
||||
url: {
|
||||
loginResetCredentialsUrl: string;
|
||||
registrationUrl: string;
|
||||
};
|
||||
realm: {
|
||||
loginWithEmailAllowed: boolean;
|
||||
rememberMe: boolean;
|
||||
password: boolean;
|
||||
resetPasswordAllowed: boolean;
|
||||
registrationAllowed: boolean;
|
||||
};
|
||||
registrationDisabled: boolean;
|
||||
login: {
|
||||
username?: string;
|
||||
rememberMe?: boolean;
|
||||
};
|
||||
usernameHidden?: boolean;
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
providers?: {
|
||||
loginUrl: string;
|
||||
alias: string;
|
||||
providerId: string;
|
||||
displayName: string;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
export type LoginPassword = Common & {
|
||||
pageId: "login-password.ftl";
|
||||
url: {
|
||||
loginResetCredentialsUrl: string;
|
||||
registrationUrl: string;
|
||||
};
|
||||
realm: {
|
||||
resetPasswordAllowed: boolean;
|
||||
};
|
||||
auth?: {
|
||||
showUsername?: boolean;
|
||||
showResetCredentials?: boolean;
|
||||
showTryAnotherWayLink?: boolean;
|
||||
attemptedUsername?: string;
|
||||
};
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
login: {
|
||||
password?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebauthnAuthenticate = Common & {
|
||||
pageId: "webauthn-authenticate.ftl";
|
||||
authenticators: {
|
||||
authenticators: WebauthnAuthenticator[];
|
||||
};
|
||||
challenge: string;
|
||||
// I hate this:
|
||||
userVerification: UserVerificationRequirement | "not specified";
|
||||
rpId: string;
|
||||
createTimeout: string;
|
||||
isUserIdentified: "true" | "false";
|
||||
shouldDisplayAuthenticators: boolean;
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
login: {};
|
||||
};
|
||||
|
||||
export type LoginUpdatePassword = Common & {
|
||||
pageId: "login-update-password.ftl";
|
||||
username: string;
|
||||
@ -270,6 +357,23 @@ export declare namespace KcContextBase {
|
||||
skipLink?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type UpdateUserProfile = Common & {
|
||||
pageId: "update-user-profile.ftl";
|
||||
profile: {
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
};
|
||||
};
|
||||
|
||||
export type IdpReviewUserProfile = Common & {
|
||||
pageId: "idp-review-user-profile.ftl";
|
||||
profile: {
|
||||
context: "IDP_REVIEW";
|
||||
attributes: Attribute[];
|
||||
attributesByName: Record<string, Attribute>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type Attribute = {
|
||||
|
@ -10,6 +10,7 @@ import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||
import { pathJoin } from "../../bin/tools/pathJoin";
|
||||
import { pathBasename } from "../tools/pathBasename";
|
||||
import { mockTestingResourcesCommonPath } from "../../bin/mockTestingResourcesPath";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
|
||||
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||
@ -17,9 +18,19 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
}): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } {
|
||||
const { mockPageId, mockData } = params ?? {};
|
||||
|
||||
if (mockPageId !== undefined) {
|
||||
const realKcContext = getKcContextFromWindow<KcContextExtended>();
|
||||
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
console.log(
|
||||
[
|
||||
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||
].join(" "),
|
||||
"background: red; color: yellow; font-size: medium"
|
||||
);
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||
@ -29,8 +40,8 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
[
|
||||
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||
`Please check the documentation of the getKcContext function`,
|
||||
].join("\n"),
|
||||
`Please check the documentation of the getKcContext function`
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,17 +49,25 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock },
|
||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||
});
|
||||
|
||||
if (partialKcContextCustomMock !== undefined) {
|
||||
deepAssign({
|
||||
"target": kcContext,
|
||||
"source": partialKcContextCustomMock,
|
||||
"source": partialKcContextCustomMock
|
||||
});
|
||||
|
||||
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
|
||||
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
|
||||
if (
|
||||
partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
|
||||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
|
||||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
|
||||
) {
|
||||
assert(
|
||||
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
|
||||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
|
||||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
|
||||
);
|
||||
|
||||
const { attributes } = kcContextDefaultMock.profile;
|
||||
|
||||
@ -56,7 +75,7 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName = {};
|
||||
|
||||
const partialAttributes = [
|
||||
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? []),
|
||||
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? [])
|
||||
].filter(exclude(undefined));
|
||||
|
||||
attributes.forEach(attribute => {
|
||||
@ -66,7 +85,7 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
|
||||
deepAssign({
|
||||
"target": augmentedAttribute,
|
||||
"source": attribute,
|
||||
"source": attribute
|
||||
});
|
||||
|
||||
if (partialAttribute !== undefined) {
|
||||
@ -74,7 +93,7 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
|
||||
deepAssign({
|
||||
"target": augmentedAttribute,
|
||||
"source": partialAttribute,
|
||||
"source": partialAttribute
|
||||
});
|
||||
}
|
||||
|
||||
@ -82,27 +101,31 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
|
||||
});
|
||||
|
||||
partialAttributes.forEach(partialAttribute => {
|
||||
const { name } = partialAttribute;
|
||||
partialAttributes
|
||||
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
|
||||
.forEach(partialAttribute => {
|
||||
const { name } = partialAttribute;
|
||||
|
||||
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
||||
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.attributesByName[name] = partialAttribute as any;
|
||||
});
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
const kcContext = getKcContextFromWindow<KcContextExtended>();
|
||||
if (realKcContext === undefined) {
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
if (kcContext !== undefined) {
|
||||
const { url } = kcContext;
|
||||
{
|
||||
const { url } = realKcContext;
|
||||
|
||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
return { "kcContext": realKcContext };
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { KcContextBase } from "./KcContextBase";
|
||||
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
||||
import { ftlValuesGlobalName } from "../../bin/keycloakify/ftlValuesGlobalName";
|
||||
|
||||
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
|
||||
? KcContextBase
|
||||
|
@ -7,26 +7,120 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
|
||||
|
||||
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||
|
||||
const attributes: Attribute[] = [
|
||||
{
|
||||
"validators": {
|
||||
"username-prohibited-characters": {
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"up-username-has-value": {},
|
||||
"length": {
|
||||
"ignore.empty.value": true,
|
||||
"min": "3",
|
||||
"max": "255"
|
||||
},
|
||||
"up-duplicate-username": {},
|
||||
"up-username-mutation": {}
|
||||
},
|
||||
"displayName": "${username}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "username",
|
||||
"readOnly": false,
|
||||
"name": "username",
|
||||
"value": "xxxx"
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"up-email-exists-as-username": {},
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"up-blank-attribute-value": {
|
||||
"error-message": "missingEmailMessage",
|
||||
"fail-on-null": false
|
||||
},
|
||||
"up-duplicate-email": {},
|
||||
"email": {
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"pattern": {
|
||||
"ignore.empty.value": true,
|
||||
"pattern": "gmail\\.com$"
|
||||
}
|
||||
},
|
||||
"displayName": "${email}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "email",
|
||||
"readOnly": false,
|
||||
"name": "email"
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"person-name-prohibited-characters": {
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"up-immutable-attribute": {},
|
||||
"up-attribute-required-by-metadata-value": {}
|
||||
},
|
||||
"displayName": "${firstName}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"readOnly": false,
|
||||
"name": "firstName"
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"person-name-prohibited-characters": {
|
||||
"ignore.empty.value": true
|
||||
},
|
||||
"up-immutable-attribute": {},
|
||||
"up-attribute-required-by-metadata-value": {}
|
||||
},
|
||||
"displayName": "${lastName}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"readOnly": false,
|
||||
"name": "lastName"
|
||||
}
|
||||
];
|
||||
|
||||
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
|
||||
|
||||
export const kcContextCommonMock: KcContextBase.Common = {
|
||||
"url": {
|
||||
"loginAction": "#",
|
||||
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
|
||||
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
|
||||
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
|
||||
},
|
||||
"realm": {
|
||||
"name": "myrealm",
|
||||
"displayName": "myrealm",
|
||||
"displayNameHtml": "myrealm",
|
||||
"internationalizationEnabled": true,
|
||||
"registrationEmailAsUsername": false,
|
||||
"registrationEmailAsUsername": false
|
||||
},
|
||||
"messagesPerField": {
|
||||
"printIfExists": (...[, x]) => x,
|
||||
"existsError": () => true,
|
||||
"get": key => `Fake error for ${key}`,
|
||||
"exists": () => true,
|
||||
"exists": () => true
|
||||
},
|
||||
"locale": {
|
||||
"supported": [
|
||||
@ -34,117 +128,117 @@ export const kcContextCommonMock: KcContextBase.Common = {
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||
"label": "Deutsch",
|
||||
"languageTag": "de",
|
||||
"languageTag": "de"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||
"label": "Norsk",
|
||||
"languageTag": "no",
|
||||
"languageTag": "no"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||
"label": "Русский",
|
||||
"languageTag": "ru",
|
||||
"languageTag": "ru"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||
"label": "Svenska",
|
||||
"languageTag": "sv",
|
||||
"languageTag": "sv"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||
"label": "Português (Brasil)",
|
||||
"languageTag": "pt-BR",
|
||||
"languageTag": "pt-BR"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||
"label": "Lietuvių",
|
||||
"languageTag": "lt",
|
||||
"languageTag": "lt"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||
"label": "English",
|
||||
"languageTag": "en",
|
||||
"languageTag": "en"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||
"label": "Italiano",
|
||||
"languageTag": "it",
|
||||
"languageTag": "it"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||
"label": "Français",
|
||||
"languageTag": "fr",
|
||||
"languageTag": "fr"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||
"label": "中文简体",
|
||||
"languageTag": "zh-CN",
|
||||
"languageTag": "zh-CN"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||
"label": "Español",
|
||||
"languageTag": "es",
|
||||
"languageTag": "es"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||
"label": "Čeština",
|
||||
"languageTag": "cs",
|
||||
"languageTag": "cs"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||
"label": "日本語",
|
||||
"languageTag": "ja",
|
||||
"languageTag": "ja"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||
"label": "Slovenčina",
|
||||
"languageTag": "sk",
|
||||
"languageTag": "sk"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||
"label": "Polski",
|
||||
"languageTag": "pl",
|
||||
"languageTag": "pl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||
"label": "Català",
|
||||
"languageTag": "ca",
|
||||
"languageTag": "ca"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||
"label": "Nederlands",
|
||||
"languageTag": "nl",
|
||||
"languageTag": "nl"
|
||||
},
|
||||
{
|
||||
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||
"label": "Türkçe",
|
||||
"languageTag": "tr",
|
||||
},
|
||||
"languageTag": "tr"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
],
|
||||
"currentLanguageTag": "en",
|
||||
"currentLanguageTag": "en"
|
||||
},
|
||||
"auth": {
|
||||
"showUsername": false,
|
||||
"showResetCredentials": false,
|
||||
"showTryAnotherWayLink": false,
|
||||
"showTryAnotherWayLink": false
|
||||
},
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"clientId": "myApp"
|
||||
},
|
||||
"scripts": [],
|
||||
"message": {
|
||||
"type": "success",
|
||||
"summary": "This is a test message",
|
||||
"summary": "This is a test message"
|
||||
},
|
||||
"isAppInitiatedAction": false,
|
||||
"isAppInitiatedAction": false
|
||||
};
|
||||
|
||||
const loginUrl = {
|
||||
...kcContextCommonMock.url,
|
||||
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
|
||||
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
|
||||
};
|
||||
|
||||
export const kcContextMocks: KcContextBase[] = [
|
||||
@ -158,17 +252,17 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"rememberMe": true,
|
||||
"password": true,
|
||||
"resetPasswordAllowed": true,
|
||||
"registrationAllowed": true,
|
||||
"registrationAllowed": true
|
||||
},
|
||||
"auth": kcContextCommonMock.auth!,
|
||||
"social": {
|
||||
"displayInfo": true,
|
||||
"displayInfo": true
|
||||
},
|
||||
"usernameEditDisabled": false,
|
||||
"login": {
|
||||
"rememberMe": false,
|
||||
"rememberMe": false
|
||||
},
|
||||
"registrationDisabled": false,
|
||||
"registrationDisabled": false
|
||||
}),
|
||||
...(() => {
|
||||
const registerCommon: KcContextBase.RegisterCommon = {
|
||||
@ -176,15 +270,15 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"url": {
|
||||
...loginUrl,
|
||||
"registrationAction":
|
||||
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0",
|
||||
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
|
||||
},
|
||||
"scripts": [],
|
||||
"isAppInitiatedAction": false,
|
||||
"passwordRequired": true,
|
||||
"recaptchaRequired": false,
|
||||
"social": {
|
||||
"displayInfo": true,
|
||||
},
|
||||
"displayInfo": true
|
||||
}
|
||||
};
|
||||
|
||||
return [
|
||||
@ -192,114 +286,18 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"pageId": "register.ftl",
|
||||
...registerCommon,
|
||||
"register": {
|
||||
"formData": {},
|
||||
},
|
||||
"formData": {}
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.RegisterUserProfile>({
|
||||
"pageId": "register-user-profile.ftl",
|
||||
...registerCommon,
|
||||
"profile": {
|
||||
"context": "REGISTRATION_PROFILE" as const,
|
||||
...(() => {
|
||||
const attributes: Attribute[] = [
|
||||
{
|
||||
"validators": {
|
||||
"username-prohibited-characters": {
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"up-username-has-value": {},
|
||||
"length": {
|
||||
"ignore.empty.value": true,
|
||||
"min": "3",
|
||||
"max": "255",
|
||||
},
|
||||
"up-duplicate-username": {},
|
||||
"up-username-mutation": {},
|
||||
},
|
||||
"displayName": "${username}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "username",
|
||||
"readOnly": false,
|
||||
"name": "username",
|
||||
"value": "xxxx",
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"up-email-exists-as-username": {},
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"up-blank-attribute-value": {
|
||||
"error-message": "missingEmailMessage",
|
||||
"fail-on-null": false,
|
||||
},
|
||||
"up-duplicate-email": {},
|
||||
"email": {
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"pattern": {
|
||||
"ignore.empty.value": true,
|
||||
"pattern": "gmail\\.com$",
|
||||
},
|
||||
},
|
||||
"displayName": "${email}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"autocomplete": "email",
|
||||
"readOnly": false,
|
||||
"name": "email",
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"person-name-prohibited-characters": {
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"up-immutable-attribute": {},
|
||||
"up-attribute-required-by-metadata-value": {},
|
||||
},
|
||||
"displayName": "${firstName}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"readOnly": false,
|
||||
"name": "firstName",
|
||||
},
|
||||
{
|
||||
"validators": {
|
||||
"length": {
|
||||
"max": "255",
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"person-name-prohibited-characters": {
|
||||
"ignore.empty.value": true,
|
||||
},
|
||||
"up-immutable-attribute": {},
|
||||
"up-attribute-required-by-metadata-value": {},
|
||||
},
|
||||
"displayName": "${lastName}",
|
||||
"annotations": {},
|
||||
"required": true,
|
||||
"groupAnnotations": {},
|
||||
"readOnly": false,
|
||||
"name": "lastName",
|
||||
},
|
||||
];
|
||||
|
||||
return {
|
||||
attributes,
|
||||
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any,
|
||||
} as any;
|
||||
})(),
|
||||
},
|
||||
}),
|
||||
attributes,
|
||||
attributesByName
|
||||
}
|
||||
})
|
||||
];
|
||||
})(),
|
||||
id<KcContextBase.Info>({
|
||||
@ -311,39 +309,39 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"actionUri": "#",
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"baseUrl": "#",
|
||||
},
|
||||
"baseUrl": "#"
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.Error>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "error.ftl",
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"baseUrl": "#",
|
||||
"baseUrl": "#"
|
||||
},
|
||||
"message": {
|
||||
"type": "error",
|
||||
"summary": "This is the error message",
|
||||
},
|
||||
"summary": "This is the error message"
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LoginResetPassword>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-reset-password.ftl",
|
||||
"realm": {
|
||||
...kcContextCommonMock.realm,
|
||||
"loginWithEmailAllowed": false,
|
||||
},
|
||||
"loginWithEmailAllowed": false
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LoginVerifyEmail>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-verify-email.ftl",
|
||||
"user": {
|
||||
"email": "john.doe@gmail.com",
|
||||
},
|
||||
"email": "john.doe@gmail.com"
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.Terms>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "terms.ftl",
|
||||
"pageId": "terms.ftl"
|
||||
}),
|
||||
id<KcContextBase.LoginOtp>({
|
||||
...kcContextCommonMock,
|
||||
@ -352,19 +350,74 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"userOtpCredentials": [
|
||||
{
|
||||
"id": "id1",
|
||||
"userLabel": "label1",
|
||||
"userLabel": "label1"
|
||||
},
|
||||
{
|
||||
"id": "id2",
|
||||
"userLabel": "label2",
|
||||
},
|
||||
],
|
||||
"userLabel": "label2"
|
||||
}
|
||||
]
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LoginUsername>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-username.ftl",
|
||||
"url": loginUrl,
|
||||
"realm": {
|
||||
...kcContextCommonMock.realm,
|
||||
"loginWithEmailAllowed": true,
|
||||
"rememberMe": true,
|
||||
"password": true,
|
||||
"resetPasswordAllowed": true,
|
||||
"registrationAllowed": true
|
||||
},
|
||||
"social": {
|
||||
"displayInfo": true
|
||||
},
|
||||
"usernameHidden": false,
|
||||
"login": {
|
||||
"rememberMe": false
|
||||
},
|
||||
"registrationDisabled": false
|
||||
}),
|
||||
id<KcContextBase.LoginPassword>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-password.ftl",
|
||||
"url": loginUrl,
|
||||
"realm": {
|
||||
...kcContextCommonMock.realm,
|
||||
"resetPasswordAllowed": true
|
||||
},
|
||||
"social": {
|
||||
"displayInfo": false
|
||||
},
|
||||
"login": {}
|
||||
}),
|
||||
id<KcContextBase.WebauthnAuthenticate>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "webauthn-authenticate.ftl",
|
||||
"url": loginUrl,
|
||||
"authenticators": {
|
||||
"authenticators": []
|
||||
},
|
||||
"realm": {
|
||||
...kcContextCommonMock.realm
|
||||
},
|
||||
"challenge": "",
|
||||
"userVerification": "not specified",
|
||||
"rpId": "",
|
||||
"createTimeout": "0",
|
||||
"isUserIdentified": "false",
|
||||
"shouldDisplayAuthenticators": false,
|
||||
"social": {
|
||||
"displayInfo": false
|
||||
},
|
||||
"login": {}
|
||||
}),
|
||||
id<KcContextBase.LoginUpdatePassword>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-update-password.ftl",
|
||||
"username": "anUsername",
|
||||
"username": "anUsername"
|
||||
}),
|
||||
id<KcContextBase.LoginUpdateProfile>({
|
||||
...kcContextCommonMock,
|
||||
@ -374,21 +427,21 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
"username": "anUsername",
|
||||
"email": "foo@example.com",
|
||||
"firstName": "aFirstName",
|
||||
"lastName": "aLastName",
|
||||
},
|
||||
"lastName": "aLastName"
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LoginIdpLinkConfirm>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-idp-link-confirm.ftl",
|
||||
"idpAlias": "FranceConnect",
|
||||
"idpAlias": "FranceConnect"
|
||||
}),
|
||||
id<KcContextBase.LoginIdpLinkEmail>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "login-idp-link-email.ftl",
|
||||
"idpAlias": "FranceConnect",
|
||||
"brokerContext": {
|
||||
"username": "anUsername",
|
||||
},
|
||||
"username": "anUsername"
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LoginConfigTotp>({
|
||||
...kcContextCommonMock,
|
||||
@ -407,21 +460,38 @@ export const kcContextMocks: KcContextBase[] = [
|
||||
digits: 6,
|
||||
lookAheadWindow: 1,
|
||||
type: "totp",
|
||||
period: 30,
|
||||
},
|
||||
},
|
||||
period: 30
|
||||
}
|
||||
}
|
||||
}),
|
||||
id<KcContextBase.LogoutConfirm>({
|
||||
...kcContextCommonMock,
|
||||
"pageId": "logout-confirm.ftl",
|
||||
"url": {
|
||||
...kcContextCommonMock.url,
|
||||
"logoutConfirmAction": "Continuer?",
|
||||
"logoutConfirmAction": "Continuer?"
|
||||
},
|
||||
"client": {
|
||||
"clientId": "myApp",
|
||||
"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
|
||||
}
|
||||
})
|
||||
];
|
||||
|
@ -130,7 +130,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
|
||||
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
|
||||
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
|
||||
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
|
||||
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -149,7 +149,7 @@ const messages = {
|
||||
"invalidPasswordRegexPatternMessage": "Neplatné heslo: neshoduje se zadaným regulárním výrazem.",
|
||||
"invalidPasswordHistoryMessage": "Neplatné heslo: Nesmí se opakovat žádné z posledních {0} hesel.",
|
||||
"invalidPasswordBlacklistedMessage": "Neplatné heslo: heslo je na černé listině.",
|
||||
"invalidPasswordGenericMessage": "Neplatné heslo: nové heslo neodpovídá pravidlům hesla.",
|
||||
"invalidPasswordGenericMessage": "Neplatné heslo: nové heslo neodpovídá pravidlům hesla."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -149,7 +149,7 @@ const messages = {
|
||||
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: Es entspricht nicht dem Regex-Muster.",
|
||||
"invalidPasswordHistoryMessage": "Ungültiges Passwort: Es darf nicht einem der letzten {0} Passwörter entsprechen.",
|
||||
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Das Passwort steht auf der Blocklist (schwarzen Liste).",
|
||||
"invalidPasswordGenericMessge": "Ungültiges Passwort: Das neue Passwort verletzt die Passwort-Richtlinien.",
|
||||
"invalidPasswordGenericMessge": "Ungültiges Passwort: Das neue Passwort verletzt die Passwort-Richtlinien."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -318,7 +318,7 @@ const messages = {
|
||||
"openshift.scope.user_info": "User information",
|
||||
"openshift.scope.user_check-access": "User access information",
|
||||
"openshift.scope.user_full": "Full Access",
|
||||
"openshift.scope.list-projects": "List projects",
|
||||
"openshift.scope.list-projects": "List projects"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -130,7 +130,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
|
||||
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
|
||||
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
|
||||
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
|
||||
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -143,7 +143,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide: doit contenir au moins {0} caractère(s) spéciaux.",
|
||||
"invalidPasswordNotUsernameMessage": "Mot de passe invalide: ne doit pas être identique au nom d'utilisateur.",
|
||||
"invalidPasswordRegexPatternMessage": "Mot de passe invalide: ne valide pas l'expression rationnelle.",
|
||||
"invalidPasswordHistoryMessage": "Mot de passe invalide: ne doit pas être égal aux {0} derniers mots de passe.",
|
||||
"invalidPasswordHistoryMessage": "Mot de passe invalide: ne doit pas être égal aux {0} derniers mots de passe."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -303,7 +303,7 @@ const messages = {
|
||||
"openshift.scope.user_info": "Informazioni utente",
|
||||
"openshift.scope.user_check-access": "Informazioni per l'accesso dell'utente",
|
||||
"openshift.scope.user_full": "Accesso completo",
|
||||
"openshift.scope.list-projects": "Elenca progetti",
|
||||
"openshift.scope.list-projects": "Elenca progetti"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -317,7 +317,7 @@ const messages = {
|
||||
"openshift.scope.user_info": "ユーザー情報",
|
||||
"openshift.scope.user_check-access": "ユーザーアクセス情報",
|
||||
"openshift.scope.user_full": "フルアクセス",
|
||||
"openshift.scope.list-projects": "プロジェクトの一覧表示",
|
||||
"openshift.scope.list-projects": "プロジェクトの一覧表示"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -136,7 +136,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
|
||||
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
|
||||
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
|
||||
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
|
||||
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -136,7 +136,7 @@ const messages = {
|
||||
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
|
||||
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
|
||||
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
|
||||
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
|
||||
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -146,7 +146,7 @@ const messages = {
|
||||
"locale_nl": "Nederlands",
|
||||
"locale_pt-BR": "Português (Brasil)",
|
||||
"locale_ru": "Русский",
|
||||
"locale_zh-CN": "中文简体",
|
||||
"locale_zh-CN": "中文简体"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -133,7 +133,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter pelo menos {0} caractere(s) especial",
|
||||
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário",
|
||||
"invalidPasswordRegexPatternMessage": "Senha inválida: não corresponde ao padrão da expressão regular.",
|
||||
"invalidPasswordHistoryMessage": "Senha inválida: não pode ser igual a qualquer uma das {0} últimas senhas.",
|
||||
"invalidPasswordHistoryMessage": "Senha inválida: não pode ser igual a qualquer uma das {0} últimas senhas."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -136,7 +136,7 @@ const messages = {
|
||||
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.",
|
||||
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не удовлетворяет регулярному выражению.",
|
||||
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролями.",
|
||||
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.",
|
||||
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -173,7 +173,7 @@ const messages = {
|
||||
"resourcesSharedWithMe": "Zdroje zdieľané so mnou",
|
||||
"permissionRequestion": "Žiadosti o povolenie",
|
||||
"permission": "Oprávnenie",
|
||||
"shares": "podiel (y)",
|
||||
"shares": "podiel (y)"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -132,7 +132,7 @@ const messages = {
|
||||
"invalidPasswordNotUsernameMessage": "Ogiltigt lösenord: Får inte vara samma som användarnamnet.",
|
||||
"invalidPasswordRegexPatternMessage": "Ogiltigt lösenord: matchar inte kravet för lösenordsmönster.",
|
||||
"invalidPasswordHistoryMessage": "Ogiltigt lösenord: Får inte vara samma som de senaste {0} lösenorden.",
|
||||
"invalidPasswordGenericMessage": "Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn.",
|
||||
"invalidPasswordGenericMessage": "Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -302,7 +302,7 @@ const messages = {
|
||||
"addTeam": "Kaynağınızı paylaşmak için ekip ekleyin",
|
||||
"myPermissions": "İzinlerim",
|
||||
"waitingforApproval": "Onay bekleniyor",
|
||||
"anyPermission": "Herhangi bir izin",
|
||||
"anyPermission": "Herhangi bir izin"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -148,7 +148,7 @@ const messages = {
|
||||
"locale_lt": "Lietuvių",
|
||||
"locale_pt-BR": "Português (Brasil)",
|
||||
"locale_ru": "Русский",
|
||||
"locale_zh-CN": "中文简体",
|
||||
"locale_zh-CN": "中文简体"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -10,7 +10,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
|
||||
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
|
||||
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
|
||||
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -12,7 +12,7 @@ const messages = {
|
||||
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
|
||||
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
|
||||
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
|
||||
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht.",
|
||||
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -37,7 +37,7 @@ const messages = {
|
||||
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
|
||||
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
|
||||
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -10,7 +10,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
|
||||
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
|
||||
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
|
||||
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
|
||||
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -10,7 +10,7 @@ const messages = {
|
||||
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
|
||||
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
|
||||
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.",
|
||||
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe.",
|
||||
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -30,7 +30,7 @@ const messages = {
|
||||
"設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
|
||||
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。",
|
||||
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。",
|
||||
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。",
|
||||
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -24,7 +24,7 @@ const messages = {
|
||||
"Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
|
||||
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.",
|
||||
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -27,7 +27,7 @@ const messages = {
|
||||
"Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
|
||||
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.",
|
||||
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -15,7 +15,7 @@ const messages = {
|
||||
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
|
||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
|
||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
|
||||
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only",
|
||||
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -18,7 +18,7 @@ const messages = {
|
||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
|
||||
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo",
|
||||
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos",
|
||||
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos",
|
||||
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -25,7 +25,7 @@ const messages = {
|
||||
"Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
|
||||
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.",
|
||||
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI.",
|
||||
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -25,7 +25,7 @@ const messages = {
|
||||
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
|
||||
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
|
||||
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
|
||||
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。",
|
||||
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -34,7 +34,7 @@ const messages = {
|
||||
"eventUpdateTotpSubject": "Actualització de OTP",
|
||||
"eventUpdateTotpBody": "OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
|
||||
"eventUpdateTotpBodyHtml":
|
||||
"<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
|
||||
"<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -62,7 +62,7 @@ const messages = {
|
||||
"linkExpirationFormatter.timePeriodUnit.days.1": "den",
|
||||
"linkExpirationFormatter.timePeriodUnit.days.2": "dny",
|
||||
"linkExpirationFormatter.timePeriodUnit.days.3": "dny",
|
||||
"linkExpirationFormatter.timePeriodUnit.days.4": "dny",
|
||||
"linkExpirationFormatter.timePeriodUnit.days.4": "dny"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -38,7 +38,7 @@ const messages = {
|
||||
"<p>Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
||||
"eventUpdateTotpSubject": "OTP Aktualisiert",
|
||||
"eventUpdateTotpBody": "OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
|
||||
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
||||
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -54,7 +54,7 @@ const messages = {
|
||||
"linkExpirationFormatter.timePeriodUnit.days": "days",
|
||||
"linkExpirationFormatter.timePeriodUnit.days.1": "day",
|
||||
"emailVerificationBodyCode": "Please verify your email address by entering in the following code.\n\n{0}\n\n.",
|
||||
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>",
|
||||
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -33,7 +33,7 @@ const messages = {
|
||||
"eventUpdateTotpSubject": "Actualización de OTP",
|
||||
"eventUpdateTotpBody": "OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
|
||||
"eventUpdateTotpBodyHtml":
|
||||
"<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
|
||||
"<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
@ -37,7 +37,7 @@ const messages = {
|
||||
"eventUpdateTotpBody":
|
||||
"Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
|
||||
"eventUpdateTotpBodyHtml":
|
||||
"<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
|
||||
"<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user