Compare commits

..

120 Commits

Author SHA1 Message Date
fa8e119514 fix(deps): update garronej_modules_update 2023-02-01 15:16:03 +00:00
677cb5c330 Update README.md 2023-01-27 15:46:30 +01:00
6e74c79bfe Merge pull request #233 from InseeFrLab/windows_compat
Windows compat
2023-01-27 15:45:31 +01:00
54474f5908 Merge branch 'main' into windows_compat 2023-01-27 15:45:25 +01:00
99cc0f519b Rollback via update 2023-01-27 01:13:16 +01:00
92a01f89ef Bump version 2023-01-27 01:12:46 +01:00
fd83a0c743 keycloak test script: use env to launch bash
This minor change allows users to use the latest version of `bash` on Mac OS.

`/bin/bash` is very old on Mac OS, and many users will have `bash` from MacPorts or Homebrew. This change allows such users to be able to use the newer `bash` installation.
2023-01-27 01:12:46 +01:00
988e46c875 fix(deps): update dependency powerhooks to ^0.22.0 2023-01-27 01:12:46 +01:00
f081c2fc20 Update dependency powerhooks to ^0.21.0 2023-01-27 01:12:46 +01:00
b4b376a1a5 Relase candidate 2023-01-27 01:09:11 +01:00
0db4179d47 fmt 2023-01-27 00:32:41 +01:00
795b7c6234 Update README.md 2023-01-27 00:27:46 +01:00
091b9a57f5 Bump version 2023-01-27 00:22:28 +01:00
564e1422ac Merge pull request #226 from lordvlad/fix-win-build
windows compatibility
2023-01-27 00:20:12 +01:00
8ed4ed3fc4 Update src/bin/tools/downloadAndUnzip.ts 2023-01-26 22:29:51 +01:00
29fe4566a7 style: remove unused variable 2023-01-26 22:20:16 +01:00
ae3bfb28ed refactor: follow suggesion to make methods read a bit more sync 2023-01-26 22:00:31 +01:00
14aab97d8a Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2023-01-26 10:50:01 +01:00
52d7a47cd7 Bump version 2023-01-26 10:48:57 +01:00
f338dcbeed #232 2023-01-26 10:48:24 +01:00
dcec058a22 Bump version 2023-01-23 02:57:12 +01:00
2bdc6b156b Merge pull request #231 from rome-user/patch-1
keycloak test script: use env to launch bash
2023-01-23 02:56:32 +01:00
84ca9e6b81 keycloak test script: use env to launch bash
This minor change allows users to use the latest version of `bash` on Mac OS.

`/bin/bash` is very old on Mac OS, and many users will have `bash` from MacPorts or Homebrew. This change allows such users to be able to use the newer `bash` installation.
2023-01-22 17:28:27 -08:00
11cb0fd2db Merge pull request #230 from InseeFrLab/renovate_main-garronej_modules_update
Update dependency powerhooks to ^0.22.0 (main)
2023-01-18 00:58:38 +01:00
3f620ffb6f fix(deps): update dependency powerhooks to ^0.22.0 2023-01-16 18:23:50 +00:00
1a0e05d073 rewrite download and unzip to use nodejs native methods 2023-01-16 14:42:20 +01:00
a4d2de23a1 Update dependency powerhooks to ^0.21.0 2023-01-16 02:38:07 +00:00
85cecc9811 fix(build): _properly_ fix bin paths 2023-01-12 22:56:02 +01:00
9899f742a8 fix(build): also rewrite bin paths in dist/package.json 2023-01-12 22:51:48 +01:00
b5484740b7 fix(build): remove dependency on *nix chown 2023-01-12 22:27:22 +01:00
016b15b437 Bump version 2023-01-10 09:05:33 +01:00
6fb936798e https://github.com/InseeFrLab/keycloakify/issues/217#issuecomment-1376849781 2023-01-10 09:05:19 +01:00
a692b87843 Bump version 2023-01-08 15:01:12 +01:00
19663885a4 Merge pull request #222 from InseeFrLab/pr/lordvlad/221
Pr/lordvlad/221
2023-01-08 14:59:22 +01:00
49b87777f9 Fmt 2023-01-08 14:57:18 +01:00
d4523bb1e6 Merge branch 'main' into patch-1 2023-01-08 12:22:14 +01:00
e3200899e2 fix: replace rm system calls with nodejs native functions; closes #219 2023-01-08 12:19:41 +01:00
36c7a1ab9e Bump version 2023-01-08 00:14:01 +01:00
c54fbd5eca Merge pull request #218 from pedrodsRocha1/improve-secure-login-flow
fix - handle username and password login errors the same way
2023-01-08 00:03:16 +01:00
bbe828071e fix - handle username and password login errors the same 2023-01-06 15:41:01 +01:00
23f6c7db00 Update garronej_modules_update 2022-11-29 18:31:14 +00:00
b1ea9e7a71 Update dependency powerhooks to ^0.20.27 2022-11-16 05:21:45 +00:00
fb71d0e272 Update dependency powerhooks to ^0.20.26 2022-11-13 22:11:59 +00:00
fa72a29999 Bump version 2022-11-11 15:49:05 +01:00
af77b31d54 Update default test container to Keycloak 20.0.1 2022-11-11 15:48:32 +01:00
8280dace26 Update garronej_modules_update 2022-10-25 03:18:01 +00:00
ecaf1c7b7c Bump version 2022-10-16 00:51:11 +02:00
8702ec29a8 Drop dependency to @emotion/react 2022-10-16 00:49:49 +02:00
d8206434bc Bump version 2022-10-15 15:39:56 +02:00
c71c2a8710 Fix breaking change of 6.8 2022-10-15 15:39:32 +02:00
e55b881017 Bump version 2022-10-15 14:22:46 +02:00
ab906ec417 #192: fix bad logic in ftl to js script 2022-10-15 14:22:11 +02:00
0b1ff529f7 Update garronej_modules_update 2022-10-14 17:37:32 +00:00
85a6835748 Fix CI 2022-10-13 12:10:08 +02:00
259271bc0f Update CI 2022-10-13 12:07:32 +02:00
b7bc0f178b Bump version 2022-10-13 12:03:24 +02:00
688455d0aa #191: Enable to only customize the Template 2022-10-13 11:58:31 +02:00
3c96d2ea42 Update garronej_modules_update 2022-10-13 01:16:45 +00:00
ab81481e5a Update garronej_modules_update 2022-10-11 05:10:22 +00:00
a429ad5dcf Bump version 2022-10-06 20:43:42 +02:00
5e1c5b510b Merge pull request #185 from Mstrodl/feature/mstrodl/webauthn-authenticate
Add support for `webauthn-authenticate.ftl`
2022-10-06 20:41:31 +02:00
9e63183f4b WebauthnAuthenticate: refactor authentication flow
Lots of weird syntax here, and we were using `useCallback` rather than
`useConstCallback`.
2022-10-06 14:25:04 -04:00
b1e740f026 Bump version 2022-10-06 01:09:00 +02:00
ce4ea55438 Add a big red warning when kcContext mock is enabled 2022-10-06 01:08:07 +02:00
18ab7cd22f Bump version 2022-10-06 00:37:51 +02:00
8807743daf Ignore mock when in Keycloak: https://github.com/InseeFrLab/keycloakify/discussions/186#discussioncomment-3809320 2022-10-06 00:36:46 +02:00
aad50377ff Merge pull request #187 from InseeFrLab/renovate_main-garronej_modules_update
Update dependency tss-react to ^4.3.4 (main)
2022-10-06 00:01:38 +02:00
4b3ae58ea7 Add support for webauthn-authenticate.ftl
Wow! This one sucks. Certainly more in need of review compared to
`login-username.ftl` and `login-password.ftl`...
2022-10-05 02:54:03 -04:00
ce2c68ecc9 Update dependency tss-react to ^4.3.4 2022-10-04 19:43:22 +00:00
0c155a7a2e Bump version 2022-10-04 21:42:47 +02:00
afddfe8b58 Merge pull request #184 from Mstrodl/feature/mstrodl/login-password
Add support for `login-password.ftl`
2022-10-04 21:38:55 +02:00
5fa0915271 Update changelog 2022-10-04 21:34:42 +02:00
6a0a170b17 Add support for login-password.ftl 2022-10-04 15:01:08 -04:00
4dde5b6e45 Bump version 2022-10-04 18:48:22 +02:00
4b93a1cb9e Merge pull request #183 from Mstrodl/feature/mstrodl/login-username 2022-10-04 18:43:02 +02:00
e3a0639a0c Add login-username support
Shown for the "Username Form" login step.

I would also like to work on:
- `login-password.ftl`
- `webauthn-authenticate.ftl`

But first want to see opinions on this!
2022-10-04 11:22:03 -04:00
4d3220820b Update dependency tss-react to ^4.3.3 2022-10-04 03:50:15 +00:00
a4ac9fb0f3 Update garronej_modules_update 2022-10-01 17:47:21 +00:00
1ff79ecf07 Update garronej_modules_update 2022-09-29 08:53:47 +00:00
1166b16420 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-27 21:35:52 +02:00
213224942f Bump version 2022-09-27 21:35:16 +02:00
ff16e66275 #177: Provide a simple way to disable fetching of default resources 2022-09-27 21:30:33 +02:00
3c338e983f Update garronej_modules_update 2022-09-14 16:33:35 +00:00
2c11ba6520 Bump version 2022-09-13 10:50:36 +02:00
9a21656706 Apply #164 on v6 2022-09-13 10:49:20 +02:00
e96ee5ba53 Bump version 2022-09-12 23:49:14 +02:00
b421633a8a Default test container use 19.0.1 2022-09-12 23:48:34 +02:00
e2e0d62560 Cleaner npm scripts 2022-09-11 01:53:56 +02:00
c71fb06940 Update garronej_modules_update 2022-09-10 20:14:00 +00:00
e2171af99c Bump version 2022-09-09 17:24:58 +02:00
8cebf049d4 Render Markdown in Terms 2022-09-09 17:24:43 +02:00
ef139ed1cc Bump version 2022-09-09 14:37:41 +02:00
d717de006a Update kcContext def 2022-09-09 14:37:27 +02:00
a44f091878 Bump version 2022-09-09 12:56:31 +02:00
1b37ba5339 Feat idp-review-user-profile.ftl #164 2022-09-09 12:56:09 +02:00
bbaa90e997 Rename misnamed default exports, removing doInsertPasswordFields 2022-09-09 10:24:50 +02:00
86e6c4a419 Bump version (changelog ignore) 2022-09-09 02:10:18 +02:00
4159883791 Feature update-user-profile.ftl #164 2022-09-09 02:07:49 +02:00
d8b00da3a1 Update tss-react 2022-09-08 15:27:41 +02:00
a24945bc1b Bump version 2022-09-08 15:18:21 +02:00
158759493f Merge pull request #170 from Tasyp/feature/silent
feat: add silent flag
2022-09-08 15:17:45 +02:00
36e32d6ddc Update src/bin/download-builtin-keycloak-theme.ts 2022-09-08 15:13:21 +02:00
84908e2ec0 Update src/bin/generate-i18n-messages.ts 2022-09-08 15:13:15 +02:00
a2dc51d811 Update src/bin/create-keycloak-email-directory.ts 2022-09-08 15:13:09 +02:00
fb3b0e2c29 fix: make sure external assets flag is a boolean 2022-09-08 15:46:27 +03:00
1a3e4c68bb test: update tests to include silent flag 2022-09-08 15:43:03 +03:00
11b2342da0 feat: add silent flag 2022-09-08 13:52:10 +03:00
80d4a808d3 Update readme 2022-09-07 13:45:34 +02:00
da4146eb59 Bump version (changelog ignore) 2022-09-07 13:40:18 +02:00
a0be35db8b Fix compat with StrictMode react 18 2022-09-07 13:39:54 +02:00
14db9cd523 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 12:02:35 +02:00
0c315385dd Update fix tests 2022-09-07 12:00:23 +02:00
c0a0eb02fb Remove Open collectivity 2022-09-07 11:59:42 +02:00
ee407c32ad Create FUNDING.yaml 2022-09-07 11:53:18 +02:00
9262d21829 Bump version 2022-09-07 11:50:41 +02:00
a13f710325 Important fix for --external-assets 2022-09-07 11:50:24 +02:00
eac1a6036f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 11:26:18 +02:00
987f3d7586 Bump version (changelog ignore) 2022-09-07 11:26:10 +02:00
875322669c Rename isAppAndKeycloakServerSharingSameDomain to areAppAndKeycloakServerSharingSameDomain #145 2022-09-07 11:25:46 +02:00
33a264b3d0 Update README.md 2022-09-07 00:32:38 +02:00
59 changed files with 2551 additions and 914 deletions

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

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

View File

@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }} if: ${{ !github.event.created && github.repository != 'garronej/ts-ci' }}
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v3
- uses: actions/setup-node@v2.1.3 - uses: actions/setup-node@v3
- uses: bahmutov/npm-install@v1 - 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: | run: |
PACKAGE_MANAGER=npm PACKAGE_MANAGER=npm
if [ -f "./yarn.lock" ]; then if [ -f "./yarn.lock" ]; then
@ -24,22 +24,21 @@ jobs:
fi fi
$PACKAGE_MANAGER run format:check $PACKAGE_MANAGER run format:check
test: test:
runs-on: macos-10.15 runs-on: ${{ matrix.os }}
needs: test_lint needs: test_lint
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy: strategy:
matrix: matrix:
node: [ '15', '14' ] node: [ '14', '15' ,'16', '17' ]
name: Test with Node v${{ matrix.node }} os: [ windows-latest, ubuntu-latest ]
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
steps: steps:
- name: Tell if project is using npm or yarn - name: Tell if project is using npm or yarn
id: step1 id: step1
uses: garronej/ts-ci@v1.1.7 uses: garronej/ts-ci@v2.0.2
with: with:
action_name: tell_if_project_uses_npm_or_yarn action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v3
- uses: actions/setup-node@v2.1.3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node }} node-version: ${{ matrix.node }}
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
@ -65,9 +64,9 @@ jobs:
from_version: ${{ steps.step1.outputs.from_version }} from_version: ${{ steps.step1.outputs.from_version }}
to_version: ${{ steps.step1.outputs.to_version }} to_version: ${{ steps.step1.outputs.to_version }}
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_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: steps:
- uses: garronej/ts-ci@v1.1.7 - uses: garronej/ts-ci@v2.0.2
id: step1 id: step1
with: with:
action_name: is_package_json_version_upgraded action_name: is_package_json_version_upgraded
@ -75,13 +74,13 @@ jobs:
create_github_release: create_github_release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
# We create a release only if the version have been upgraded and we are on a default branch # We create a release only if the version have been upgraded and we are on the main branch
# PR on the default branch can release beta but not real release # or if we are on a branch of the repo that has an PR open on main.
if: | if: |
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' && needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
( (
github.event_name == 'push' || 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: needs:
- check_if_version_upgraded - check_if_version_upgraded
@ -93,7 +92,7 @@ jobs:
target_commitish: ${{ github.head_ref || github.ref }} target_commitish: ${{ github.head_ref || github.ref }}
generate_release_notes: true generate_release_notes: true
draft: false 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: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@ -103,12 +102,11 @@ jobs:
- create_github_release - create_github_release
- check_if_version_upgraded - check_if_version_upgraded
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v3
with: with:
ref: ${{ github.ref }} ref: ${{ github.ref }}
- uses: actions/setup-node@v2.1.3 - uses: actions/setup-node@v3
with: with:
node-version: '15'
registry-url: https://registry.npmjs.org/ registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1 - uses: bahmutov/npm-install@v1
- run: | - run: |
@ -117,7 +115,7 @@ jobs:
PACKAGE_MANAGER=yarn PACKAGE_MANAGER=yarn
fi fi
$PACKAGE_MANAGER run build $PACKAGE_MANAGER run build
- run: npx -y -p denoify@1.0.2 enable_short_npm_import_path - run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
env: env:
DRY_RUN: "0" DRY_RUN: "0"
- name: Publishing on NPM - name: Publishing on NPM
@ -131,11 +129,11 @@ jobs:
false false
fi fi
EXTRA_ARGS="" EXTRA_ARGS=""
if [ "$IS_BETA" = "true" ]; then if [ "$IS_PRE_RELEASE" = "true" ]; then
EXTRA_ARGS="--tag beta" EXTRA_ARGS="--tag next"
fi fi
npm publish $EXTRA_ARGS npm publish $EXTRA_ARGS
env: env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }} 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 }}

View File

@ -27,7 +27,14 @@
<a href="https://www.keycloakify.dev">Home</a> <a href="https://www.keycloakify.dev">Home</a>
- -
<a href="https://docs.keycloakify.dev">Documentation</a> <a href="https://docs.keycloakify.dev">Documentation</a>
</p> </p>
<p align="center"> ---- Project starter / Demo setup ---- </p>
<p align="center">
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
-
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
</p>
<p align="center"> ---- </p>
</p> </p>
@ -37,11 +44,43 @@
</p> </p>
> 🗣 V6 have been released 🎉 > 🗣 V6 have been released 🎉
> [It features major improvements](/#600). > [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6). > Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
# Changelog highlights # Changelog highlights
## 6.10.0
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/226)). WSL is no longer required 🎉
## 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 ## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded. - Bundle size drastically reduced, locals and component dynamically loaded.

20
package.json Executable file → Normal file
View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "6.0.0", "version": "6.10.1",
"description": "Keycloak theme generator for Reacts app", "description": "Keycloak theme generator for Reacts app",
"repository": { "repository": {
"type": "git", "type": "git",
@ -13,7 +13,8 @@
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/", "build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js", "grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl", "copy-files": "copyfiles -u 1 src/**/*.ftl",
"test": "yarn build:test && node dist_test/test/bin && node dist_test/test/lib", "pretest": "yarn build:test",
"test": "node dist_test/test/bin && node dist_test/test/lib",
"generate-messages": "node dist/bin/generate-i18n-messages.js", "generate-messages": "node dist/bin/generate-i18n-messages.js",
"link_in_test_app": "node dist/bin/link_in_test_app.js", "link_in_test_app": "node dist/bin/link_in_test_app.js",
"_format": "prettier '**/*.{ts,tsx,json,md}'", "_format": "prettier '**/*.{ts,tsx,json,md}'",
@ -54,12 +55,12 @@
], ],
"homepage": "https://github.com/garronej/keycloakify", "homepage": "https://github.com/garronej/keycloakify",
"peerDependencies": { "peerDependencies": {
"@emotion/react": "^11.4.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0" "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}, },
"devDependencies": { "devDependencies": {
"@emotion/react": "^11.4.1", "@babel/core": "^7.0.0",
"@types/memoizee": "^0.4.7", "@types/memoizee": "^0.4.7",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25", "@types/node": "^17.0.25",
"@types/react": "18.0.9", "@types/react": "18.0.9",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
@ -69,21 +70,24 @@
"properties-parser": "^0.3.1", "properties-parser": "^0.3.1",
"react": "18.1.0", "react": "18.1.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"@emotion/react": "^11.10.4",
"typescript": "^4.2.3" "typescript": "^4.2.3"
}, },
"dependencies": { "dependencies": {
"@octokit/rest": "^18.12.0", "@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2", "cli-select": "^1.1.2",
"evt": "^2.4.0", "evt": "^2.4.13",
"memoizee": "^0.4.15", "memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.2", "minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"powerhooks": "^0.20.15", "powerhooks": "^0.22.1",
"react-markdown": "^5.0.3", "react-markdown": "^5.0.3",
"rfc4648": "^1.5.2",
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"tsafe": "^1.0.1", "tsafe": "^1.4.2",
"tss-react": "^4.1.1", "tss-react": "4.4.1-rc.0",
"zod": "^3.17.10" "zod": "^3.17.10"
} }
} }

View File

@ -13,11 +13,11 @@
"packageRules": [ "packageRules": [
{ {
"packagePatterns": ["*"], "packagePatterns": ["*"],
"excludePackagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"], "excludePackagePatterns": ["powerhooks", "tsafe", "evt"],
"enabled": false "enabled": false
}, },
{ {
"packagePatterns": ["tss-react", "powerhooks", "tsafe", "evt"], "packagePatterns": ["powerhooks", "tsafe", "evt"],
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"automerge": true, "automerge": true,
"automergeType": "branch", "automergeType": "branch",

View File

@ -6,11 +6,15 @@ import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase"; import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion"; import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs"; import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
if (require.main === module) { if (require.main === module) {
(async () => { (async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
if (fs.existsSync(keycloakThemeEmailDirPath)) { if (fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`); logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1); process.exit(-1);
} }
@ -21,7 +25,8 @@ if (require.main === module) {
downloadBuiltinKeycloakTheme({ downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath "destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
}); });
transformCodebase({ transformCodebase({
@ -29,7 +34,7 @@ if (require.main === module) {
"destDirPath": keycloakThemeEmailDirPath "destDirPath": keycloakThemeEmailDirPath
}); });
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`); logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true }); fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})(); })();

View File

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

View File

@ -4,7 +4,8 @@ import { join as pathJoin, relative as pathRelative, dirname as pathDirname } fr
import { crawl } from "./tools/crawl"; import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot"; import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm"; import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files, //NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version. // update the version array for generating for newer version.
@ -12,16 +13,20 @@ import { rm_rf, rm_r } from "./tools/rm";
//@ts-ignore //@ts-ignore
const propertiesParser = require("properties-parser"); const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) { for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
console.log({ keycloakVersion }); logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44"); const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
rm_rf(tmpDirPath); fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
downloadBuiltinKeycloakTheme({ downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
isSilent
}); });
type Dictionary = { [idiomId: string]: string }; type Dictionary = { [idiomId: string]: string };
@ -48,7 +53,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
}); });
} }
rm_r(tmpDirPath); fs.rmSync(tmpDirPath, { recursive: true, force: true });
Object.keys(record).forEach(pageType => { Object.keys(record).forEach(pageType => {
const recordForPageType = record[pageType]; const recordForPageType = record[pageType];
@ -75,7 +80,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
) )
); );
console.log(`${filePath} wrote`); logger.log(`${filePath} wrote`);
}); });
}); });
} }

View File

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

View File

@ -32,60 +32,82 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
"printIfExists": function (fieldName, x) { "printIfExists": function (fieldName, x) {
<#if !messagesPerField?? > <#if !messagesPerField?? >
return undefined; 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> </#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) { "existsError": function (fieldName) {
<#if !messagesPerField?? > <#if !messagesPerField?? >
return false; 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> </#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) { "get": function (fieldName) {
<#if !messagesPerField?? > <#if !messagesPerField?? >
return ''; 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> </#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) { "exists": function (fieldName) {
<#if !messagesPerField?? > <#if !messagesPerField?? >
return false; 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> </#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> <#list object as array_item>
<#if !array_item??>
<#local out_seq += ["null,"]>
<#continue>
</#if>
<#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])> <#local rec_out = ftl_object_to_js_code_declaring_an_object(array_item, path + [ i ])>
<#local i = i + 1> <#local i = i + 1>

View File

@ -13,6 +13,9 @@ import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java // https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [ export const pageIds = [
"login.ftl", "login.ftl",
"login-username.ftl",
"login-password.ftl",
"webauthn-authenticate.ftl",
"register.ftl", "register.ftl",
"register-user-profile.ftl", "register-user-profile.ftl",
"info.ftl", "info.ftl",
@ -27,7 +30,9 @@ export const pageIds = [
"login-idp-link-email.ftl", "login-idp-link-email.ftl",
"login-page-expired.ftl", "login-page-expired.ftl",
"login-config-totp.ftl", "login-config-totp.ftl",
"logout-confirm.ftl" "logout-confirm.ftl",
"update-user-profile.ftl",
"idp-review-user-profile.ftl"
] as const; ] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -46,11 +51,11 @@ export namespace BuildOptionsLike {
}; };
export type SameDomain = CommonExternalAssets & { export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true; areAppAndKeycloakServerSharingSameDomain: true;
}; };
export type DifferentDomains = CommonExternalAssets & { export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false; areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string; urlOrigin: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -76,7 +81,7 @@ export function generateFtlFilesCodeFactory(params: {
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: { fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) { if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements; break fix_imports_statements;
} }

View File

@ -30,12 +30,12 @@ export function generateJavaStackFiles(params: {
doBundlesEmailTemplate doBundlesEmailTemplate
} = params; } = params;
const artefactId = `${themeName}-keycloak-theme`;
{ {
const { pomFileCode } = (function generatePomFileCode(): { const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string; pomFileCode: string;
} { } {
const artefactId = `${themeName}-keycloak-theme`;
const pomFileCode = [ const pomFileCode = [
`<?xml version="1.0"?>`, `<?xml version="1.0"?>`,
`<project xmlns="http://maven.apache.org/POM/4.0.0"`, `<project xmlns="http://maven.apache.org/POM/4.0.0"`,
@ -84,6 +84,6 @@ export function generateJavaStackFiles(params: {
} }
return { return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`) "jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artefactId}-${version}.jar`)
}; };
} }

View File

@ -5,12 +5,12 @@ import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFrom
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode"; import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl"; import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme"; import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import * as child_process from "child_process";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath"; import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside"; import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect"; import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -19,6 +19,7 @@ export namespace BuildOptionsLike {
themeName: string; themeName: string;
extraPages?: string[]; extraPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
isSilent: boolean;
}; };
export type Standalone = Common & { export type Standalone = Common & {
@ -34,11 +35,11 @@ export namespace BuildOptionsLike {
}; };
export type SameDomain = CommonExternalAssets & { export type SameDomain = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: true; areAppAndKeycloakServerSharingSameDomain: true;
}; };
export type DifferentDomains = CommonExternalAssets & { export type DifferentDomains = CommonExternalAssets & {
isAppAndKeycloakServerSharingSameDomain: false; areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string; urlOrigin: string;
urlPathname: string | undefined; urlPathname: string | undefined;
}; };
@ -51,15 +52,16 @@ export namespace BuildOptionsLike {
assert<typeof buildOptions extends BuildOptionsLike ? true : false>(); assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
} }
export function generateKeycloakThemeResources(params: { export async function generateKeycloakThemeResources(params: {
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string; keycloakThemeBuildingDirPath: string;
keycloakThemeEmailDirPath: string; keycloakThemeEmailDirPath: string;
keycloakVersion: string; keycloakVersion: string;
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
}): { doBundlesEmailTemplate: boolean } { }): Promise<{ doBundlesEmailTemplate: boolean }> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params; const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login"); const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {}; let allCssGlobalsToDefine: Record<string, string> = {};
@ -97,7 +99,7 @@ export function generateKeycloakThemeResources(params: {
} }
if (/\.js?$/i.test(filePath)) { if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.isAppAndKeycloakServerSharingSameDomain) { if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined; return undefined;
} }
@ -117,7 +119,7 @@ export function generateKeycloakThemeResources(params: {
email: { email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) { if (!fs.existsSync(keycloakThemeEmailDirPath)) {
console.log( logger.log(
[ [
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`, `Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈` `To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
@ -152,9 +154,10 @@ export function generateKeycloakThemeResources(params: {
{ {
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd"); const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
downloadBuiltinKeycloakTheme({ await downloadBuiltinKeycloakTheme({
keycloakVersion, keycloakVersion,
"destDirPath": tmpDirPath "destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
}); });
const themeResourcesDirPath = pathJoin(themeDirPath, "resources"); const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
@ -186,8 +189,7 @@ export function generateKeycloakThemeResources(params: {
); );
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8")); fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
fs.rmSync(tmpDirPath, { recursive: true, force: true });
child_process.execSync(`rm -r ${tmpDirPath}`);
} }
fs.writeFileSync( fs.writeFileSync(

View File

@ -34,7 +34,7 @@ export function generateStartKeycloakTestingContainer(params: {
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename), pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from( Buffer.from(
[ [
"#!/bin/bash", "#!/usr/bin/env bash",
"", "",
`docker rm ${containerName} || true`, `docker rm ${containerName} || true`,
"", "",

View File

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

View File

@ -57,7 +57,7 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
: ` : `
var p= ""; var p= "";
Object.defineProperty(${n}, "p", { Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : p; }, get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;} set: function (value){ p = value;}
}); });
` `
@ -73,13 +73,13 @@ export function replaceImportsFromStaticInJsCode(params: { jsCode: string; build
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) => .replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/` ? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group}) + "static/` : `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
) )
//TODO: Write a test case for this //TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) => .replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},` ? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}" : ${group2}) + ${group3},` : `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
); );
return { fixedJsCode }; return { fixedJsCode };

View File

@ -15,7 +15,8 @@ fs.writeFileSync(
return { return {
...packageJsonParsed, ...packageJsonParsed,
"main": packageJsonParsed["main"].replace(/^dist\//, ""), "main": packageJsonParsed["main"].replace(/^dist\//, ""),
"types": packageJsonParsed["types"].replace(/^dist\//, "") "types": packageJsonParsed["types"].replace(/^dist\//, ""),
"bin": Object.fromEntries(Object.entries<string>(packageJsonParsed["bin"]).map(([k, v]) => [k, v.replace(/^dist\//, "")]))
}; };
})(), })(),
null, null,
@ -43,7 +44,8 @@ const commonThirdPartyDeps = (() => {
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home"); 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 execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
const { targetModuleName, cwd } = params; const { targetModuleName, cwd } = params;

View File

@ -0,0 +1,15 @@
import parseArgv from "minimist";
export type CliOptions = {
isSilent: boolean;
hasExternalAssets: boolean;
};
export const getCliOptions = (processArgv: string[]): CliOptions => {
const argv = parseArgv(processArgv);
return {
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
};
};

View File

@ -1,75 +1,289 @@
import { basename as pathBasename, join as pathJoin } from "path"; import { dirname as pathDirname, basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process"; import { createReadStream, createWriteStream, unlinkSync } from "fs";
import * as fs from "fs"; import { stat, mkdir, unlink, readFile, writeFile } from "fs/promises";
import { transformCodebase } from "./transformCodebase"; import { transformCodebase } from "./transformCodebase";
import { rm, rm_rf } from "./rm"; import { createHash } from "crypto";
import * as crypto from "crypto"; import http from "http";
import https from "https";
import { createInflateRaw } from "zlib";
/** assert url ends with .zip */ import type { Readable } from "stream";
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string; cacheDirPath: string }) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const extractDirPath = pathJoin( function hash(s: string) {
cacheDirPath, return createHash("sha256").update(s).digest("hex");
`_${crypto.createHash("sha256").update(JSON.stringify({ url, pathOfDirToExtractInArchive })).digest("hex").substring(0, 15)}` }
);
fs.mkdirSync(cacheDirPath, { "recursive": true }); async function maybeReadFile(path: string) {
try {
const { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath } = (() => { return await readFile(path, "utf-8");
const filePath = pathJoin(cacheDirPath, "isSuccessByExtractDirPath.json"); } catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return undefined;
type IsSuccessByExtractDirPath = Record<string, boolean | undefined>; throw error;
function readIsSuccessByExtractDirPath(): IsSuccessByExtractDirPath {
if (!fs.existsSync(filePath)) {
return {};
}
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
});
rm_rf(extractDirPath);
fs.mkdirSync(extractDirPath);
const zipFileBasename = pathBasename(url);
execSync(`curl -L ${url} -o ${zipFileBasename}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath
});
rm(zipFileBasename, { "cwd": extractDirPath });
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: true
});
} }
}
transformCodebase({ async function maybeStat(path: string) {
"srcDirPath": pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive), try {
destDirPath return await stat(path);
} catch (error) {
if ((error as Error & { code: string }).code === "ENOENT") return undefined;
throw error;
}
}
/**
* Download a file from `url` to `dir`. Will try to avoid downloading existing
* files by using an `{hash(url)}.etag` file. If this file exists, we add an
* etag headear, so server can tell us if file changed and we should re-download
* or if our file is up-to-date.
*
* Warning, this method assumes that the target filename can be extracted from
* url, content-disposition headers are ignored.
*
* If the target directory does not exist, it will be created.
*
* If the target file exists and is out of date, it will be overwritten.
* If the target file exists and there is no etag file, the target file will
* be overwritten.
*
* @param url download url
* @param dir target directory
* @returns promise for the full path of the downloaded file
*/
async function download(url: string, dir: string): Promise<string> {
await mkdir(dir, { recursive: true });
const filename = pathBasename(url);
const filepath = pathJoin(dir, filename);
// If downloaded file exists already and has an `.etag` companion file,
// read the etag from that file. This will avoid re-downloading the file
// if it is up to date.
const exists = await maybeStat(filepath);
const etagFilepath = pathJoin(dir, "_" + hash(url).substring(0, 15) + ".etag");
const etag = !exists ? undefined : await maybeReadFile(etagFilepath);
return new Promise((resolve, reject) => {
// use inner method to allow following redirects
function request(url1: URL) {
const headers: Record<string, string> = {};
if (etag) headers["If-None-Match"] = etag;
(url1.protocol === "https:" ? https : http).get(url1, { headers }, response => {
if (response.statusCode === 301 || response.statusCode === 302) {
// follow redirects
request(new URL(response.headers.location!!));
} else if (response.statusCode === 304) {
// up-to-date, resolve now
resolve(filepath);
} else if (response.statusCode !== 200) {
reject(new Error(`Request to ${url1} returned status ${response.statusCode}.`));
} else {
const fp = createWriteStream(filepath, { autoClose: true });
fp.on("err", e => {
fp.close();
unlinkSync(filepath);
reject(e);
});
fp.on("finish", async () => {
// when targetfile has been written, write etag file so that
// next time around we don't need to re-download
const responseEtag = response.headers.etag;
if (responseEtag) await writeFile(etagFilepath, responseEtag, "utf-8");
resolve(filepath);
});
response.pipe(fp);
}
});
}
request(new URL(url));
}); });
} }
/**
* @typedef
* @type MultiError = Error & { cause: Error[] }
*/
/**
* Extract the archive `zipFile` into the directory `dir`. If `archiveDir` is given,
* only that directory will be extracted, stripping the given path components.
*
* If dir does not exist, it will be created.
*
* If any archive file exists, it will be overwritten.
*
* Will unzip using all available nodejs worker threads.
*
* Will try to clean up extracted files on failure.
*
* If unpacking fails, will either throw an regular error, or
* possibly an `MultiError`, which contains a `cause` field with
* a number of root cause errors.
*
* Warning this method is not optimized for continuous reading of the zip
* archive, but is a trade-off between simplicity and allowing extraction
* of a single directory from the archive.
*
* @param zipFile the file to unzip
* @param dir the target directory
* @param archiveDir if given, unpack only files from this archive directory
* @throws {MultiError} error
* @returns Promise for a list of full file paths pointing to actually extracted files
*/
async function unzip(zipFile: string, dir: string, archiveDir?: string): Promise<string[]> {
await mkdir(dir, { recursive: true });
const promises: Promise<string>[] = [];
// Iterate over all files in the zip, skip files which are not in archiveDir,
// if given.
for await (const record of iterateZipArchive(zipFile)) {
const { path: recordPath, createReadStream: createRecordReadStream } = record;
const filePath = pathJoin(dir, recordPath);
const parent = pathDirname(filePath);
if (archiveDir && !recordPath.startsWith(archiveDir)) continue;
promises.push(
new Promise<string>(async (resolve, reject) => {
await mkdir(parent, { recursive: true });
// Pull the file out of the archive, write it to the target directory
const input = createRecordReadStream();
const output = createWriteStream(filePath);
output.on("error", e => reject(Object.assign(e, { filePath })));
output.on("finish", () => resolve(filePath));
input.pipe(output);
})
);
}
// Wait until _all_ files are either extracted or failed
const results = await Promise.allSettled(promises);
const success = results.filter(r => r.status === "fulfilled").map(r => (r as PromiseFulfilledResult<string>).value);
const failure = results.filter(r => r.status === "rejected").map(r => (r as PromiseRejectedResult).reason);
// If any extraction failed, try to clean up, then throw a MultiError,
// which has a `cause` field, containing a list of root cause errors.
if (failure.length) {
await Promise.all(success.map(path => unlink(path)));
await Promise.all(failure.map(e => e && e.path && unlink(e.path as string)));
const e = new Error("Failed to extract: " + failure.map(e => e.message).join(";"));
(e as any).cause = failure;
throw e;
}
return success;
}
/**
*
* @param file file to read
* @param start first byte to read
* @param end last byte to read
* @returns Promise of a buffer of read bytes
*/
async function readFileChunk(file: string, start: number, end: number): Promise<Buffer> {
const chunks: Buffer[] = [];
return new Promise((resolve, reject) => {
const stream = createReadStream(file, { start, end });
stream.on("error", e => reject(e));
stream.on("end", () => resolve(Buffer.concat(chunks)));
stream.on("data", chunk => chunks.push(chunk as Buffer));
});
}
type ZipRecord = {
path: string;
createReadStream: () => Readable;
compressionMethod: "deflate" | undefined;
};
type ZipRecordGenerator = AsyncGenerator<ZipRecord, void, unknown>;
/**
* Iterate over all records of a zipfile, and yield a ZipRecord.
* Use `record.createReadStream()` to actually read the file.
*
* Warning this method will only work with single-disk zip files.
* Warning this method may fail if the zip archive has an crazy amount
* of files and the central directory is not fully contained within the
* last 65k bytes of the zip file.
*
* @param zipFile
* @returns AsyncGenerator which will yield ZipRecords
*/
async function* iterateZipArchive(zipFile: string): ZipRecordGenerator {
// Need to know zip file size before we can do anything else
const { size } = await stat(zipFile);
const chunkSize = 65_535 + 22 + 1; // max comment size + end header size + wiggle
// Read last ~65k bytes. Zip files have an comment up to 65_535 bytes at the very end,
// before that comes the zip central directory end header.
let chunk = await readFileChunk(zipFile, size - chunkSize, size);
const unread = size - chunk.length;
let i = chunk.length - 4;
let found = false;
// Find central directory end header, reading backwards from the end
while (!found && i-- > 0) if (chunk[i] === 0x50 && chunk.readUInt32LE(i) === 0x06054b50) found = true;
if (!found) throw new Error("Not a zip file");
// This method will fail on a multi-disk zip, so bail early.
if (chunk.readUInt16LE(i + 4) !== 0) throw new Error("Multi-disk zip not supported");
let nFiles = chunk.readUint16LE(i + 10);
// Get the position of the central directory
const directorySize = chunk.readUint32LE(i + 12);
const directoryOffset = chunk.readUint32LE(i + 16);
if (directoryOffset === 0xffff_ffff) throw new Error("zip64 not supported");
if (directoryOffset > size) throw new Error(`Central directory offset ${directoryOffset} is outside file`);
i = directoryOffset - unread;
// If i < 0, it means that the central directory is not contained within `chunk`
if (i < 0) {
chunk = await readFileChunk(zipFile, directoryOffset, directoryOffset + directorySize);
i = 0;
}
// Now iterate the central directory records, yield an `ZipRecord` for every entry
while (nFiles-- > 0) {
// Check for marker bytes
if (chunk.readUInt32LE(i) !== 0x02014b50) throw new Error("No central directory record at position " + (unread + i));
const compressionMethod = ({ 8: "deflate" } as const)[chunk.readUint16LE(i + 10)];
const compressedFileSize = chunk.readUint32LE(i + 20);
const filenameLength = chunk.readUint16LE(i + 28);
const extraLength = chunk.readUint16LE(i + 30);
const commentLength = chunk.readUint16LE(i + 32);
// Start of thea actual content byte stream is after the 'local' record header,
// which is 30 bytes long plus filename and extra field
const start = chunk.readUint32LE(i + 42) + 30 + filenameLength + extraLength;
const end = start + compressedFileSize;
const filename = chunk.slice(i + 46, i + 46 + filenameLength).toString("utf-8");
const createRecordReadStream = () => {
const input = createReadStream(zipFile, { start, end });
if (compressionMethod === "deflate") {
const inflate = createInflateRaw();
input.pipe(inflate);
return inflate;
}
return input;
};
if (end > start) yield { path: filename, createReadStream: createRecordReadStream, compressionMethod };
// advance pointer to next central directory entry
i += 46 + filenameLength + extraLength + commentLength;
}
}
export async function downloadAndUnzip({
url,
destDirPath,
pathOfDirToExtractInArchive,
cacheDirPath
}: {
isSilent: boolean;
url: string;
destDirPath: string;
pathOfDirToExtractInArchive?: string;
cacheDirPath: string;
}) {
const downloadHash = hash(JSON.stringify({ url, pathOfDirToExtractInArchive })).substring(0, 15);
const extractDirPath = pathJoin(cacheDirPath, `_${downloadHash}`);
const zipFilepath = await download(url, cacheDirPath);
const zipMtime = (await stat(zipFilepath)).mtimeMs;
const unzipMtime = (await maybeStat(extractDirPath))?.mtimeMs;
if (!unzipMtime || zipMtime > unzipMtime) await unzip(zipFilepath, extractDirPath, pathOfDirToExtractInArchive);
const srcDirPath = pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive);
transformCodebase({ srcDirPath, destDirPath });
}

View File

@ -1,10 +1,17 @@
import { getProjectRoot } from "./getProjectRoot"; import { getProjectRoot } from "./getProjectRoot";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import * as child_process from "child_process"; import { constants } from "fs";
import * as fs from "fs"; import { chmod, stat } from "fs/promises";
Object.entries<string>(JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["bin"]).forEach(([, scriptPath]) => async () => {
child_process.execSync(`chmod +x ${scriptPath}`, { var { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
"cwd": getProjectRoot()
}) var promises = Object.values<string>(bin).map(async scriptPath => {
); const fullPath = pathJoin(getProjectRoot(), scriptPath);
const oldMode = (await stat(fullPath)).mode;
const newMode = oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH;
await chmod(fullPath, newMode);
});
await Promise.all(promises);
};

27
src/bin/tools/logger.ts Normal file
View File

@ -0,0 +1,27 @@
type LoggerOpts = {
force?: boolean;
};
type Logger = {
log: (message: string, opts?: LoggerOpts) => void;
warn: (message: string) => void;
error: (message: string) => void;
};
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
return {
log: (message, { force } = {}) => {
if (isSilent && !force) {
return;
}
console.log(message);
},
warn: message => {
console.warn(message);
},
error: message => {
console.error(message);
}
};
};

View File

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

View File

@ -1,18 +1,27 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Error = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Error; i18n: I18n } & KcProps) => { 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 { message, client } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("errorTitle")} headerNode={msg("errorTitle")}
formNode={ formNode={

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

View File

@ -1,11 +1,21 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import { assert } from "../tools/assert"; import { assert } from "../tools/assert";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Info; i18n: I18n } & KcProps) => { 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; const { msgStr, msg } = i18n;
assert(kcContext.message !== undefined); assert(kcContext.message !== undefined);
@ -14,8 +24,7 @@ const Info = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Inf
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>} headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
formNode={ formNode={

View File

@ -3,6 +3,8 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import { __unsafe_useI18n as useI18n } from "../i18n"; import { __unsafe_useI18n as useI18n } from "../i18n";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
const Login = lazy(() => import("./Login")); const Login = lazy(() => import("./Login"));
const Register = lazy(() => import("./Register")); const Register = lazy(() => import("./Register"));
@ -13,6 +15,9 @@ const LoginResetPassword = lazy(() => import("./LoginResetPassword"));
const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail")); const LoginVerifyEmail = lazy(() => import("./LoginVerifyEmail"));
const Terms = lazy(() => import("./Terms")); const Terms = lazy(() => import("./Terms"));
const LoginOtp = lazy(() => import("./LoginOtp")); 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 LoginUpdatePassword = lazy(() => import("./LoginUpdatePassword"));
const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile")); const LoginUpdateProfile = lazy(() => import("./LoginUpdateProfile"));
const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm")); const LoginIdpLinkConfirm = lazy(() => import("./LoginIdpLinkConfirm"));
@ -20,8 +25,19 @@ const LoginPageExpired = lazy(() => import("./LoginPageExpired"));
const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail")); const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp")); const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm")); const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const UpdateUserProfile = lazy(() => import("./UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("./IdpReviewUserProfile"));
export type KcAppProps = KcProps & {
kcContext: KcContextBase;
i18n?: I18n;
doFetchDefaultThemeResources?: boolean;
Template?: (props: TemplateProps) => JSX.Element | null;
};
const KcApp = memo((props_: KcAppProps) => {
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const i18n = (function useClosure() { const i18n = (function useClosure() {
const i18n = useI18n({ const i18n = useI18n({
kcContext, kcContext,
@ -36,44 +52,54 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcConte
return null; return null;
} }
const props = { i18n, ...kcProps }; const commonProps = { i18n, Template, ...kcProps };
return ( return (
<Suspense> <Suspense>
{(() => { {(() => {
switch (kcContext.pageId) { switch (kcContext.pageId) {
case "login.ftl": case "login.ftl":
return <Login {...{ kcContext, ...props }} />; return <Login {...{ kcContext, ...commonProps }} />;
case "register.ftl": case "register.ftl":
return <Register {...{ kcContext, ...props }} />; return <Register {...{ kcContext, ...commonProps }} />;
case "register-user-profile.ftl": case "register-user-profile.ftl":
return <RegisterUserProfile {...{ kcContext, ...props }} />; return <RegisterUserProfile {...{ kcContext, ...commonProps }} />;
case "info.ftl": case "info.ftl":
return <Info {...{ kcContext, ...props }} />; return <Info {...{ kcContext, ...commonProps }} />;
case "error.ftl": case "error.ftl":
return <Error {...{ kcContext, ...props }} />; return <Error {...{ kcContext, ...commonProps }} />;
case "login-reset-password.ftl": case "login-reset-password.ftl":
return <LoginResetPassword {...{ kcContext, ...props }} />; return <LoginResetPassword {...{ kcContext, ...commonProps }} />;
case "login-verify-email.ftl": case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, ...props }} />; return <LoginVerifyEmail {...{ kcContext, ...commonProps }} />;
case "terms.ftl": case "terms.ftl":
return <Terms {...{ kcContext, ...props }} />; return <Terms {...{ kcContext, ...commonProps }} />;
case "login-otp.ftl": case "login-otp.ftl":
return <LoginOtp {...{ kcContext, ...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": case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, ...props }} />; return <LoginUpdatePassword {...{ kcContext, ...commonProps }} />;
case "login-update-profile.ftl": case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, ...props }} />; return <LoginUpdateProfile {...{ kcContext, ...commonProps }} />;
case "login-idp-link-confirm.ftl": case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />; return <LoginIdpLinkConfirm {...{ kcContext, ...commonProps }} />;
case "login-idp-link-email.ftl": case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />; return <LoginIdpLinkEmail {...{ kcContext, ...commonProps }} />;
case "login-page-expired.ftl": case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, ...props }} />; return <LoginPageExpired {...{ kcContext, ...commonProps }} />;
case "login-config-totp.ftl": case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, ...props }} />; return <LoginConfigTotp {...{ kcContext, ...commonProps }} />;
case "logout-confirm.ftl": case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, ...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> </Suspense>

View File

@ -84,6 +84,7 @@ export type KcProps = KcPropsGeneric<
| "kcFormSocialAccountDoubleListClass" | "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass" | "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon" | "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass" | "kcFormClass"
| "kcFormGroupErrorClass" | "kcFormGroupErrorClass"
| "kcLabelClass" | "kcLabelClass"
@ -105,12 +106,16 @@ export type KcProps = KcPropsGeneric<
| "kcSrOnlyClass" | "kcSrOnlyClass"
| "kcSelectAuthListClass" | "kcSelectAuthListClass"
| "kcSelectAuthListItemClass" | "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass" | "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass" | "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass" | "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass" | "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass" | "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass" | "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass" | "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass" | "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass" | "kcAuthenticatorOTPClass"
@ -138,6 +143,7 @@ export const defaultKcProps = {
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"], "kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"], "kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"], "kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
"kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"], "kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"], "kcFormGroupErrorClass": ["has-error"],
@ -173,6 +179,10 @@ export const defaultKcProps = {
// css classes for select-authenticator form // css classes for select-authenticator form
"kcSelectAuthListClass": ["list-group", "list-view-pf"], "kcSelectAuthListClass": ["list-group", "list-view-pf"],
"kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"], "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"], "kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"], "kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"], "kcSelectAuthListItemBodyClass": ["list-view-pf-body"],

View File

@ -1,19 +1,27 @@
import React, { useState, memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react"; import type { FormEventHandler } from "react";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Login; i18n: I18n } & KcProps) => { 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 { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
const { cx } = useCssAndCx();
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => { const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
@ -32,20 +40,21 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayInfo={social.displayInfo} displayInfo={social.displayInfo}
displayWide={realm.password && social.providers !== undefined} displayWide={realm.password && social.providers !== undefined}
headerNode={msg("doLogIn")} headerNode={msg("doLogIn")}
formNode={ formNode={
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}> <div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
<div <div
id="kc-form-wrapper" 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 && ( {realm.password && (
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post"> <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 const label = !realm.loginWithEmailAllowed
? "username" ? "username"
@ -57,13 +66,13 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
return ( return (
<> <>
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}> <label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}>
{msg(label)} {msg(label)}
</label> </label>
<input <input
tabIndex={1} tabIndex={1}
id={autoCompleteHelper} 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 //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 //the browser how to pre fill the form but before submit we put it back
//to username because it is what keycloak expects. //to username because it is what keycloak expects.
@ -81,20 +90,20 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
); );
})()} })()}
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<label htmlFor="password" className={cx(props.kcLabelClass)}> <label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
{msg("password")} {msg("password")}
</label> </label>
<input <input
tabIndex={2} tabIndex={2}
id="password" id="password"
className={cx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="password" name="password"
type="password" type="password"
autoComplete="off" autoComplete="off"
/> />
</div> </div>
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}> <div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
<div id="kc-form-options"> <div id="kc-form-options">
{realm.rememberMe && !usernameEditDisabled && ( {realm.rememberMe && !usernameEditDisabled && (
<div className="checkbox"> <div className="checkbox">
@ -115,7 +124,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
</div> </div>
)} )}
</div> </div>
<div className={cx(props.kcFormOptionsWrapperClass)}> <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
{realm.resetPasswordAllowed && ( {realm.resetPasswordAllowed && (
<span> <span>
<a tabIndex={5} href={url.loginResetCredentialsUrl}> <a tabIndex={5} href={url.loginResetCredentialsUrl}>
@ -125,7 +134,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
)} )}
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
<input <input
type="hidden" type="hidden"
id="id-hidden-input" id="id-hidden-input"
@ -138,11 +147,11 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
/> />
<input <input
tabIndex={4} tabIndex={4}
className={cx( className={clsx(
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonPrimaryClass, kcProps.kcButtonPrimaryClass,
props.kcButtonBlockClass, kcProps.kcButtonBlockClass,
props.kcButtonLargeClass kcProps.kcButtonLargeClass
)} )}
name="login" name="login"
id="kc-login" id="kc-login"
@ -155,16 +164,16 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
)} )}
</div> </div>
{realm.password && social.providers !== undefined && ( {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 <ul
className={cx( className={clsx(
props.kcFormSocialAccountListClass, kcProps.kcFormSocialAccountListClass,
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
)} )}
> >
{social.providers.map(p => ( {social.providers.map(p => (
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}> <li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}>
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}> <a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
<span>{p.displayName}</span> <span>{p.displayName}</span>
</a> </a>
</li> </li>

View File

@ -1,27 +1,34 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => { export type LoginConfigTotpProps = KcProps & {
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext; 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 { msg, msgStr } = i18n;
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = { const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1", "HmacSHA1": "SHA1",
HmacSHA256: "SHA256", "HmacSHA256": "SHA256",
HmacSHA512: "SHA512" "HmacSHA512": "SHA512"
}; };
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("loginTotpTitle")} headerNode={msg("loginTotpTitle")}
formNode={ formNode={
<> <>
@ -93,26 +100,26 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
</li> </li>
</ol> </ol>
<form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post"> <form action={url.loginAction} className={clsx(kcProps.kcFormClass)} id="kc-totp-settings-form" method="post">
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<label htmlFor="totp" className={cx(props.kcLabelClass)}> <label htmlFor="totp" className={clsx(kcProps.kcLabelClass)}>
{msg("authenticatorCode")} {msg("authenticatorCode")}
</label>{" "} </label>{" "}
<span className="required">*</span> <span className="required">*</span>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="totp" id="totp"
name="totp" name="totp"
autoComplete="off" autoComplete="off"
className={cx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("totp")} aria-invalid={messagesPerField.existsError("totp")}
/> />
{messagesPerField.existsError("totp") && ( {messagesPerField.existsError("totp") && (
<span id="input-error-otp-code" className={cx(props.kcInputErrorMessageClass)} aria-live="polite"> <span id="input-error-otp-code" className={clsx(kcProps.kcInputErrorMessageClass)} aria-live="polite">
{messagesPerField.get("totp")} {messagesPerField.get("totp")}
</span> </span>
)} )}
@ -121,24 +128,24 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
{mode && <input type="hidden" id="mode" value={mode} />} {mode && <input type="hidden" id="mode" value={mode} />}
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<label htmlFor="userLabel" className={cx(props.kcLabelClass)}> <label htmlFor="userLabel" className={clsx(kcProps.kcLabelClass)}>
{msg("loginTotpDeviceName")} {msg("loginTotpDeviceName")}
</label>{" "} </label>{" "}
{totp.otpCredentials.length >= 1 && <span className="required">*</span>} {totp.otpCredentials.length >= 1 && <span className="required">*</span>}
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="userLabel" id="userLabel"
name="userLabel" name="userLabel"
autoComplete="off" autoComplete="off"
className={cx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
aria-invalid={messagesPerField.existsError("userLabel")} aria-invalid={messagesPerField.existsError("userLabel")}
/> />
{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")} {messagesPerField.get("userLabel")}
</span> </span>
)} )}
@ -149,17 +156,17 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
<> <>
<input <input
type="submit" type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn" id="saveTOTPBtn"
value={msgStr("doSubmit")} value={msgStr("doSubmit")}
/> />
<button <button
type="submit" type="submit"
className={cx( className={clsx(
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonDefaultClass, kcProps.kcButtonDefaultClass,
props.kcButtonLargeClass, kcProps.kcButtonLargeClass,
props.kcButtonLargeClass kcProps.kcButtonLargeClass
)} )}
id="cancelTOTPBtn" id="cancelTOTPBtn"
name="cancel-aia" name="cancel-aia"
@ -171,7 +178,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
) : ( ) : (
<input <input
type="submit" type="submit"
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
id="saveTOTPBtn" id="saveTOTPBtn"
value={msgStr("doSubmit")} value={msgStr("doSubmit")}
/> />

View File

@ -1,28 +1,40 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import type { I18n } from "../i18n"; 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 { url, idpAlias } = kcContext;
const { msg } = i18n; const { msg } = i18n;
const { cx } = useCssAndCx();
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("confirmLinkIdpTitle")} headerNode={msg("confirmLinkIdpTitle")}
formNode={ formNode={
<form id="kc-register-form" action={url.loginAction} method="post"> <form id="kc-register-form" action={url.loginAction} method="post">
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<button <button
type="submit" type="submit"
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction" name="submitAction"
id="updateProfile" id="updateProfile"
value="updateProfile" value="updateProfile"
@ -31,7 +43,12 @@ const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
</button> </button>
<button <button
type="submit" type="submit"
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonDefaultClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="submitAction" name="submitAction"
id="linkAccount" id="linkAccount"
value="linkAccount" value="linkAccount"

View File

@ -1,18 +1,27 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginIdpLinkEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail; i18n: I18n } & KcProps) => { 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 { url, realm, brokerContext, idpAlias } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("emailLinkIdpTitle", idpAlias)} headerNode={msg("emailLinkIdpTitle", idpAlias)}
formNode={ formNode={
<> <>

View File

@ -1,16 +1,24 @@
import React, { useEffect, memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { headInsert } from "../tools/headInsert"; import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => { export type LoginOtpProps = KcProps & {
const { otpLogin, url } = kcContext; 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; const { msg, msgStr } = i18n;
@ -33,46 +41,50 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("doLogIn")} headerNode={msg("doLogIn")}
formNode={ formNode={
<form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> <form id="kc-otp-login-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
{otpLogin.userOtpCredentials.length > 1 && ( {otpLogin.userOtpCredentials.length > 1 && (
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
{otpLogin.userOtpCredentials.map(otpCredential => ( {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}" /> <input type="hidden" value="${otpCredential.id}" />
<div className={cx(props.kcSelectOTPListItemClass)}> <div className={clsx(kcProps.kcSelectOTPListItemClass)}>
<span className={cx(props.kcAuthenticatorOtpCircleClass)} /> <span className={clsx(kcProps.kcAuthenticatorOtpCircleClass)} />
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2> <h2 className={clsx(kcProps.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
)} )}
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="otp" className={cx(props.kcLabelClass)}> <label htmlFor="otp" className={clsx(kcProps.kcLabelClass)}>
{msg("loginOtpOneTime")} {msg("loginOtpOneTime")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input id="otp" name="otp" autoComplete="off" type="text" className={cx(props.kcInputClass)} autoFocus /> <input id="otp" name="otp" autoComplete="off" type="text" className={clsx(kcProps.kcInputClass)} autoFocus />
</div> </div>
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)} /> <div className={clsx(kcProps.kcFormOptionsWrapperClass)} />
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
name="login" name="login"
id="kc-login" id="kc-login"
type="submit" type="submit"

View File

@ -1,18 +1,27 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginPageExpired = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginPageExpired; i18n: I18n } & KcProps) => { 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 { url } = kcContext;
const { msg } = i18n; const { msg } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("pageExpiredTitle")} headerNode={msg("pageExpiredTitle")}
formNode={ formNode={

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

View File

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

View File

@ -1,12 +1,20 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => { export type LoginUpdatePasswordProps = KcProps & {
const { cx } = useCssAndCx(); 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; const { msg, msgStr } = i18n;
@ -14,11 +22,10 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("updatePasswordTitle")} headerNode={msg("updatePasswordTitle")}
formNode={ formNode={
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post"> <form id="kc-passwd-update-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
<input <input
type="text" type="text"
id="username" 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" }} /> <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={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}>
<div className={cx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password-new" className={cx(props.kcLabelClass)}> <label htmlFor="password-new" className={clsx(kcProps.kcLabelClass)}>
{msg("passwordNew")} {msg("passwordNew")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="password" type="password"
id="password-new" id="password-new"
name="password-new" name="password-new"
autoFocus autoFocus
autoComplete="new-password" autoComplete="new-password"
className={cx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
/> />
</div> </div>
</div> </div>
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}> <div
<div className={cx(props.kcLabelWrapperClass)}> className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass))}
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}> >
<div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
{msg("passwordConfirm")} {msg("passwordConfirm")}
</label> </label>
</div> </div>
<div className={cx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="password" type="password"
id="password-confirm" id="password-confirm"
name="password-confirm" name="password-confirm"
autoComplete="new-password" autoComplete="new-password"
className={cx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
/> />
</div> </div>
</div> </div>
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}> <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
{isAppInitiatedAction && ( {isAppInitiatedAction && (
<div className="checkbox"> <div className="checkbox">
<label> <label>
@ -79,16 +88,16 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
{isAppInitiatedAction ? ( {isAppInitiatedAction ? (
<> <>
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)} className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
type="submit" type="submit"
defaultValue={msgStr("doSubmit")} defaultValue={msgStr("doSubmit")}
/> />
<button <button
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)} className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
type="submit" type="submit"
name="cancel-aia" name="cancel-aia"
value="true" value="true"
@ -98,11 +107,11 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
</> </>
) : ( ) : (
<input <input
className={cx( className={clsx(
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonPrimaryClass, kcProps.kcButtonPrimaryClass,
props.kcButtonBlockClass, kcProps.kcButtonBlockClass,
props.kcButtonLargeClass kcProps.kcButtonLargeClass
)} )}
type="submit" type="submit"
defaultValue={msgStr("doSubmit")} defaultValue={msgStr("doSubmit")}

View File

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

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

View File

@ -1,18 +1,27 @@
import React, { memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LoginVerifyEmail = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginVerifyEmail; i18n: I18n } & KcProps) => { 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 { msg } = i18n;
const { url, user } = kcContext; const { url, user } = kcContext;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("emailVerifyTitle")} headerNode={msg("emailVerifyTitle")}
formNode={ formNode={

View File

@ -1,21 +1,28 @@
import React, { memo } from "react"; import React, { memo } from "react";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import Template from "./Template"; import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LogoutConfirm; i18n: I18n } & KcProps) => { export type LogoutConfirmProps = KcProps & {
const { url, client, logoutConfirm } = kcContext; 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; const { msg, msgStr } = i18n;
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("logoutConfirmTitle")} headerNode={msg("logoutConfirmTitle")}
formNode={ formNode={
@ -24,18 +31,18 @@ const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContex
<p className="instruction">{msg("logoutConfirmHeader")}</p> <p className="instruction">{msg("logoutConfirmHeader")}</p>
<form className="form-actions" action={url.logoutConfirmAction} method="POST"> <form className="form-actions" action={url.logoutConfirmAction} method="POST">
<input type="hidden" name="session_code" value={logoutConfirm.code} /> <input type="hidden" name="session_code" value={logoutConfirm.code} />
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options"> <div id="kc-form-options">
<div className={cx(props.kcFormOptionsWrapperClass)}></div> <div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
<input <input
tabIndex={4} tabIndex={4}
className={cx( className={clsx(
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonPrimaryClass, kcProps.kcButtonPrimaryClass,
props.kcButtonBlockClass, kcProps.kcButtonBlockClass,
props.kcButtonLargeClass kcProps.kcButtonLargeClass
)} )}
name="confirmLogout" name="confirmLogout"
id="kc-logout" id="kc-logout"

View File

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

View File

@ -1,59 +1,61 @@
import React, { useMemo, memo, useEffect, useState, Fragment } from "react"; import React, { memo, useState } from "react";
import Template from "./Template"; import DefaultTemplate from "./Template";
import type { TemplateProps } from "./Template";
import type { KcProps } from "./KcProps"; import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { 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 { 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 { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n; 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); const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true} displayRequiredFields={true}
doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post"> <form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...props} /> <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <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 className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div> </div>
</div> </div>
)} )}
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}>
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}> <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={cx(props.kcFormOptionsWrapperClass)}> <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
<span> <span>
<a href={url.loginUrl}>{msg("backToLogin")}</a> <a href={url.loginUrl}>{msg("backToLogin")}</a>
</span> </span>
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input <input
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit" type="submit"
value={msgStr("doRegister")} value={msgStr("doRegister")}
disabled={!isFomSubmittable} 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; export default RegisterUserProfile;

View File

@ -7,7 +7,7 @@ import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import type { KcTemplateProps } from "./KcProps"; import type { KcTemplateProps } from "./KcProps";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
export type TemplateProps = { export type TemplateProps = {
@ -42,8 +42,6 @@ const Template = memo((props: TemplateProps) => {
doFetchDefaultThemeResources doFetchDefaultThemeResources
} = props; } = props;
const { cx } = useCssAndCx();
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag)); const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag));
@ -96,7 +94,7 @@ const Template = memo((props: TemplateProps) => {
if (props.kcHtmlClass !== undefined) { if (props.kcHtmlClass !== undefined) {
const htmlClassList = document.getElementsByTagName("html")[0].classList; const htmlClassList = document.getElementsByTagName("html")[0].classList;
const tokens = cx(props.kcHtmlClass).split(" "); const tokens = clsx(props.kcHtmlClass).split(" ");
htmlClassList.add(...tokens); htmlClassList.add(...tokens);
@ -115,18 +113,18 @@ const Template = memo((props: TemplateProps) => {
} }
return ( return (
<div className={cx(props.kcLoginClass)}> <div className={clsx(props.kcLoginClass)}>
<div id="kc-header" className={cx(props.kcHeaderClass)}> <div id="kc-header" className={clsx(props.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(props.kcHeaderWrapperClass)}> <div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}>
{msg("loginTitleHtml", realm.displayNameHtml)} {msg("loginTitleHtml", realm.displayNameHtml)}
</div> </div>
</div> </div>
<div className={cx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}> <div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
<header className={cx(props.kcFormHeaderClass)}> <header className={clsx(props.kcFormHeaderClass)}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && ( {realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<div id="kc-locale"> <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"> <div className="kc-dropdown" id="kc-locale-dropdown">
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]} {labelBySupportedLanguageTag[currentLanguageTag]}
@ -146,8 +144,8 @@ const Template = memo((props: TemplateProps) => {
)} )}
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
displayRequiredFields ? ( displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}> <div className={clsx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> <span className="required">*</span>
{msg("requiredFields")} {msg("requiredFields")}
@ -161,20 +159,20 @@ const Template = memo((props: TemplateProps) => {
<h1 id="kc-page-title">{headerNode}</h1> <h1 id="kc-page-title">{headerNode}</h1>
) )
) : displayRequiredFields ? ( ) : displayRequiredFields ? (
<div className={cx(props.kcContentWrapperClass)}> <div className={clsx(props.kcContentWrapperClass)}>
<div className={cx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> {msg("requiredFields")} <span className="required">*</span> {msg("requiredFields")}
</span> </span>
</div> </div>
<div className="col-md-10"> <div className="col-md-10">
{showUsernameNode} {showUsernameNode}
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(props.kcFormGroupClass)}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <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> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -185,12 +183,12 @@ const Template = memo((props: TemplateProps) => {
) : ( ) : (
<> <>
{showUsernameNode} {showUsernameNode}
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(props.kcFormGroupClass)}>
<div id="kc-username"> <div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label> <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}> <a id="reset-login" href={url.loginRestartFlowUrl}>
<div className="kc-login-tooltip"> <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> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -203,11 +201,11 @@ const Template = memo((props: TemplateProps) => {
<div id="kc-content-wrapper"> <div id="kc-content-wrapper">
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<div className={cx("alert", `alert-${message.type}`)}> <div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={cx(props.kcFeedbackSuccessIcon)}></span>} {message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>}
{message.type === "warning" && <span className={cx(props.kcFeedbackWarningIcon)}></span>} {message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>}
{message.type === "error" && <span className={cx(props.kcFeedbackErrorIcon)}></span>} {message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>}
{message.type === "info" && <span className={cx(props.kcFeedbackInfoIcon)}></span>} {message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>}
<span <span
className="kc-feedback-text" className="kc-feedback-text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -222,10 +220,10 @@ const Template = memo((props: TemplateProps) => {
id="kc-select-try-another-way-form" id="kc-select-try-another-way-form"
action={url.loginAction} action={url.loginAction}
method="post" method="post"
className={cx(displayWide && props.kcContentWrapperClass)} className={clsx(displayWide && props.kcContentWrapperClass)}
> >
<div className={cx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}> <div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
<div className={cx(props.kcFormGroupClass)}> <div className={clsx(props.kcFormGroupClass)}>
<input type="hidden" name="tryAnotherWay" value="on" /> <input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}> <a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>
{msg("doTryAnotherWay")} {msg("doTryAnotherWay")}
@ -235,8 +233,8 @@ const Template = memo((props: TemplateProps) => {
</form> </form>
)} )}
{displayInfo && ( {displayInfo && (
<div id="kc-info" className={cx(props.kcSignUpClass)}> <div id="kc-info" className={clsx(props.kcSignUpClass)}>
<div id="kc-info-wrapper" className={cx(props.kcInfoAreaWrapperClass)}> <div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}>
{infoNode} {infoNode}
</div> </div>
</div> </div>

View File

@ -1,8 +1,9 @@
import React, { useEffect, memo } from "react"; 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 { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase"; import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "../tools/useCssAndCx"; import { clsx } from "../tools/clsx";
import { Evt } from "evt"; import { Evt } from "evt";
import { useRerenderOnStateChange } from "evt/hooks"; import { useRerenderOnStateChange } from "evt/hooks";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
@ -11,6 +12,8 @@ import type { I18n } from "../i18n";
import memoize from "memoizee"; import memoize from "memoizee";
import { useConst } from "powerhooks/useConst"; import { useConst } from "powerhooks/useConst";
import { useConstCallback } from "powerhooks/useConstCallback"; import { useConstCallback } from "powerhooks/useConstCallback";
import { Markdown } from "../tools/Markdown";
import type { Extends } from "tsafe";
export const evtTermMarkdown = Evt.create<string | undefined>(undefined); export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
@ -21,7 +24,7 @@ export type KcContextLike = {
}; };
}; };
assert<KcContextBase extends KcContextLike ? true : false>(); assert<Extends<KcContextBase, KcContextLike>>();
/** Allow to avoid bundling the terms and download it on demand*/ /** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: { export function useDownloadTerms(params: {
@ -53,13 +56,20 @@ export function useDownloadTerms(params: {
}, []); }, []);
} }
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; const { msg, msgStr } = i18n;
useRerenderOnStateChange(evtTermMarkdown); useRerenderOnStateChange(evtTermMarkdown);
const { cx } = useCssAndCx();
const { url } = kcContext; const { url } = kcContext;
if (evtTermMarkdown.state === undefined) { if (evtTermMarkdown.state === undefined) {
@ -68,21 +78,20 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
return ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
displayMessage={false} displayMessage={false}
headerNode={msg("termsTitle")} headerNode={msg("termsTitle")}
formNode={ formNode={
<> <>
<div id="kc-terms-text">{evtTermMarkdown.state}</div> <div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
<form className="form-actions" action={url.loginAction} method="POST"> <form className="form-actions" action={url.loginAction} method="POST">
<input <input
className={cx( className={clsx(
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonClass, kcProps.kcButtonClass,
props.kcButtonPrimaryClass, kcProps.kcButtonPrimaryClass,
props.kcButtonLargeClass kcProps.kcButtonLargeClass
)} )}
name="accept" name="accept"
id="kc-accept" id="kc-accept"
@ -90,7 +99,7 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
value={msgStr("doAccept")} value={msgStr("doAccept")}
/> />
<input <input
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)} className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
name="cancel" name="cancel"
id="kc-decline" id="kc-decline"
type="submit" type="submit"

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

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

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

View File

@ -2,6 +2,7 @@ import type { PageId } from "../../bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { MessageKeyBase } from "../i18n"; import type { MessageKeyBase } from "../i18n";
import type { KcTemplateClassKey } from "../components/KcProps";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never; type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
@ -19,13 +20,28 @@ export type KcContextBase =
| KcContextBase.LoginVerifyEmail | KcContextBase.LoginVerifyEmail
| KcContextBase.Terms | KcContextBase.Terms
| KcContextBase.LoginOtp | KcContextBase.LoginOtp
| KcContextBase.LoginUsername
| KcContextBase.WebauthnAuthenticate
| KcContextBase.LoginPassword
| KcContextBase.LoginUpdatePassword | KcContextBase.LoginUpdatePassword
| KcContextBase.LoginUpdateProfile | KcContextBase.LoginUpdateProfile
| KcContextBase.LoginIdpLinkConfirm | KcContextBase.LoginIdpLinkConfirm
| KcContextBase.LoginIdpLinkEmail | KcContextBase.LoginIdpLinkEmail
| KcContextBase.LoginPageExpired | KcContextBase.LoginPageExpired
| KcContextBase.LoginConfigTotp | 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 declare namespace KcContextBase {
export type Common = { 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 & { export type LoginUpdatePassword = Common & {
pageId: "login-update-password.ftl"; pageId: "login-update-password.ftl";
username: string; username: string;
@ -270,6 +357,23 @@ export declare namespace KcContextBase {
skipLink?: boolean; skipLink?: boolean;
}; };
}; };
export type UpdateUserProfile = Common & {
pageId: "update-user-profile.ftl";
profile: {
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
export type IdpReviewUserProfile = Common & {
pageId: "idp-review-user-profile.ftl";
profile: {
context: "IDP_REVIEW";
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
};
};
} }
export type Attribute = { export type Attribute = {

View File

@ -10,6 +10,7 @@ import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { pathJoin } from "../../bin/tools/pathJoin"; import { pathJoin } from "../../bin/tools/pathJoin";
import { pathBasename } from "../tools/pathBasename"; import { pathBasename } from "../tools/pathBasename";
import { mockTestingResourcesCommonPath } from "../../bin/mockTestingResourcesPath"; import { mockTestingResourcesCommonPath } from "../../bin/mockTestingResourcesPath";
import { symToStr } from "tsafe/symToStr";
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: { export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"]; mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
@ -17,9 +18,19 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
}): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } { }): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } {
const { mockPageId, mockData } = params ?? {}; 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 //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 kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId); const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
@ -47,8 +58,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
"source": partialKcContextCustomMock "source": partialKcContextCustomMock
}); });
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") { if (
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl"); partialKcContextCustomMock.pageId === "register-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
) {
assert(
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
);
const { attributes } = kcContextDefaultMock.profile; const { attributes } = kcContextDefaultMock.profile;
@ -60,8 +79,6 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
].filter(exclude(undefined)); ].filter(exclude(undefined));
attributes.forEach(attribute => { attributes.forEach(attribute => {
console.log("====>", attribute);
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name); const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any; const augmentedAttribute: Attribute = {} as any;
@ -100,13 +117,15 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
return { kcContext }; 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)); url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
} }
return { kcContext }; return { "kcContext": realKcContext };
} }

View File

@ -7,6 +7,100 @@ import { pathJoin } from "../../../bin/tools/pathJoin";
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/"; const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
const attributes: Attribute[] = [
{
"validators": {
"username-prohibited-characters": {
"ignore.empty.value": true
},
"up-username-has-value": {},
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "255"
},
"up-duplicate-username": {},
"up-username-mutation": {}
},
"displayName": "${username}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx"
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true
},
"pattern": {
"ignore.empty.value": true,
"pattern": "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName"
}
];
const attributesByName = Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any;
export const kcContextCommonMock: KcContextBase.Common = { export const kcContextCommonMock: KcContextBase.Common = {
"url": { "url": {
"loginAction": "#", "loginAction": "#",
@ -200,104 +294,8 @@ export const kcContextMocks: KcContextBase[] = [
...registerCommon, ...registerCommon,
"profile": { "profile": {
"context": "REGISTRATION_PROFILE" as const, "context": "REGISTRATION_PROFILE" as const,
...(() => { attributes,
const attributes: Attribute[] = [ attributesByName
{
"validators": {
"username-prohibited-characters": {
"ignore.empty.value": true
},
"up-username-has-value": {},
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "255"
},
"up-duplicate-username": {},
"up-username-mutation": {}
},
"displayName": "${username}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx"
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true
},
"pattern": {
"ignore.empty.value": true,
"pattern": "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName"
}
];
return {
attributes,
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
} as any;
})()
} }
}) })
]; ];
@ -361,6 +359,61 @@ export const kcContextMocks: KcContextBase[] = [
] ]
} }
}), }),
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>({ id<KcContextBase.LoginUpdatePassword>({
...kcContextCommonMock, ...kcContextCommonMock,
"pageId": "login-update-password.ftl", "pageId": "login-update-password.ftl",
@ -423,5 +476,22 @@ export const kcContextMocks: KcContextBase[] = [
"baseUrl": "#" "baseUrl": "#"
}, },
"logoutConfirm": { "code": "123", skipLink: false } "logoutConfirm": { "code": "123", skipLink: false }
}),
id<KcContextBase.UpdateUserProfile>({
...kcContextCommonMock,
"pageId": "update-user-profile.ftl",
"profile": {
attributes,
attributesByName
}
}),
id<KcContextBase.IdpReviewUserProfile>({
...kcContextCommonMock,
"pageId": "idp-review-user-profile.ftl",
"profile": {
context: "IDP_REVIEW",
attributes,
attributesByName
}
}) })
]; ];

View File

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

View File

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

7
src/lib/tools/clsx.ts Normal file
View File

@ -0,0 +1,7 @@
import { classnames } from "tss-react/tools/classnames";
import type { Cx } from "tss-react";
/** Drop in replacement for https://www.npmjs.com/package/clsx */
export const clsx: Cx = (...args) => {
return classnames(args);
};

View File

@ -1,11 +1,12 @@
import { createMakeStyles } from "tss-react"; import { clsx as cx } from "./clsx";
const { useStyles } = createMakeStyles({
"useTheme": () => ({})
});
/**
* @deprecated: Use clsx instead.
* import { clsx } from "keycloakify/lib/tools/clsx";
* You can use clsx as cx.
* If you where using the css() function you can import
* it from @emotion/css: https://emotion.sh/docs/@emotion/css
*/
export function useCssAndCx() { export function useCssAndCx() {
const { css, cx } = useStyles(); return { cx };
return { css, cx };
} }

View File

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

View File

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

View File

@ -103,13 +103,13 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
const fixedJsCodeExpected = ` const fixedJsCodeExpected = `
function f() { function f() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : a.p) + "static/js/" + ({}[e] || e) + "." + { return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0" 3: "0664cdc0"
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
function sameAsF() { function sameAsF() {
return ("kcContext" in window ? "https://demo-app.keycloakify.dev" : a.p) + "static/js/" + ({}[e] || e) + "." + { return ("kcContext" in window ? "https://demo-app.keycloakify.dev/" : a.p) + "static/js/" + ({}[e] || e) + "." + {
3: "0664cdc0" 3: "0664cdc0"
}[e] + ".chunk.js" }[e] + ".chunk.js"
} }
@ -119,7 +119,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
if( pd === undefined || pd.configurable ){ if( pd === undefined || pd.configurable ){
var p= ""; var p= "";
Object.defineProperty(__webpack_require__, "p", { Object.defineProperty(__webpack_require__, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev" : p; }, get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; } set: function (value){ p = value; }
}); });
} }
@ -137,7 +137,7 @@ import { assetIsSameCode } from "../tools/assertIsSameCode";
if( pd === undefined || pd.configurable ){ if( pd === undefined || pd.configurable ){
var p= ""; var p= "";
Object.defineProperty(t, "p", { Object.defineProperty(t, "p", {
get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev" : p; }, get: function() { return "kcContext" in window ? "https://demo-app.keycloakify.dev/" : p; },
set: function (value){ p = value; } set: function (value){ p = value; }
}); });
} }

View File

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

371
yarn.lock
View File

@ -2,24 +2,125 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/code-frame@^7.0.0": "@ampproject/remapping@^2.1.0":
version "2.2.0"
resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d"
integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==
dependencies:
"@jridgewell/gen-mapping" "^0.1.0"
"@jridgewell/trace-mapping" "^0.3.9"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==
dependencies: dependencies:
"@babel/highlight" "^7.18.6" "@babel/highlight" "^7.18.6"
"@babel/helper-module-imports@^7.16.7": "@babel/compat-data@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.19.3.tgz#707b939793f867f5a73b2666e6d9a3396eb03151"
integrity sha512-prBHMK4JYYK+wDjJF1q99KK4JLL+egWS4nmNqdlMUgCExMZ+iZW0hGhyC3VEbsPjvaN0TBhW//VIFwBrk8sEiw==
"@babel/core@^7.0.0":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.19.3.tgz#2519f62a51458f43b682d61583c3810e7dcee64c"
integrity sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==
dependencies:
"@ampproject/remapping" "^2.1.0"
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.19.3"
"@babel/helper-compilation-targets" "^7.19.3"
"@babel/helper-module-transforms" "^7.19.0"
"@babel/helpers" "^7.19.0"
"@babel/parser" "^7.19.3"
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.3"
"@babel/types" "^7.19.3"
convert-source-map "^1.7.0"
debug "^4.1.0"
gensync "^1.0.0-beta.2"
json5 "^2.2.1"
semver "^6.3.0"
"@babel/generator@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.19.3.tgz#d7f4d1300485b4547cb6f94b27d10d237b42bf59"
integrity sha512-fqVZnmp1ncvZU757UzDheKZpfPgatqY59XtW2/j/18H7u76akb8xqvjw82f+i2UKd/ksYsSick/BCLQUUtJ/qQ==
dependencies:
"@babel/types" "^7.19.3"
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
"@babel/helper-compilation-targets@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz#a10a04588125675d7c7ae299af86fa1b2ee038ca"
integrity sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==
dependencies:
"@babel/compat-data" "^7.19.3"
"@babel/helper-validator-option" "^7.18.6"
browserslist "^4.21.3"
semver "^6.3.0"
"@babel/helper-environment-visitor@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
"@babel/helper-function-name@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c"
integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==
dependencies:
"@babel/template" "^7.18.10"
"@babel/types" "^7.19.0"
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-module-imports@^7.16.7", "@babel/helper-module-imports@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
dependencies: dependencies:
"@babel/types" "^7.18.6" "@babel/types" "^7.18.6"
"@babel/helper-module-transforms@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz#309b230f04e22c58c6a2c0c0c7e50b216d350c30"
integrity sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==
dependencies:
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-module-imports" "^7.18.6"
"@babel/helper-simple-access" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/helper-validator-identifier" "^7.18.6"
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.0"
"@babel/types" "^7.19.0"
"@babel/helper-plugin-utils@^7.18.6": "@babel/helper-plugin-utils@^7.18.6":
version "7.18.9" version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz#4b8aea3b069d8cb8a72cdfe28ddf5ceca695ef2f" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz#4796bb14961521f0f8715990bee2fb6e51ce21bf"
integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== integrity sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==
"@babel/helper-simple-access@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz#d6d8f51f4ac2978068df934b569f08f29788c7ea"
integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-split-export-declaration@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075"
integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-string-parser@^7.18.10": "@babel/helper-string-parser@^7.18.10":
version "7.18.10" version "7.18.10"
@ -31,6 +132,25 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
"@babel/helper-validator-identifier@^7.19.1":
version "7.19.1"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2"
integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==
"@babel/helper-validator-option@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8"
integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==
"@babel/helpers@^7.19.0":
version "7.19.0"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.19.0.tgz#f30534657faf246ae96551d88dd31e9d1fa1fc18"
integrity sha512-DRBCKGwIEdqY3+rPJgG/dKfQy9+08rHIAJx8q2p+HSWP87s2HCrQmaAMMyMll2kIXKCW0cO1RdQskx15Xakftg==
dependencies:
"@babel/template" "^7.18.10"
"@babel/traverse" "^7.19.0"
"@babel/types" "^7.19.0"
"@babel/highlight@^7.18.6": "@babel/highlight@^7.18.6":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf"
@ -40,6 +160,11 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/parser@^7.18.10", "@babel/parser@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.19.3.tgz#8dd36d17c53ff347f9e55c328710321b49479a9a"
integrity sha512-pJ9xOlNWHiy9+FuFP09DEAFbAn4JskgRsVcc169w2xRBC3FRGuQEwjeIMMND9L2zc0iEhO/tGv4Zq+km+hxNpQ==
"@babel/plugin-syntax-jsx@^7.17.12": "@babel/plugin-syntax-jsx@^7.17.12":
version "7.18.6" version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0"
@ -48,12 +173,46 @@
"@babel/helper-plugin-utils" "^7.18.6" "@babel/helper-plugin-utils" "^7.18.6"
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3": "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3":
version "7.18.9" version "7.19.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.19.4.tgz#a42f814502ee467d55b38dd1c256f53a7b885c78"
integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== integrity sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==
dependencies: dependencies:
regenerator-runtime "^0.13.4" regenerator-runtime "^0.13.4"
"@babel/template@^7.18.10":
version "7.18.10"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71"
integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/parser" "^7.18.10"
"@babel/types" "^7.18.10"
"@babel/traverse@^7.19.0", "@babel/traverse@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.19.3.tgz#3a3c5348d4988ba60884e8494b0592b2f15a04b4"
integrity sha512-qh5yf6149zhq2sgIXmwjnsvmnNQC2iw70UFjp4olxucKrWd/dvlUsBI88VSLUsnMNF7/vnOiA+nk1+yLoCqROQ==
dependencies:
"@babel/code-frame" "^7.18.6"
"@babel/generator" "^7.19.3"
"@babel/helper-environment-visitor" "^7.18.9"
"@babel/helper-function-name" "^7.19.0"
"@babel/helper-hoist-variables" "^7.18.6"
"@babel/helper-split-export-declaration" "^7.18.6"
"@babel/parser" "^7.19.3"
"@babel/types" "^7.19.3"
debug "^4.1.0"
globals "^11.1.0"
"@babel/types@^7.18.10", "@babel/types@^7.19.0", "@babel/types@^7.19.3":
version "7.19.3"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.19.3.tgz#fc420e6bbe54880bce6779ffaf315f5e43ec9624"
integrity sha512-hGCaQzIY22DJlDh9CH7NOxgKkFjBk0Cw9xDO1Xmh2151ti7wiGfQ3LauXzL4HP1fmFlTX6XjpRETTpUcv7wQLw==
dependencies:
"@babel/helper-string-parser" "^7.18.10"
"@babel/helper-validator-identifier" "^7.19.1"
to-fast-properties "^2.0.0"
"@babel/types@^7.18.6": "@babel/types@^7.18.6":
version "7.18.13" version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.13.tgz#30aeb9e514f4100f7c1cb6e5ba472b30e48f519a"
@ -81,7 +240,7 @@
source-map "^0.5.7" source-map "^0.5.7"
stylis "4.0.13" stylis "4.0.13"
"@emotion/cache@*", "@emotion/cache@^11.10.0": "@emotion/cache@^11.10.0":
version "11.10.3" version "11.10.3"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.10.3.tgz#c4f67904fad10c945fea5165c3a5a0583c164b87"
integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ== integrity sha512-Psmp/7ovAa8appWh3g51goxu/z3iVms7JXOreq136D8Bbn6dYraPnmL6mdM8GThEx9vwSn92Fz+mGSjBzN8UPQ==
@ -102,7 +261,7 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f"
integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==
"@emotion/react@^11.4.1": "@emotion/react@^11.10.4":
version "11.10.4" version "11.10.4"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.4.tgz#9dc6bccbda5d70ff68fdb204746c0e8b13a79199" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.4.tgz#9dc6bccbda5d70ff68fdb204746c0e8b13a79199"
integrity sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA== integrity sha512-j0AkMpr6BL8gldJZ6XQsQ8DnS9TxEQu1R+OGmDZiWjBAJtCcbt0tS3I/YffoqHXxH6MjgI7KdMbYKw3MEiU9eA==
@ -116,7 +275,7 @@
"@emotion/weak-memoize" "^0.3.0" "@emotion/weak-memoize" "^0.3.0"
hoist-non-react-statics "^3.3.1" hoist-non-react-statics "^3.3.1"
"@emotion/serialize@*", "@emotion/serialize@^1.1.0": "@emotion/serialize@^1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.0.tgz#b1f97b1011b09346a40e9796c37a3397b4ea8ea8" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.0.tgz#b1f97b1011b09346a40e9796c37a3397b4ea8ea8"
integrity sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA== integrity sha512-F1ZZZW51T/fx+wKbVlwsfchr5q97iW8brAnXmsskz4d0hVB4O3M/SiA3SaeH06x02lSNzkkQv+n3AX3kCXKSFA==
@ -142,7 +301,7 @@
resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df" resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz#ffadaec35dbb7885bd54de3fa267ab2f860294df"
integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A== integrity sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==
"@emotion/utils@*", "@emotion/utils@^1.2.0": "@emotion/utils@^1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.0.tgz#9716eaccbc6b5ded2ea5a90d65562609aab0f561"
integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==
@ -152,6 +311,46 @@
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb" resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz#ea89004119dc42db2e1dba0f97d553f7372f6fcb"
integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==
"@jridgewell/gen-mapping@^0.1.0":
version "0.1.1"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996"
integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==
dependencies:
"@jridgewell/set-array" "^1.0.0"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
dependencies:
"@jridgewell/set-array" "^1.0.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.9"
"@jridgewell/resolve-uri@^3.0.3":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
"@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.14"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.15"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774"
integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@octokit/auth-token@^2.4.4": "@octokit/auth-token@^2.4.4":
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36" resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.5.0.tgz#27c37ea26c205f28443402477ffd261311f21e36"
@ -265,6 +464,11 @@
resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519" resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.8.tgz#04adc0c266a0f5d72db0556fdda2ba17dad9b519"
integrity sha512-qDpXKGgwKywnQt/64fH1O0LiPA++QGIYeykEUiZ51HymKVRLnUSGcRuF60IfpPeeXiuRwiR/W4y7S5VzbrgLCA== integrity sha512-qDpXKGgwKywnQt/64fH1O0LiPA++QGIYeykEUiZ51HymKVRLnUSGcRuF60IfpPeeXiuRwiR/W4y7S5VzbrgLCA==
"@types/minimist@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c"
integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==
"@types/node@^17.0.25": "@types/node@^17.0.25":
version "17.0.45" version "17.0.45"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.45.tgz#2c0fafd78705e7a18b7906b5201a522719dc5190"
@ -392,11 +596,26 @@ braces@^3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
browserslist@^4.21.3:
version "4.21.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987"
integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==
dependencies:
caniuse-lite "^1.0.30001400"
electron-to-chromium "^1.4.251"
node-releases "^2.0.6"
update-browserslist-db "^1.0.9"
callsites@^3.0.0: callsites@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
caniuse-lite@^1.0.30001400:
version "1.0.30001415"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001415.tgz#fd7ea96e9e94c181a7f56e7571efb43d92b860cc"
integrity sha512-ER+PfgCJUe8BqunLGWd/1EY4g8AzQcsDAVzdtMGKVtQEmKAwaFfU6vb7EAVIqTMYsqxBorYZi2+22Iouj/y7GQ==
chalk@^2.0.0: chalk@^2.0.0:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -545,6 +764,11 @@ concat-map@0.0.1:
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
convert-source-map@^1.5.0: convert-source-map@^1.5.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f"
integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==
convert-source-map@^1.7.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
@ -618,7 +842,7 @@ d@1, d@^1.0.1:
es5-ext "^0.10.50" es5-ext "^0.10.50"
type "^1.0.1" type "^1.0.1"
debug@^4.0.0, debug@^4.3.2: debug@^4.0.0, debug@^4.1.0, debug@^4.3.2:
version "4.3.4" version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -660,6 +884,11 @@ domutils@^3.0.1:
domelementtype "^2.3.0" domelementtype "^2.3.0"
domhandler "^5.0.1" domhandler "^5.0.1"
electron-to-chromium@^1.4.251:
version "1.4.271"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.271.tgz#2d9f04f6a53c70e1bb1acfaae9c39f07ca40d290"
integrity sha512-BCPBtK07xR1/uY2HFDtl3wK2De66AW4MSiPlLrnPNxKC/Qhccxd59W73654S3y6Rb/k3hmuGJOBnhjfoutetXA==
emoji-regex@^8.0.0: emoji-regex@^8.0.0:
version "8.0.0" version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
@ -743,14 +972,14 @@ event-emitter@^0.3.5:
d "1" d "1"
es5-ext "~0.10.14" es5-ext "~0.10.14"
evt@^2.4.0: evt@^2.4.13:
version "2.4.0" version "2.4.13"
resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.0.tgz#5052d3a685bba8f7e7be699b358c1781bd4037bf" resolved "https://registry.yarnpkg.com/evt/-/evt-2.4.13.tgz#5ef873159ce62e099d58801a3e4b8e0f5b648017"
integrity sha512-1V8FnExwqzX2fufT793mHfCnQay078kRdAEDHzJz863pgJK9aeDiWXZFs5aKmzJfIgi+svevtApVnleZXz4Jeg== integrity sha512-haTVOsmjzk+28zpzvVwan9Zw2rLQF2izgi7BKjAPRzZAfcv+8scL0TpM8MzvGNKFYHiy+Bq3r6FYIIUPl9kt3A==
dependencies: dependencies:
minimal-polyfills "^2.2.2" minimal-polyfills "^2.2.2"
run-exclusive "^2.2.16" run-exclusive "^2.2.18"
tsafe "^1.0.1" tsafe "^1.4.1"
execa@^5.1.1: execa@^5.1.1:
version "5.1.1" version "5.1.1"
@ -816,6 +1045,11 @@ function-bind@^1.1.1:
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d"
integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
get-caller-file@^2.0.5: get-caller-file@^2.0.5:
version "2.0.5" version "2.0.5"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
@ -843,6 +1077,11 @@ glob@^7.0.5, glob@^7.1.3:
once "^1.3.0" once "^1.3.0"
path-is-absolute "^1.0.0" path-is-absolute "^1.0.0"
globals@^11.1.0:
version "11.12.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e"
integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==
has-flag@^3.0.0: has-flag@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@ -1033,11 +1272,21 @@ isexe@^2.0.0:
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
jsesc@^2.5.1:
version "2.5.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4"
integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==
json-parse-even-better-errors@^2.3.0: json-parse-even-better-errors@^2.3.0:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d"
integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==
json5@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c"
integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==
lines-and-columns@^1.1.6: lines-and-columns@^1.1.6:
version "1.2.4" version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@ -1188,6 +1437,11 @@ minimatch@^3.0.3, minimatch@^3.1.1:
dependencies: dependencies:
brace-expansion "^1.1.7" brace-expansion "^1.1.7"
minimist@^1.2.6:
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
mkdirp@^1.0.4: mkdirp@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
@ -1210,6 +1464,11 @@ node-fetch@^2.6.7:
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-releases@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503"
integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==
noms@0.0.0: noms@0.0.0:
version "0.0.0" version "0.0.0"
resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859"
@ -1356,6 +1615,11 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.3.1: picomatch@^2.3.1:
version "2.3.1" version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
@ -1375,15 +1639,15 @@ please-upgrade-node@^3.2.0:
dependencies: dependencies:
semver-compare "^1.0.0" semver-compare "^1.0.0"
powerhooks@^0.20.15: powerhooks@^0.22.1:
version "0.20.15" version "0.22.1"
resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.20.15.tgz#e5093a11f3ed1badbc09944acf5a3fb15ac3f1d7" resolved "https://registry.yarnpkg.com/powerhooks/-/powerhooks-0.22.1.tgz#4e6c720e702e138c0869447e0d04bd0550530bbb"
integrity sha512-28xNjPTtjPPP8vyi9SWXfn0U9hjxm/UzuarD2uofIXa/CPWmkCeTtikyLoR6yuiE4pOfvdDXVfU5im40GuzJaQ== integrity sha512-wR/gVPtpOeVAkjJXAHbTB6IBAIWL3RZo69DqxPZ2rXV108cwzOW978KZKd2h7m0QHJdg4ZXYwOsZ13E1EGrTFA==
dependencies: dependencies:
evt "^2.4.0" evt "^2.4.13"
memoizee "^0.4.15" memoizee "^0.4.15"
resize-observer-polyfill "^1.5.1" resize-observer-polyfill "^1.5.1"
tsafe "^1.0.1" tsafe "^1.4.2"
prettier@^2.3.0: prettier@^2.3.0:
version "2.7.1" version "2.7.1"
@ -1463,9 +1727,9 @@ readable-stream@~2.3.6:
util-deprecate "~1.0.1" util-deprecate "~1.0.1"
regenerator-runtime@^0.13.4: regenerator-runtime@^0.13.4:
version "0.13.9" version "0.13.10"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw==
remark-parse@^9.0.0: remark-parse@^9.0.0:
version "9.0.0" version "9.0.0"
@ -1506,6 +1770,11 @@ restore-cursor@^3.1.0:
onetime "^5.1.0" onetime "^5.1.0"
signal-exit "^3.0.2" signal-exit "^3.0.2"
rfc4648@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/rfc4648/-/rfc4648-1.5.2.tgz#cf5dac417dd83e7f4debf52e3797a723c1373383"
integrity sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==
rfdc@^1.3.0: rfdc@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b"
@ -1518,10 +1787,10 @@ rimraf@^3.0.2:
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
run-exclusive@^2.2.16: run-exclusive@^2.2.18:
version "2.2.16" version "2.2.18"
resolved "https://registry.yarnpkg.com/run-exclusive/-/run-exclusive-2.2.16.tgz#8fa30a23037760af296c47872a5f6b38f25accf0" resolved "https://registry.yarnpkg.com/run-exclusive/-/run-exclusive-2.2.18.tgz#ec930edc3a7044750dc827df9372bde8f610f586"
integrity sha512-cdYv2LDvaBCRnrqXrwDFs1SgzGTx0EIsiEReTpsprEDR6hRUVlSyjoMYu+rez4S1gpz6YbOQxcmYFMXJQknVnQ== integrity sha512-TXr1Gkl1iEAOCCpBTRm/2m0+1KGjORcWpZZ+VGGTe7dYX8E4y8/fMvrHk0zf+kclec2R//tpvdBxgG0bDgaJfw==
dependencies: dependencies:
minimal-polyfills "^2.2.1" minimal-polyfills "^2.2.1"
@ -1552,6 +1821,11 @@ semver-regex@^3.1.2:
resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4" resolved "https://registry.yarnpkg.com/semver-regex/-/semver-regex-3.1.4.tgz#13053c0d4aa11d070a2f2872b6b1e3ae1e1971b4"
integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA== integrity sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==
semver@^6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
shebang-command@^2.0.0: shebang-command@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
@ -1723,24 +1997,25 @@ trough@^1.0.0:
resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406" resolved "https://registry.yarnpkg.com/trough/-/trough-1.0.5.tgz#b8b639cefad7d0bb2abd37d433ff8293efa5f406"
integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA== integrity sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==
tsafe@^1.0.1: tsafe@^1.4.1:
version "1.0.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.0.1.tgz#c8c4eb2d75d1478418a4941307c5dd667fd76d23" resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.4.1.tgz#59cdad8ac41babf88e56dcd1a683ae2fb145d059"
integrity sha512-FgJ1a4rE7YbmW5QIzpsfFl4tsAp0x74FH2bVE6qODb2U8jSrwTr5/ckIazeylme5zXndVbtgKm4BZdqmoGhiPw== integrity sha512-3IDBalvf6SyvHFS14UiwCWzqdSdo+Q0k2J7DZyJYaHW/iraW9DJpaBKDJpry3yQs3o/t/A+oGaRW3iVt2lKxzA==
tsafe@^1.4.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.4.2.tgz#1cc597f6e286ef8a64b918a0a6284bd04347e11f"
integrity sha512-KfP0PYzRjl1LY1DnJPNlD4a0tZSg4uUuZxI4aG04T+j7WJvKZbh1zpaukphhSmtoJMgX1RV8eZVKTEiH8wOGbA==
tslib@^2.1.0: tslib@^2.1.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3"
integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==
tss-react@^4.1.1: tss-react@4.4.1-rc.0:
version "4.1.1" version "4.4.1-rc.0"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.1.1.tgz#207220417e4b2f8eb26d8280ab9cdeb385063069" resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-4.4.1-rc.0.tgz#96f9c2edcb9208ae39e845e6bafc9a50c9b96c66"
integrity sha512-K1U2s/GGw+XycUjJGztJsLUhwm8KJWz5afL5WZU3SwMeQsA+gbETM6bSxVk2/DXBdw9uYLL9jkSYPAXh0tfYBw== integrity sha512-ozltdpdDrB/6Ml6UpaL/eGwGZzTXXcHnJxwwO7mxcFaZnLaetbbbQHNUerRmqr21i9vgA6HFBAzOv9pa2PUsww==
dependencies:
"@emotion/cache" "*"
"@emotion/serialize" "*"
"@emotion/utils" "*"
type-fest@^0.21.3: type-fest@^0.21.3:
version "0.21.3" version "0.21.3"
@ -1818,6 +2093,14 @@ untildify@^4.0.0:
resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b"
integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==
update-browserslist-db@^1.0.9:
version "1.0.9"
resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.9.tgz#2924d3927367a38d5c555413a7ce138fc95fcb18"
integrity sha512-/xsqn21EGVdXI3EXSum1Yckj3ZVZugqyOZQ/CxYPBD/R+ko9NSUScf8tFF4dOKY+2pvSSJA/S+5B8s4Zr4kyvg==
dependencies:
escalade "^3.1.1"
picocolors "^1.0.0"
util-deprecate@~1.0.1: util-deprecate@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"