Compare commits
381 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e18a7390c | |||
5f43f1afc6 | |||
2fc9c03430 | |||
d951a9ba02 | |||
93385af675 | |||
dd75d0ece7 | |||
dcd37ed916 | |||
2e4d722d7f | |||
09543400ca | |||
8b101e5043 | |||
b31fff9c2b | |||
0c5b100dd9 | |||
8937d19891 | |||
0fdd9e75a6 | |||
77da00c2c5 | |||
3744080d11 | |||
c9e546a8fd | |||
6691992a79 | |||
1ea0f4c339 | |||
8bfa117be2 | |||
b3acecdcea | |||
ec479c7e91 | |||
fd7760d9ed | |||
c9fcec6889 | |||
fd901ef2cf | |||
8afdaa8f0e | |||
254bfccc62 | |||
5b4aeca63c | |||
17871daf0c | |||
cdd4460968 | |||
fa6a37880b | |||
d4e1dabe12 | |||
a3fd376b24 | |||
aaac1f54e8 | |||
41c0329822 | |||
74d48fd7e1 | |||
9c3c953129 | |||
f5cae18da7 | |||
59d47592d9 | |||
2b6c991190 | |||
26020ba8bb | |||
b573bc20b5 | |||
210dbfa265 | |||
b37cac93ff | |||
eea953efb6 | |||
7ad9d7b291 | |||
20937c4f72 | |||
dbbfa07639 | |||
9e1a4cad5c | |||
02bbedcfca | |||
cd70d90914 | |||
819f297de8 | |||
0608adde89 | |||
ad7bcf4669 | |||
2eccc86e83 | |||
16d18f23a1 | |||
5631ae1b6c | |||
5fb29992f6 | |||
910d633ac2 | |||
32f8380e56 | |||
43e4dd6bb6 | |||
4f0b1688db | |||
9e75ee09bb | |||
9ae8822e00 | |||
babffd1fe6 | |||
5615d62032 | |||
4b89d15c1e | |||
815f510d5f | |||
199ba193be | |||
4ae9bd3f9a | |||
1c9cf639ea | |||
0040464ca1 | |||
79997efbb6 | |||
0e42009798 | |||
93fdcb8739 | |||
aca926e202 | |||
9941027b10 | |||
9104de4290 | |||
5dc692809c | |||
8dc1d1bd21 | |||
fe588485a9 | |||
19ef1d7025 | |||
62523a8662 | |||
6e97665e2e | |||
4988680353 | |||
c5de5c20c7 | |||
1a0fee1aa2 | |||
06a44603cd | |||
e48459762e | |||
235ebeae97 | |||
dfe909606e | |||
6fd0c7726c | |||
819e045811 | |||
1ba780598d | |||
aeb0cb3110 | |||
88923838c5 | |||
df9f6fd7fd | |||
98e46d6ac9 | |||
daff614fb4 | |||
5ea324c7f2 | |||
23fedbf94a | |||
593d66d8d6 | |||
851dcd5bf7 | |||
2e919681ae | |||
5da68cd48c | |||
27fdaeff46 | |||
53c0079656 | |||
93780b77e0 | |||
b712ed0421 | |||
ee96f1b345 | |||
d13464df3d | |||
6bde2e4d96 | |||
0a4953c020 | |||
96c488880c | |||
7e0adf3f66 | |||
09f716440a | |||
2251c84171 | |||
5cfe78dcd1 | |||
6a48325132 | |||
294be0a79a | |||
c94b264b44 | |||
7220c4e3e3 | |||
5aadeba2ec | |||
0f47a5b6ba | |||
36f32d28f2 | |||
6d69ccf229 | |||
37073b42be | |||
837501c948 | |||
b300966fa8 | |||
730eb06c84 | |||
aca8d3f4b7 | |||
b5b3af4659 | |||
6cd231426d | |||
0c7cd1cd75 | |||
2425704ead | |||
4e22159206 | |||
52cf1ba02c | |||
516e84182f | |||
a3a9853e18 | |||
08e26600fd | |||
7793c2c6ba | |||
9e826d16dd | |||
80618bbd9c | |||
38ad47ea75 | |||
45ed359bef | |||
fcc26c3e7a | |||
d4ff6b1f40 | |||
557de34eea | |||
e034dc4d90 | |||
cfbd1e5e4b | |||
0df661819f | |||
1a9f6d10d4 | |||
a787215c95 | |||
64ab400af5 | |||
a463878bf2 | |||
9f72024c61 | |||
243fbd4dc9 | |||
4e6a290693 | |||
ac05d529ca | |||
b38d79004a | |||
f4a547df11 | |||
2b87c35058 | |||
b11833e450 | |||
fa8e119514 | |||
677cb5c330 | |||
6e74c79bfe | |||
54474f5908 | |||
99cc0f519b | |||
92a01f89ef | |||
fd83a0c743 | |||
988e46c875 | |||
f081c2fc20 | |||
b4b376a1a5 | |||
0db4179d47 | |||
795b7c6234 | |||
091b9a57f5 | |||
564e1422ac | |||
8ed4ed3fc4 | |||
29fe4566a7 | |||
ae3bfb28ed | |||
14aab97d8a | |||
52d7a47cd7 | |||
f338dcbeed | |||
dcec058a22 | |||
2bdc6b156b | |||
84ca9e6b81 | |||
11cb0fd2db | |||
3f620ffb6f | |||
1a0e05d073 | |||
a4d2de23a1 | |||
85cecc9811 | |||
9899f742a8 | |||
b5484740b7 | |||
016b15b437 | |||
6fb936798e | |||
a692b87843 | |||
19663885a4 | |||
49b87777f9 | |||
d4523bb1e6 | |||
e3200899e2 | |||
36c7a1ab9e | |||
c54fbd5eca | |||
bbe828071e | |||
23f6c7db00 | |||
b1ea9e7a71 | |||
fb71d0e272 | |||
fa72a29999 | |||
af77b31d54 | |||
8280dace26 | |||
ecaf1c7b7c | |||
8702ec29a8 | |||
d8206434bc | |||
c71c2a8710 | |||
e55b881017 | |||
ab906ec417 | |||
0b1ff529f7 | |||
85a6835748 | |||
259271bc0f | |||
b7bc0f178b | |||
688455d0aa | |||
3c96d2ea42 | |||
ab81481e5a | |||
a429ad5dcf | |||
5e1c5b510b | |||
9e63183f4b | |||
b1e740f026 | |||
ce4ea55438 | |||
18ab7cd22f | |||
8807743daf | |||
aad50377ff | |||
4b3ae58ea7 | |||
ce2c68ecc9 | |||
0c155a7a2e | |||
afddfe8b58 | |||
5fa0915271 | |||
6a0a170b17 | |||
4dde5b6e45 | |||
4b93a1cb9e | |||
e3a0639a0c | |||
4d3220820b | |||
a4ac9fb0f3 | |||
1ff79ecf07 | |||
1166b16420 | |||
213224942f | |||
ff16e66275 | |||
3c338e983f | |||
2c11ba6520 | |||
9a21656706 | |||
e96ee5ba53 | |||
b421633a8a | |||
e2e0d62560 | |||
c71fb06940 | |||
e2171af99c | |||
8cebf049d4 | |||
ef139ed1cc | |||
d717de006a | |||
a44f091878 | |||
1b37ba5339 | |||
bbaa90e997 | |||
86e6c4a419 | |||
4159883791 | |||
d8b00da3a1 | |||
a24945bc1b | |||
158759493f | |||
36e32d6ddc | |||
84908e2ec0 | |||
a2dc51d811 | |||
fb3b0e2c29 | |||
1a3e4c68bb | |||
11b2342da0 | |||
80d4a808d3 | |||
da4146eb59 | |||
a0be35db8b | |||
14db9cd523 | |||
0c315385dd | |||
c0a0eb02fb | |||
ee407c32ad | |||
9262d21829 | |||
a13f710325 | |||
eac1a6036f | |||
987f3d7586 | |||
875322669c | |||
33a264b3d0 | |||
c059eff170 | |||
b4a22fc9dd | |||
6d1cbdc463 | |||
2bfbba4daf | |||
21ffe82bde | |||
8e6f597027 | |||
16c5065560 | |||
c4b985f1a4 | |||
042747c7d2 | |||
e4a46f31de | |||
6d9e62d2b4 | |||
9caaa507b1 | |||
5c7d3c5b44 | |||
8bac57d87a | |||
b8d759cd63 | |||
da72e3e5ac | |||
2afd36fee0 | |||
b7e75d8828 | |||
30e20f4e7d | |||
ce0ab8dccf | |||
5b20ab2f7c | |||
daaaed43df | |||
3a4bd791ad | |||
eecddd7f6b | |||
a34eaa136e | |||
53be8b5e96 | |||
f0ae5ea908 | |||
9910556a8b | |||
5997416e1b | |||
9a9fc56f85 | |||
2a5e919f29 | |||
8031d51e15 | |||
56ce9c0d0d | |||
8cd584cbd5 | |||
f5b87f4669 | |||
a1a65c5529 | |||
832434095e | |||
b85f1ef351 | |||
8bee5d788e | |||
0752d857e2 | |||
07e4056694 | |||
0eb4ab85b3 | |||
69ef47daf8 | |||
6eaa1f69ac | |||
5aab75fae0 | |||
7407c98005 | |||
dcd4322e44 | |||
81a4d46b08 | |||
e85895ab55 | |||
095bdb16ba | |||
68de7f897d | |||
51b4c6b1bd | |||
6d4ac977c1 | |||
a73fc5ebc1 | |||
3c8461a39f | |||
de76d06e48 | |||
a27c28c24f | |||
ed234ec88b | |||
7a0a046596 | |||
0641151ca1 | |||
c6dc2377fa | |||
a3050b3983 | |||
69c15bd473 | |||
2be40816b2 | |||
a98bb25133 | |||
d130c23f5d | |||
cd936ee4ef | |||
67e3dca0c3 | |||
de53f1ff40 | |||
d79081dee4 | |||
449f100bc0 | |||
0612b2d0a4 | |||
583a3e541a | |||
9d8d30a864 | |||
95cda8538f | |||
b116f22152 | |||
ff2fb0d6dc | |||
020823e933 | |||
5879972924 | |||
8031294230 | |||
e0a6935c49 | |||
3d581a5454 | |||
317ad8386c | |||
7ad5011280 | |||
76990702f0 | |||
9a27824fe9 | |||
d877d90bf3 | |||
0b790c47e6 | |||
6b49c8dd95 | |||
e56f9b144e | |||
3c82944daf | |||
5e3070a6c4 | |||
a3a0e9eebe | |||
c6593f03bc | |||
54dc4f650c | |||
dec7af0381 | |||
ae001eea54 | |||
4d0e17a11e |
2
.gitattributes
vendored
2
.gitattributes
vendored
@ -1,3 +1,3 @@
|
|||||||
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
src/lib/i18n/generated_kcMessages/* linguist-documentation
|
||||||
src/bin/build-keycloak-theme/index.ts -linguist-detectable
|
src/bin/keycloakify/index.ts -linguist-detectable
|
||||||
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable
|
||||||
|
4
.github/FUNDING.yaml
vendored
Normal file
4
.github/FUNDING.yaml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [garronej]
|
||||||
|
custom: ['https://www.ringerhq.com/experts/garronej']
|
25
.github/release.yaml
vendored
Normal file
25
.github/release.yaml
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
changelog:
|
||||||
|
exclude:
|
||||||
|
labels:
|
||||||
|
- ignore-for-release
|
||||||
|
authors:
|
||||||
|
- octocat
|
||||||
|
categories:
|
||||||
|
- title: Breaking Changes 🛠
|
||||||
|
labels:
|
||||||
|
- breaking
|
||||||
|
- title: Exciting New Features 🎉
|
||||||
|
labels:
|
||||||
|
- feature
|
||||||
|
- title: Fixes 🔧
|
||||||
|
labels:
|
||||||
|
- fix
|
||||||
|
- title: Documentation 🔧
|
||||||
|
labels:
|
||||||
|
- docs
|
||||||
|
- title: CI 👷
|
||||||
|
labels:
|
||||||
|
- ci
|
||||||
|
- title: Other Changes
|
||||||
|
labels:
|
||||||
|
- '*'
|
87
.github/workflows/ci.yaml
vendored
87
.github/workflows/ci.yaml
vendored
@ -9,12 +9,13 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
test_formatting:
|
test_lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
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.8.15
|
- uses: bahmutov/npm-install@v1
|
||||||
- name: If this step fails run 'yarn format' then commit again.
|
- name: If this step fails run 'yarn format' then commit again.
|
||||||
run: |
|
run: |
|
||||||
PACKAGE_MANAGER=npm
|
PACKAGE_MANAGER=npm
|
||||||
@ -23,25 +24,24 @@ 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_formatting
|
needs: test_lint
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node: [ '15', '14' ]
|
node: [ '16' ]
|
||||||
name: Test with Node v${{ matrix.node }}
|
os: [ 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.6
|
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.8.15
|
- uses: bahmutov/npm-install@v1
|
||||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
||||||
run: |
|
run: |
|
||||||
yarn build
|
yarn build
|
||||||
@ -64,73 +64,58 @@ 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.6
|
- 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
|
||||||
branch: ${{ github.head_ref || github.ref }}
|
branch: ${{ github.head_ref || github.ref }}
|
||||||
|
|
||||||
update_changelog:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: check_if_version_upgraded
|
|
||||||
if: needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true'
|
|
||||||
steps:
|
|
||||||
- uses: garronej/ts-ci@v1.1.6
|
|
||||||
with:
|
|
||||||
action_name: update_changelog
|
|
||||||
branch: ${{ github.head_ref || github.ref }}
|
|
||||||
|
|
||||||
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 the main branch
|
||||||
|
# or if we are on a branch of the repo that has an PR open on main.
|
||||||
|
if: |
|
||||||
|
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
|
||||||
|
(
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
needs.check_if_version_upgraded.outputs.is_pre_release == 'true'
|
||||||
|
)
|
||||||
needs:
|
needs:
|
||||||
- update_changelog
|
|
||||||
- check_if_version_upgraded
|
- check_if_version_upgraded
|
||||||
steps:
|
steps:
|
||||||
- name: Build GitHub release body
|
- uses: softprops/action-gh-release@v1
|
||||||
id: step1
|
|
||||||
run: |
|
|
||||||
if [ "$FROM_VERSION" = "0.0.0" ]; then
|
|
||||||
echo "::set-output name=body::🚀"
|
|
||||||
else
|
|
||||||
echo "::set-output name=body::📋 [CHANGELOG](https://github.com/$GITHUB_REPOSITORY/blob/v$TO_VERSION/CHANGELOG.md)"
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
FROM_VERSION: ${{ needs.check_if_version_upgraded.outputs.from_version }}
|
|
||||||
TO_VERSION: ${{ needs.check_if_version_upgraded.outputs.to_version }}
|
|
||||||
- uses: garronej/action-gh-release@v0.2.0
|
|
||||||
with:
|
with:
|
||||||
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
name: Release v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||||
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
tag_name: v${{ needs.check_if_version_upgraded.outputs.to_version }}
|
||||||
target_commitish: ${{ github.head_ref || github.ref }}
|
target_commitish: ${{ github.head_ref || github.ref }}
|
||||||
body: ${{ steps.step1.outputs.body }}
|
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 }}
|
||||||
|
|
||||||
publish_on_npm:
|
publish_on_npm:
|
||||||
runs-on: macos-10.15
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
- update_changelog
|
- 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.8.15
|
- uses: bahmutov/npm-install@v1
|
||||||
- run: |
|
- run: |
|
||||||
PACKAGE_MANAGER=npm
|
PACKAGE_MANAGER=npm
|
||||||
if [ -f "./yarn.lock" ]; then
|
if [ -f "./yarn.lock" ]; then
|
||||||
PACKAGE_MANAGER=yarn
|
PACKAGE_MANAGER=yarn
|
||||||
fi
|
fi
|
||||||
$PACKAGE_MANAGER run build
|
$PACKAGE_MANAGER run build
|
||||||
- run: npx -y -p denoify@0.6.5 denoify_enable_short_npm_import_path
|
- run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
|
||||||
env:
|
env:
|
||||||
DRY_RUN: "0"
|
DRY_RUN: "0"
|
||||||
- name: Publishing on NPM
|
- name: Publishing on NPM
|
||||||
@ -144,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 }}
|
||||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -41,6 +41,7 @@ jspm_packages
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/dist
|
/dist
|
||||||
|
/dist_test
|
||||||
|
|
||||||
/sample_react_project/
|
/sample_react_project/
|
||||||
/.yarn_home/
|
/.yarn_home/
|
||||||
@ -49,3 +50,5 @@ jspm_packages
|
|||||||
|
|
||||||
/keycloak_email
|
/keycloak_email
|
||||||
/build_keycloak
|
/build_keycloak
|
||||||
|
/src/login/i18n/baseMessages/
|
||||||
|
/src/account/i18n/baseMessages/
|
@ -1,7 +1,12 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
/dist/
|
/dist/
|
||||||
|
/dist_test/
|
||||||
/CHANGELOG.md
|
/CHANGELOG.md
|
||||||
/.yarn_home/
|
/.yarn_home/
|
||||||
/src/test/apps/
|
/src/test/apps/
|
||||||
/src/tools/types/
|
/src/tools/types/
|
||||||
/sample_react_project
|
/sample_react_project
|
||||||
|
/build_keycloak/
|
||||||
|
/.vscode/
|
||||||
|
/src/login/i18n/baseMessages/
|
||||||
|
/src/account/i18n/baseMessages/
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
"semi": true,
|
"semi": true,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"quoteProps": "preserve",
|
"quoteProps": "preserve",
|
||||||
"trailingComma": "all",
|
"trailingComma": "none",
|
||||||
"bracketSpacing": true,
|
"bracketSpacing": true,
|
||||||
"arrowParens": "avoid"
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
1017
CHANGELOG.md
1017
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
86
README.md
86
README.md
@ -8,9 +8,6 @@
|
|||||||
<a href="https://github.com/garronej/keycloakify/actions">
|
<a href="https://github.com/garronej/keycloakify/actions">
|
||||||
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
<img src="https://github.com/garronej/keycloakify/workflows/ci/badge.svg?branch=main">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://bundlephobia.com/package/keycloakify">
|
|
||||||
<img src="https://img.shields.io/bundlephobia/minzip/keycloakify">
|
|
||||||
</a>
|
|
||||||
<a href="https://www.npmjs.com/package/keycloakify">
|
<a href="https://www.npmjs.com/package/keycloakify">
|
||||||
<img src="https://img.shields.io/npm/dm/keycloakify">
|
<img src="https://img.shields.io/npm/dm/keycloakify">
|
||||||
</a>
|
</a>
|
||||||
@ -27,8 +24,9 @@
|
|||||||
<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>
|
-
|
||||||
|
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||||
|
</p>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@ -36,8 +34,82 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> 🗣 V6 have been released 🎉
|
||||||
|
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
|
||||||
|
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
|
||||||
|
|
||||||
# Changelog highlights
|
# Changelog highlights
|
||||||
|
|
||||||
|
## 7.0 🍾
|
||||||
|
|
||||||
|
- Account theme support 🚀
|
||||||
|
- It's much easier to customize pages at the CSS level, you can now see in the browser dev tool the customizable classes.
|
||||||
|
- New interactive CLI tool `npx eject-keycloak-page`, that enables to select the page you want to customize at the component level.
|
||||||
|
|
||||||
|
## 6.13
|
||||||
|
|
||||||
|
- Build work behind corporate proxies, [see issue](https://github.com/InseeFrLab/keycloakify/issues/257).
|
||||||
|
|
||||||
|
## 6.12
|
||||||
|
|
||||||
|
Massive improvement in the developer experience:
|
||||||
|
|
||||||
|
- There is now only one starter repo: https://github.com/codegouvfr/keycloakify-starter
|
||||||
|
- A lot of comments have been added in the code of the starter to make it easier to get started.
|
||||||
|
- The doc has been updated: https://docs.keycloakify.dev
|
||||||
|
- A lot of improvements in the type system.
|
||||||
|
|
||||||
|
## 6.11.4
|
||||||
|
|
||||||
|
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/InseeFrLab/keycloakify/pull/239).
|
||||||
|
- Feature new build options: [`bundler`](https://docs.keycloakify.dev/build-options#keycloakify.bundler), [`groupId`](https://docs.keycloakify.dev/build-options#keycloakify.groupid), [`artifactId`](https://docs.keycloakify.dev/build-options#keycloakify.artifactid), [`version`](https://docs.keycloakify.dev/build-options#version).
|
||||||
|
Theses options can be user to customize the output name of the .jar. You can use environnement variables to overrides the values read in the package.json. Thanks to @lordvlad.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
|
- Bundle size drastically reduced, locals and component dynamically loaded.
|
||||||
|
- First print much quicker, use of React.lazy() everywhere.
|
||||||
|
- Real i18n API.
|
||||||
|
- Actual documentation for build options.
|
||||||
|
|
||||||
|
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
|
||||||
|
|
||||||
|
## 5.8.0
|
||||||
|
|
||||||
|
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/InseeFrLab/keycloakify/issues/141)
|
||||||
|
|
||||||
## 5.7.0
|
## 5.7.0
|
||||||
|
|
||||||
- Feat `logout-confirm.ftl`. [PR](https://github.com/InseeFrLab/keycloakify/pull/120)
|
- Feat `logout-confirm.ftl`. [PR](https://github.com/InseeFrLab/keycloakify/pull/120)
|
||||||
@ -57,7 +129,9 @@ If you already had a `keycloak_theme_email` you should rename it `keycloak_email
|
|||||||
|
|
||||||
## v5.0.0
|
## v5.0.0
|
||||||
|
|
||||||
New i18n system. Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
|
[Migration guide](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63)
|
||||||
|
New i18n system.
|
||||||
|
Import of terms and services have changed. [See example](https://github.com/garronej/keycloakify-demo-app/blob/a5b6a50f24bc25e082931f5ad9ebf47492acd12a/src/index.tsx#L46-L63).
|
||||||
|
|
||||||
## v4.10.0
|
## v4.10.0
|
||||||
|
|
||||||
|
71
package.json
Executable file → Normal file
71
package.json
Executable file → Normal file
@ -1,29 +1,35 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "5.7.2",
|
"version": "7.0.0",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Create Keycloak themes using React",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/garronej/keycloakify.git"
|
"url": "git://github.com/inseefrlab/keycloakify.git"
|
||||||
},
|
},
|
||||||
"main": "dist/lib/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/lib/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rimraf dist/",
|
"prepare": "yarn generate-i18n-messages",
|
||||||
"build": "yarn clean && tsc && yarn grant-exec-perms && yarn copy-files",
|
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/tsconfig.json && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
|
||||||
|
"build:watch": "tsc -p src/tsconfig.json && (concurrently \"tsc -p src/tsconfig.json -w\" \"tsc-alias -p src/tsconfig.json\")",
|
||||||
|
"build:test": "rimraf dist_test/ && tsc -p test/tsconfig.json && tsc-alias -p test/tsconfig.json && yarn copy-files dist_test/src",
|
||||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||||
"test": "node dist/test/bin/main && node dist/test/lib",
|
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
||||||
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.xml src/**/*.js dist/",
|
"test": "yarn build:test && node dist_test/test/bin && node dist_test/test/lib",
|
||||||
"generate-messages": "node dist/bin/generate-i18n-messages.js",
|
"test:sample-app": "yarn build:test && node dist_test/test/bin/main.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}'",
|
||||||
"format": "yarn _format --write",
|
"format": "yarn _format --write",
|
||||||
"format:check": "yarn _format --list-different"
|
"format:check": "yarn _format --list-different",
|
||||||
|
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
|
||||||
|
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
|
||||||
|
"link-in-starter": "yarn link-in-app keycloakify-starter",
|
||||||
|
"tsc-watch": "tsc -p src/bin -w & tsc -p src/lib -w "
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
|
"keycloakify": "dist/bin/keycloakify/index.js",
|
||||||
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
|
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
|
||||||
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
|
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
|
||||||
|
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{ts,tsx,json,md}": [
|
"*.{ts,tsx,json,md}": [
|
||||||
@ -39,10 +45,9 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"files": [
|
"files": [
|
||||||
"src/",
|
"src/",
|
||||||
"!src/test/",
|
|
||||||
"dist/",
|
"dist/",
|
||||||
"!dist/test/",
|
"!dist/tsconfig.tsbuildinfo",
|
||||||
"!dist/tsconfig.tsbuildinfo"
|
"!dist/bin/tsconfig.tsbuildinfo"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bluehats",
|
"bluehats",
|
||||||
@ -54,16 +59,17 @@
|
|||||||
"login",
|
"login",
|
||||||
"register"
|
"register"
|
||||||
],
|
],
|
||||||
"homepage": "https://github.com/garronej/keycloakify",
|
"homepage": "https://www.keycloakify.dev",
|
||||||
"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/make-fetch-happen": "^10.0.1",
|
||||||
"@types/node": "^17.0.25",
|
"@types/minimist": "^1.2.2",
|
||||||
|
"@types/node": "^18.15.3",
|
||||||
"@types/react": "18.0.9",
|
"@types/react": "18.0.9",
|
||||||
|
"concurrently": "^7.6.0",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
@ -71,20 +77,23 @@
|
|||||||
"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",
|
||||||
"typescript": "^4.2.3"
|
"scripting-tools": "^0.19.13",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsc-alias": "^1.8.3",
|
||||||
|
"typescript": "^5.0.1-rc"
|
||||||
},
|
},
|
||||||
"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.0.0-beta.45",
|
"evt": "^2.4.18",
|
||||||
"memoizee": "^0.4.15",
|
"make-fetch-happen": "^11.0.3",
|
||||||
"minimal-polyfills": "^2.2.1",
|
"minimal-polyfills": "^2.2.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
"powerhooks": "^0.20.3",
|
|
||||||
"react-markdown": "^5.0.3",
|
"react-markdown": "^5.0.3",
|
||||||
"scripting-tools": "^0.19.13",
|
"rfc4648": "^1.5.2",
|
||||||
"tsafe": "^0.10.0",
|
"tsafe": "^1.6.0",
|
||||||
"tss-react": "^3.7.0"
|
"zod": "^3.17.10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
27
renovate.json
Normal file
27
renovate.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"baseBranches": ["main", "landingpage"],
|
||||||
|
"extends": ["config:base"],
|
||||||
|
"dependencyDashboard": false,
|
||||||
|
"bumpVersion": "patch",
|
||||||
|
"rangeStrategy": "bump",
|
||||||
|
"ignorePaths": [".github/**"],
|
||||||
|
"branchPrefix": "renovate_",
|
||||||
|
"vulnerabilityAlerts": {
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"packagePatterns": ["*"],
|
||||||
|
"excludePackagePatterns": ["tsafe", "evt"],
|
||||||
|
"enabled": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"packagePatterns": ["tsafe", "evt"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"automerge": true,
|
||||||
|
"automergeType": "branch",
|
||||||
|
"groupName": "garronej_modules_update"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
117
scripts/generate-i18n-messages.ts
Normal file
117
scripts/generate-i18n-messages.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
|
||||||
|
import { crawl } from "../src/bin/tools/crawl";
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "../src/bin/download-builtin-keycloak-theme";
|
||||||
|
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||||
|
import { getCliOptions } from "../src/bin/tools/cliOptions";
|
||||||
|
import { getLogger } from "../src/bin/tools/logger";
|
||||||
|
|
||||||
|
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
|
// update the version array for generating for newer version.
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
const propertiesParser = require("properties-parser");
|
||||||
|
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const keycloakVersion = "21.0.1";
|
||||||
|
|
||||||
|
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
||||||
|
|
||||||
|
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
type Dictionary = { [idiomId: string]: string };
|
||||||
|
|
||||||
|
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
|
{
|
||||||
|
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
||||||
|
|
||||||
|
crawl(baseThemeDirPath).forEach(filePath => {
|
||||||
|
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/);
|
||||||
|
|
||||||
|
if (match === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, typeOfPage, language] = match;
|
||||||
|
|
||||||
|
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||||
|
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
||||||
|
([key, value]: any) => [key, value.replace(/''/g, "'")]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(tmpDirPath, { recursive: true, force: true });
|
||||||
|
|
||||||
|
Object.keys(record).forEach(themeType => {
|
||||||
|
const recordForPageType = record[themeType];
|
||||||
|
|
||||||
|
if (themeType !== "login" && themeType !== "account") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseMessagesDirPath = pathJoin(getProjectRoot(), "src", themeType, "i18n", "baseMessages");
|
||||||
|
|
||||||
|
const languages = Object.keys(recordForPageType);
|
||||||
|
|
||||||
|
const generatedFileHeader = [
|
||||||
|
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
|
||||||
|
"//PLEASE DO NOT EDIT MANUALLY",
|
||||||
|
""
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
languages.forEach(language => {
|
||||||
|
const filePath = pathJoin(baseMessagesDirPath, `${language}.ts`);
|
||||||
|
|
||||||
|
fs.mkdirSync(pathDirname(filePath), { "recursive": true });
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
generatedFileHeader,
|
||||||
|
"/* spell-checker: disable */",
|
||||||
|
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
|
||||||
|
"",
|
||||||
|
"export default messages;",
|
||||||
|
"/* spell-checker: enable */"
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.log(`${filePath} wrote`);
|
||||||
|
});
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(baseMessagesDirPath, "index.ts"),
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
generatedFileHeader,
|
||||||
|
"export async function getMessages(currentLanguageTag: string) {",
|
||||||
|
" const { default: messages } = await (() => {",
|
||||||
|
" switch (currentLanguageTag) {",
|
||||||
|
...languages.map(language => ` case "${language}": return import("./${language}");`),
|
||||||
|
' default: return { "default": {} };',
|
||||||
|
" }",
|
||||||
|
" })();",
|
||||||
|
" return messages;",
|
||||||
|
"}"
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})();
|
143
scripts/link-in-app.ts
Normal file
143
scripts/link-in-app.ts
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import { execSync } from "child_process";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { getProjectRoot } from "../src/bin/tools/getProjectRoot";
|
||||||
|
import * as fs from "fs";
|
||||||
|
|
||||||
|
const singletonDependencies: string[] = ["react", "@types/react"];
|
||||||
|
|
||||||
|
const rootDirPath = getProjectRoot();
|
||||||
|
|
||||||
|
//NOTE: This is only required because of: https://github.com/garronej/ts-ci/blob/c0e207b9677523d4ec97fe672ddd72ccbb3c1cc4/README.md?plain=1#L54-L58
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(rootDirPath, "dist", "package.json"),
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify(
|
||||||
|
(() => {
|
||||||
|
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
...packageJsonParsed,
|
||||||
|
"main": packageJsonParsed["main"]?.replace(/^dist\//, ""),
|
||||||
|
"types": packageJsonParsed["types"]?.replace(/^dist\//, ""),
|
||||||
|
"module": packageJsonParsed["module"]?.replace(/^dist\//, ""),
|
||||||
|
"exports": !("exports" in packageJsonParsed)
|
||||||
|
? undefined
|
||||||
|
: Object.fromEntries(
|
||||||
|
Object.entries(packageJsonParsed["exports"]).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
(value as string).replace(/^\.\/dist\//, "./")
|
||||||
|
])
|
||||||
|
)
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
null,
|
||||||
|
2
|
||||||
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.cpSync(pathJoin(rootDirPath, "src"), pathJoin(rootDirPath, "dist", "src"), { "recursive": true });
|
||||||
|
|
||||||
|
const commonThirdPartyDeps = (() => {
|
||||||
|
// For example [ "@emotion" ] it's more convenient than
|
||||||
|
// having to list every sub emotion packages (@emotion/css @emotion/utils ...)
|
||||||
|
// in singletonDependencies
|
||||||
|
const namespaceSingletonDependencies: string[] = [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
...namespaceSingletonDependencies
|
||||||
|
.map(namespaceModuleName =>
|
||||||
|
fs
|
||||||
|
.readdirSync(pathJoin(rootDirPath, "node_modules", namespaceModuleName))
|
||||||
|
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
|
||||||
|
)
|
||||||
|
.reduce((prev, curr) => [...prev, ...curr], []),
|
||||||
|
...singletonDependencies
|
||||||
|
];
|
||||||
|
})();
|
||||||
|
|
||||||
|
const yarnGlobalDirPath = pathJoin(rootDirPath, ".yarn_home");
|
||||||
|
|
||||||
|
fs.rmSync(yarnGlobalDirPath, { "recursive": true, "force": true });
|
||||||
|
fs.mkdirSync(yarnGlobalDirPath);
|
||||||
|
|
||||||
|
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
||||||
|
const { targetModuleName, cwd } = params;
|
||||||
|
|
||||||
|
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : ["--no-bin-links"])].join(" ");
|
||||||
|
|
||||||
|
console.log(`$ cd ${pathRelative(rootDirPath, cwd) || "."} && ${cmd}`);
|
||||||
|
|
||||||
|
execSync(cmd, {
|
||||||
|
cwd,
|
||||||
|
"env": {
|
||||||
|
...process.env,
|
||||||
|
"HOME": yarnGlobalDirPath
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAppPaths = (() => {
|
||||||
|
const [, , ...testAppNames] = process.argv;
|
||||||
|
|
||||||
|
return testAppNames
|
||||||
|
.map(testAppName => {
|
||||||
|
const testAppPath = pathJoin(rootDirPath, "..", testAppName);
|
||||||
|
|
||||||
|
if (fs.existsSync(testAppPath)) {
|
||||||
|
return testAppPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
.filter((path): path is string => path !== undefined);
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (testAppPaths.length === 0) {
|
||||||
|
console.error("No test app to link into!");
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
|
||||||
|
|
||||||
|
console.log("=== Linking common dependencies ===");
|
||||||
|
|
||||||
|
const total = commonThirdPartyDeps.length;
|
||||||
|
let current = 0;
|
||||||
|
|
||||||
|
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
||||||
|
current++;
|
||||||
|
|
||||||
|
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
||||||
|
|
||||||
|
const localInstallPath = pathJoin(
|
||||||
|
...[rootDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
|
||||||
|
);
|
||||||
|
|
||||||
|
execYarnLink({ "cwd": localInstallPath });
|
||||||
|
});
|
||||||
|
|
||||||
|
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
|
||||||
|
testAppPaths.forEach(testAppPath =>
|
||||||
|
execYarnLink({
|
||||||
|
"cwd": testAppPath,
|
||||||
|
"targetModuleName": commonThirdPartyDep
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("=== Linking in house dependencies ===");
|
||||||
|
|
||||||
|
execYarnLink({ "cwd": pathJoin(rootDirPath, "dist") });
|
||||||
|
|
||||||
|
testAppPaths.forEach(testAppPath =>
|
||||||
|
execYarnLink({
|
||||||
|
"cwd": testAppPath,
|
||||||
|
"targetModuleName": JSON.parse(fs.readFileSync(pathJoin(rootDirPath, "package.json")).toString("utf8"))["name"]
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export {};
|
26
src/account/Fallback.tsx
Normal file
26
src/account/Fallback.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { lazy, Suspense } from "react";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import type { I18n } from "keycloakify/account/i18n";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import { assert, type Equals } from "tsafe/assert";
|
||||||
|
|
||||||
|
const Password = lazy(() => import("keycloakify/account/pages/Password"));
|
||||||
|
const Account = lazy(() => import("keycloakify/account/pages/Account"));
|
||||||
|
|
||||||
|
export default function Fallback(props: PageProps<KcContext, I18n>) {
|
||||||
|
const { kcContext, ...rest } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
{(() => {
|
||||||
|
switch (kcContext.pageId) {
|
||||||
|
case "password.ftl":
|
||||||
|
return <Password kcContext={kcContext} {...rest} />;
|
||||||
|
case "account.ftl":
|
||||||
|
return <Account kcContext={kcContext} {...rest} />;
|
||||||
|
}
|
||||||
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
|
})()}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
130
src/account/Template.tsx
Normal file
130
src/account/Template.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
|
||||||
|
import { type TemplateProps } from "keycloakify/account/TemplateProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import type { I18n } from "./i18n";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
|
||||||
|
export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
|
||||||
|
|
||||||
|
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
|
||||||
|
|
||||||
|
const { locale, url, features, realm, message, referrer } = kcContext;
|
||||||
|
|
||||||
|
const { isReady } = usePrepareTemplate({
|
||||||
|
"doFetchDefaultThemeResources": doUseDefaultCss,
|
||||||
|
url,
|
||||||
|
"stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
|
||||||
|
"styles": ["css/account.css"],
|
||||||
|
"htmlClassName": undefined,
|
||||||
|
"bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isReady) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<header className="navbar navbar-default navbar-pf navbar-main header">
|
||||||
|
<nav className="navbar" role="navigation">
|
||||||
|
<div className="navbar-header">
|
||||||
|
<div className="container">
|
||||||
|
<h1 className="navbar-title">Keycloak</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="navbar-collapse navbar-collapse-1">
|
||||||
|
<div className="container">
|
||||||
|
<ul className="nav navbar-nav navbar-utility">
|
||||||
|
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
|
||||||
|
<li>
|
||||||
|
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||||
|
<a href="#" id="kc-current-locale-link">
|
||||||
|
{labelBySupportedLanguageTag[currentLanguageTag]}
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
{locale.supported.map(({ languageTag }) => (
|
||||||
|
<li key={languageTag} className="kc-dropdown-item">
|
||||||
|
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||||
|
<a href="#" onClick={() => changeLocale(languageTag)}>
|
||||||
|
{labelBySupportedLanguageTag[languageTag]}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{referrer?.url !== undefined && (
|
||||||
|
<li>
|
||||||
|
<a href={referrer.url} id="referrer">
|
||||||
|
{msg("backTo", referrer.name)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<a href={url.getLogoutUrl()}>{msg("doSignOut")}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="container">
|
||||||
|
<div className="bs-sidebar col-sm-3">
|
||||||
|
<ul>
|
||||||
|
<li className={clsx(active === "account" && "active")}>
|
||||||
|
<a href={url.accountUrl}>{msg("account")}</a>
|
||||||
|
</li>
|
||||||
|
{features.passwordUpdateSupported && (
|
||||||
|
<li className={clsx(active === "password" && "active")}>
|
||||||
|
<a href={url.passwordUrl}>{msg("password")}</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li className={clsx(active === "totp" && "active")}>
|
||||||
|
<a href={url.totpUrl}>{msg("authenticator")}</a>
|
||||||
|
</li>
|
||||||
|
{features.identityFederation && (
|
||||||
|
<li className={clsx(active === "social" && "active")}>
|
||||||
|
<a href={url.socialUrl}>{msg("federatedIdentity")}</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li className={clsx(active === "sessions" && "active")}>
|
||||||
|
<a href={url.sessionsUrl}>{msg("sessions")}</a>
|
||||||
|
</li>
|
||||||
|
<li className={clsx(active === "applications" && "active")}>
|
||||||
|
<a href={url.applicationsUrl}>{msg("applications")}</a>
|
||||||
|
</li>
|
||||||
|
{features.log && (
|
||||||
|
<li className={clsx(active === "log" && "active")}>
|
||||||
|
<a href={url.logUrl}>{msg("log")}</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{realm.userManagedAccessAllowed && features.authorization && (
|
||||||
|
<li className={clsx(active === "authorization" && "active")}>
|
||||||
|
<a href={url.resourceUrl}>{msg("myResources")}</a>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-9 content-area">
|
||||||
|
{message !== undefined && (
|
||||||
|
<div className={clsx("alert", `alert-${message.type}`)}>
|
||||||
|
{message.type === "success" && <span className="pficon pficon-ok"></span>}
|
||||||
|
{message.type === "error" && <span className="pficon pficon-error-circle-o"></span>}
|
||||||
|
<span className="kc-feedback-text">{message.summary}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
14
src/account/TemplateProps.ts
Normal file
14
src/account/TemplateProps.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { KcContext } from "./kcContext";
|
||||||
|
import type { I18n } from "./i18n";
|
||||||
|
|
||||||
|
export type TemplateProps<KcContext extends KcContext.Common, I18nExtended extends I18n> = {
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18nExtended;
|
||||||
|
doUseDefaultCss: boolean;
|
||||||
|
active: string;
|
||||||
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClassKey = "kcBodyClass" | "kcButtonClass" | "kcButtonPrimaryClass" | "kcButtonLargeClass" | "kcButtonDefaultClass";
|
229
src/account/i18n/i18n.tsx
Normal file
229
src/account/i18n/i18n.tsx
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import fallbackMessages from "./baseMessages/en";
|
||||||
|
import { getMessages } from "./baseMessages";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { KcContext } from "../kcContext/KcContext";
|
||||||
|
import { Markdown } from "keycloakify/tools/Markdown";
|
||||||
|
|
||||||
|
export const fallbackLanguageTag = "en";
|
||||||
|
|
||||||
|
export type KcContextLike = {
|
||||||
|
locale?: {
|
||||||
|
currentLanguageTag: string;
|
||||||
|
supported: { languageTag: string; url: string; label: string }[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
assert<KcContext extends KcContextLike ? true : false>();
|
||||||
|
|
||||||
|
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||||
|
|
||||||
|
export type GenericI18n<MessageKey extends string> = {
|
||||||
|
/**
|
||||||
|
* e.g: "en", "fr", "zh-CN"
|
||||||
|
*
|
||||||
|
* The current language
|
||||||
|
*/
|
||||||
|
currentLanguageTag: string;
|
||||||
|
/**
|
||||||
|
* To call when the user switch language.
|
||||||
|
* This will cause the page to be reloaded,
|
||||||
|
* on next load currentLanguageTag === newLanguageTag
|
||||||
|
*/
|
||||||
|
changeLocale: (newLanguageTag: string) => never;
|
||||||
|
/**
|
||||||
|
* e.g. "en" => "English", "fr" => "Français", ...
|
||||||
|
*
|
||||||
|
* Used to render a select that enable user to switch language.
|
||||||
|
* ex: https://user-images.githubusercontent.com/6702424/186044799-38801eec-4e89-483b-81dd-8e9233e8c0eb.png
|
||||||
|
* */
|
||||||
|
labelBySupportedLanguageTag: Record<string, string>;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
*
|
||||||
|
* msg("access-denied") === <span>Access denied</span>
|
||||||
|
* msg("impersonateTitleHtml", "Foo") === <span><strong>Foo</strong> Impersonate User</span>
|
||||||
|
*/
|
||||||
|
msg: (key: MessageKey, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* It's the same thing as msg() but instead of returning a JSX.Element it returns a string.
|
||||||
|
* It can be more convenient to manipulate strings but if there are HTML tags it wont render.
|
||||||
|
* msgStr("impersonateTitleHtml", "Foo") === "<strong>Foo</strong> Impersonate User"
|
||||||
|
*/
|
||||||
|
msgStr: (key: MessageKey, ...args: (string | undefined)[]) => string;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* advancedMsg("${access-denied} foo bar") === <span>${msgStr("access-denied")} foo bar<span> === <span>Access denied foo bar</span>
|
||||||
|
* advancedMsg("${access-denied}") === advancedMsg("access-denied") === msg("access-denied") === <span>Access denied</span>
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === <span>not-a-message-key</span>
|
||||||
|
*/
|
||||||
|
advancedMsg: (key: string, ...args: (string | undefined)[]) => JSX.Element;
|
||||||
|
/**
|
||||||
|
* Examples assuming currentLanguageTag === "en"
|
||||||
|
* advancedMsg("${access-denied} foo bar") === msg("access-denied") + " foo bar" === "Access denied foo bar"
|
||||||
|
* advancedMsg("${not-a-message-key}") === advancedMsg(not-a-message-key") === "not-a-message-key"
|
||||||
|
*/
|
||||||
|
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type I18n = GenericI18n<MessageKey>;
|
||||||
|
|
||||||
|
export function createUseI18n<ExtraMessageKey extends string = never>(extraMessages: {
|
||||||
|
[languageTag: string]: { [key in ExtraMessageKey]: string };
|
||||||
|
}) {
|
||||||
|
function useI18n(params: { kcContext: KcContextLike }): GenericI18n<MessageKey | ExtraMessageKey> | null {
|
||||||
|
const { kcContext } = params;
|
||||||
|
|
||||||
|
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||||
|
|
||||||
|
const refHasStartedFetching = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (refHasStartedFetching.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refHasStartedFetching.current = true;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||||
|
|
||||||
|
setI18n({
|
||||||
|
...createI18nTranslationFunctions({
|
||||||
|
"fallbackMessages": {
|
||||||
|
...fallbackMessages,
|
||||||
|
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||||
|
} as any,
|
||||||
|
"messages": {
|
||||||
|
...(await getMessages(currentLanguageTag)),
|
||||||
|
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||||
|
...(extraMessages[currentLanguageTag] ?? {})
|
||||||
|
} as any
|
||||||
|
}),
|
||||||
|
currentLanguageTag,
|
||||||
|
"changeLocale": newLanguageTag => {
|
||||||
|
const { locale } = kcContext;
|
||||||
|
|
||||||
|
assert(locale !== undefined, "Internationalization not enabled");
|
||||||
|
|
||||||
|
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||||
|
|
||||||
|
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||||
|
|
||||||
|
window.location.href = targetSupportedLocale.url;
|
||||||
|
|
||||||
|
assert(false, "never");
|
||||||
|
},
|
||||||
|
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||||
|
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return i18n ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { useI18n };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||||
|
fallbackMessages: Record<MessageKey, string>;
|
||||||
|
messages: Record<MessageKey, string>;
|
||||||
|
}): Pick<GenericI18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||||
|
const { fallbackMessages, messages } = params;
|
||||||
|
|
||||||
|
function resolveMsg(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): string | JSX.Element | undefined {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const messageOrUndefined: string | undefined = (messages as any)[key] ?? (fallbackMessages as any)[key];
|
||||||
|
|
||||||
|
if (messageOrUndefined === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = messageOrUndefined;
|
||||||
|
|
||||||
|
const messageWithArgsInjectedIfAny = (() => {
|
||||||
|
const startIndex = message
|
||||||
|
.match(/{[0-9]+}/g)
|
||||||
|
?.map(g => g.match(/{([0-9]+)}/)![1])
|
||||||
|
.map(indexStr => parseInt(indexStr))
|
||||||
|
.sort((a, b) => a - b)[0];
|
||||||
|
|
||||||
|
if (startIndex === undefined) {
|
||||||
|
// No {0} in message (no arguments expected)
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
let messageWithArgsInjected = message;
|
||||||
|
|
||||||
|
args.forEach((arg, i) => {
|
||||||
|
if (arg === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messageWithArgsInjected = messageWithArgsInjected.replace(new RegExp(`\\{${i + startIndex}\\}`, "g"), arg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return messageWithArgsInjected;
|
||||||
|
})();
|
||||||
|
|
||||||
|
return doRenderMarkdown ? (
|
||||||
|
<Markdown allowDangerousHtml renderers={{ "paragraph": "span" }}>
|
||||||
|
{messageWithArgsInjectedIfAny}
|
||||||
|
</Markdown>
|
||||||
|
) : (
|
||||||
|
messageWithArgsInjectedIfAny
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMsgAdvanced(props: { key: string; args: (string | undefined)[]; doRenderMarkdown: boolean }): JSX.Element | string {
|
||||||
|
const { key, args, doRenderMarkdown } = props;
|
||||||
|
|
||||||
|
const match = key.match(/^\$\{([^{]+)\}$/);
|
||||||
|
|
||||||
|
const keyUnwrappedFromCurlyBraces = match === null ? key : match[1];
|
||||||
|
|
||||||
|
const out = resolveMsg({
|
||||||
|
"key": keyUnwrappedFromCurlyBraces,
|
||||||
|
args,
|
||||||
|
doRenderMarkdown
|
||||||
|
});
|
||||||
|
|
||||||
|
return (out !== undefined ? out : doRenderMarkdown ? <span>{keyUnwrappedFromCurlyBraces}</span> : keyUnwrappedFromCurlyBraces) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"msgStr": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": false }) as string,
|
||||||
|
"msg": (key, ...args) => resolveMsg({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsg": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": true }) as JSX.Element,
|
||||||
|
"advancedMsgStr": (key, ...args) => resolveMsgAdvanced({ key, args, "doRenderMarkdown": false }) as string
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const keycloakifyExtraMessages = {
|
||||||
|
"en": {
|
||||||
|
"shouldBeEqual": "{0} should be equal to {1}",
|
||||||
|
"shouldBeDifferent": "{0} should be different to {1}",
|
||||||
|
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Must be an integer",
|
||||||
|
"notAValidOption": "Not a valid option"
|
||||||
|
},
|
||||||
|
"fr": {
|
||||||
|
/* spell-checker: disable */
|
||||||
|
"shouldBeEqual": "{0} doit être égal à {1}",
|
||||||
|
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||||
|
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||||
|
"mustBeAnInteger": "Doit être un nombre entier",
|
||||||
|
"notAValidOption": "N'est pas une option valide",
|
||||||
|
|
||||||
|
"logoutConfirmTitle": "Déconnexion",
|
||||||
|
"logoutConfirmHeader": "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
|
||||||
|
"doLogout": "Se déconnecter"
|
||||||
|
/* spell-checker: enable */
|
||||||
|
}
|
||||||
|
};
|
1
src/account/i18n/index.ts
Normal file
1
src/account/i18n/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type { I18n } from "./i18n";
|
8
src/account/index.ts
Normal file
8
src/account/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import Fallback from "keycloakify/account/Fallback";
|
||||||
|
|
||||||
|
export default Fallback;
|
||||||
|
|
||||||
|
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||||
|
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||||
|
|
||||||
|
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
83
src/account/kcContext/KcContext.ts
Normal file
83
src/account/kcContext/KcContext.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import type { AccountThemePageId } from "keycloakify/bin/keycloakify/generateFtl";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
|
export type KcContext = KcContext.Password | KcContext.Account;
|
||||||
|
|
||||||
|
export declare namespace KcContext {
|
||||||
|
export type Common = {
|
||||||
|
locale?: {
|
||||||
|
supported: {
|
||||||
|
url: string;
|
||||||
|
label: string;
|
||||||
|
languageTag: string;
|
||||||
|
}[];
|
||||||
|
currentLanguageTag: string;
|
||||||
|
};
|
||||||
|
url: {
|
||||||
|
accountUrl: string;
|
||||||
|
passwordUrl: string;
|
||||||
|
totpUrl: string;
|
||||||
|
socialUrl: string;
|
||||||
|
sessionsUrl: string;
|
||||||
|
applicationsUrl: string;
|
||||||
|
logUrl: string;
|
||||||
|
resourceUrl: string;
|
||||||
|
resourcesCommonPath: string;
|
||||||
|
resourcesPath: string;
|
||||||
|
getLogoutUrl: () => string;
|
||||||
|
};
|
||||||
|
features: {
|
||||||
|
passwordUpdateSupported: boolean;
|
||||||
|
identityFederation: boolean;
|
||||||
|
log: boolean;
|
||||||
|
authorization: boolean;
|
||||||
|
};
|
||||||
|
realm: {
|
||||||
|
internationalizationEnabled: boolean;
|
||||||
|
userManagedAccessAllowed: boolean;
|
||||||
|
};
|
||||||
|
message?: {
|
||||||
|
type: "success" | "warning" | "error" | "info";
|
||||||
|
summary: string;
|
||||||
|
};
|
||||||
|
referrer?: {
|
||||||
|
url?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
messagesPerField: {
|
||||||
|
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
|
||||||
|
existsError: (fieldName: string) => boolean;
|
||||||
|
get: (fieldName: string) => string;
|
||||||
|
exists: (fieldName: string) => boolean;
|
||||||
|
};
|
||||||
|
account: {
|
||||||
|
email?: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Password = Common & {
|
||||||
|
pageId: "password.ftl";
|
||||||
|
password: {
|
||||||
|
passwordSet: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Account = Common & {
|
||||||
|
pageId: "account.ftl";
|
||||||
|
url: {
|
||||||
|
referrerURI: string;
|
||||||
|
accountUrl: string;
|
||||||
|
};
|
||||||
|
realm: {
|
||||||
|
registrationEmailAsUsername: boolean;
|
||||||
|
editUsernameAllowed: boolean;
|
||||||
|
};
|
||||||
|
stateChecker: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
assert<Equals<KcContext["pageId"], AccountThemePageId>>();
|
76
src/account/kcContext/getKcContext.ts
Normal file
76
src/account/kcContext/getKcContext.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||||
|
import { deepAssign } from "keycloakify/tools/deepAssign";
|
||||||
|
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||||
|
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { pathBasename } from "keycloakify/tools/pathBasename";
|
||||||
|
import { mockTestingResourcesCommonPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||||
|
|
||||||
|
export function getKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||||
|
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
|
||||||
|
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||||
|
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
|
||||||
|
const { mockPageId, mockData } = params ?? {};
|
||||||
|
|
||||||
|
const realKcContext = getKcContextFromWindow<KcContextExtension>();
|
||||||
|
|
||||||
|
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||||
|
//TODO maybe trow if no mock fo custom page
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
[
|
||||||
|
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||||
|
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||||
|
].join(" "),
|
||||||
|
"background: red; color: yellow; font-size: medium"
|
||||||
|
);
|
||||||
|
|
||||||
|
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||||
|
|
||||||
|
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
|
||||||
|
console.warn(
|
||||||
|
[
|
||||||
|
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
||||||
|
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
||||||
|
`Please check the documentation of the getKcContext function`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kcContext: any = {};
|
||||||
|
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (partialKcContextCustomMock !== undefined) {
|
||||||
|
deepAssign({
|
||||||
|
"target": kcContext,
|
||||||
|
"source": partialKcContextCustomMock
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { kcContext };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (realKcContext === undefined) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!("account" in realKcContext)) {
|
||||||
|
return { "kcContext": undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const { url } = realKcContext;
|
||||||
|
|
||||||
|
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "kcContext": realKcContext };
|
||||||
|
}
|
11
src/account/kcContext/getKcContextFromWindow.ts
Normal file
11
src/account/kcContext/getKcContextFromWindow.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
|
||||||
|
import { ftlValuesGlobalName } from "keycloakify/bin/keycloakify/ftlValuesGlobalName";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
|
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [KcContextExtension] extends [never]
|
||||||
|
? KcContext
|
||||||
|
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
|
||||||
|
|
||||||
|
export function getKcContextFromWindow<KcContextExtension extends { pageId: string } = never>(): ExtendKcContext<KcContextExtension> | undefined {
|
||||||
|
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
|
||||||
|
}
|
1
src/account/kcContext/index.ts
Normal file
1
src/account/kcContext/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type { KcContext } from "./KcContext";
|
174
src/account/kcContext/kcContextMocks.ts
Normal file
174
src/account/kcContext/kcContextMocks.ts
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import "minimal-polyfills/Object.fromEntries";
|
||||||
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath } from "keycloakify/bin/mockTestingResourcesPath";
|
||||||
|
import { pathJoin } from "keycloakify/bin/tools/pathJoin";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import type { KcContext } from "./KcContext";
|
||||||
|
|
||||||
|
const PUBLIC_URL = process.env["PUBLIC_URL"] ?? "/";
|
||||||
|
|
||||||
|
export const kcContextCommonMock: KcContext.Common = {
|
||||||
|
"url": {
|
||||||
|
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
|
||||||
|
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
|
||||||
|
"resourceUrl": "#",
|
||||||
|
"accountUrl": "#",
|
||||||
|
"applicationsUrl": "#",
|
||||||
|
"getLogoutUrl": () => "#",
|
||||||
|
"logUrl": "#",
|
||||||
|
"passwordUrl": "#",
|
||||||
|
"sessionsUrl": "#",
|
||||||
|
"socialUrl": "#",
|
||||||
|
"totpUrl": "#"
|
||||||
|
},
|
||||||
|
"realm": {
|
||||||
|
"internationalizationEnabled": true,
|
||||||
|
"userManagedAccessAllowed": true
|
||||||
|
},
|
||||||
|
"messagesPerField": {
|
||||||
|
"printIfExists": () => {
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
"existsError": () => false,
|
||||||
|
"get": key => `Fake error for ${key}`,
|
||||||
|
"exists": () => false
|
||||||
|
},
|
||||||
|
"locale": {
|
||||||
|
"supported": [
|
||||||
|
/* spell-checker: disable */
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
|
||||||
|
"label": "Deutsch",
|
||||||
|
"languageTag": "de"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
|
||||||
|
"label": "Norsk",
|
||||||
|
"languageTag": "no"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
|
||||||
|
"label": "Русский",
|
||||||
|
"languageTag": "ru"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
|
||||||
|
"label": "Svenska",
|
||||||
|
"languageTag": "sv"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
|
||||||
|
"label": "Português (Brasil)",
|
||||||
|
"languageTag": "pt-BR"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
|
||||||
|
"label": "Lietuvių",
|
||||||
|
"languageTag": "lt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
|
||||||
|
"label": "English",
|
||||||
|
"languageTag": "en"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
|
||||||
|
"label": "Italiano",
|
||||||
|
"languageTag": "it"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
|
||||||
|
"label": "Français",
|
||||||
|
"languageTag": "fr"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
|
||||||
|
"label": "中文简体",
|
||||||
|
"languageTag": "zh-CN"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
|
||||||
|
"label": "Español",
|
||||||
|
"languageTag": "es"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
|
||||||
|
"label": "Čeština",
|
||||||
|
"languageTag": "cs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
|
||||||
|
"label": "日本語",
|
||||||
|
"languageTag": "ja"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
|
||||||
|
"label": "Slovenčina",
|
||||||
|
"languageTag": "sk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
|
||||||
|
"label": "Polski",
|
||||||
|
"languageTag": "pl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
|
||||||
|
"label": "Català",
|
||||||
|
"languageTag": "ca"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
|
||||||
|
"label": "Nederlands",
|
||||||
|
"languageTag": "nl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
|
||||||
|
"label": "Türkçe",
|
||||||
|
"languageTag": "tr"
|
||||||
|
}
|
||||||
|
/* spell-checker: enable */
|
||||||
|
],
|
||||||
|
"currentLanguageTag": "en"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "success",
|
||||||
|
"summary": "This is a test message"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"authorization": true,
|
||||||
|
"identityFederation": true,
|
||||||
|
"log": true,
|
||||||
|
"passwordUpdateSupported": true
|
||||||
|
},
|
||||||
|
"referrer": undefined,
|
||||||
|
"account": {
|
||||||
|
"firstName": "john",
|
||||||
|
"lastName": "doe",
|
||||||
|
"email": "john.doe@code.gouv.fr",
|
||||||
|
"username": "doe_j"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const kcContextMocks: KcContext[] = [
|
||||||
|
id<KcContext.Password>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "password.ftl",
|
||||||
|
"password": {
|
||||||
|
"passwordSet": true
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
id<KcContext.Account>({
|
||||||
|
...kcContextCommonMock,
|
||||||
|
"pageId": "account.ftl",
|
||||||
|
"url": {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
"referrerURI": "#",
|
||||||
|
"accountUrl": "#"
|
||||||
|
},
|
||||||
|
"realm": {
|
||||||
|
...kcContextCommonMock.realm,
|
||||||
|
"registrationEmailAsUsername": true,
|
||||||
|
"editUsernameAllowed": true
|
||||||
|
},
|
||||||
|
"stateChecker": ""
|
||||||
|
})
|
||||||
|
];
|
12
src/account/lib/useGetClassName.ts
Normal file
12
src/account/lib/useGetClassName.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { createUseClassName } from "keycloakify/lib/useGetClassName";
|
||||||
|
import type { ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
|
|
||||||
|
export const { useGetClassName } = createUseClassName<ClassKey>({
|
||||||
|
"defaultClasses": {
|
||||||
|
"kcBodyClass": undefined,
|
||||||
|
"kcButtonClass": "btn",
|
||||||
|
"kcButtonPrimaryClass": "btn-primary",
|
||||||
|
"kcButtonLargeClass": "btn-lg",
|
||||||
|
"kcButtonDefaultClass": "btn-default"
|
||||||
|
}
|
||||||
|
});
|
134
src/account/pages/Account.tsx
Normal file
134
src/account/pages/Account.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
"classes": {
|
||||||
|
...classes,
|
||||||
|
"kcBodyClass": clsx(classes?.kcBodyClass, "user")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, realm, messagesPerField, stateChecker, account } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="account">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-10">
|
||||||
|
<h2>{msg("editAccountHtmlTitle")}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2 subtitle">
|
||||||
|
<span className="subtitle">
|
||||||
|
<span className="required">*</span> {msg("requiredFields")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action={url.accountUrl} className="form-horizontal" method="post">
|
||||||
|
<input type="hidden" id="stateChecker" name="stateChecker" value={stateChecker} />
|
||||||
|
|
||||||
|
{!realm.registrationEmailAsUsername && (
|
||||||
|
<div className={clsx("form-group", messagesPerField.printIfExists("username", "has-error"))}>
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="username" className="control-label">
|
||||||
|
{msg("username")}
|
||||||
|
</label>
|
||||||
|
{realm.editUsernameAllowed && <span className="required">*</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
disabled={!realm.editUsernameAllowed}
|
||||||
|
value={account.username ?? ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={clsx("form-group", messagesPerField.printIfExists("email", "has-error"))}>
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="email" className="control-label">
|
||||||
|
{msg("email")}
|
||||||
|
</label>{" "}
|
||||||
|
<span className="required">*</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="text" className="form-control" id="email" name="email" autoFocus value={account.email ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx("form-group", messagesPerField.printIfExists("firstName", "has-error"))}>
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="firstName" className="control-label">
|
||||||
|
{msg("firstName")}
|
||||||
|
</label>{" "}
|
||||||
|
<span className="required">*</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="text" className="form-control" id="firstName" name="firstName" value={account.firstName ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={clsx("form-group", messagesPerField.printIfExists("lastName", "has-error"))}>
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="lastName" className="control-label">
|
||||||
|
{msg("lastName")}
|
||||||
|
</label>{" "}
|
||||||
|
<span className="required">*</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="text" className="form-control" id="lastName" name="lastName" value={account.lastName ?? ""} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||||
|
<div>
|
||||||
|
{url.referrerURI !== undefined && <a href={url.referrerURI}>${msg("backToApplication")}</a>}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Save"
|
||||||
|
>
|
||||||
|
{msg("doSave")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonDefaultClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Cancel"
|
||||||
|
>
|
||||||
|
{msg("doCancel")}
|
||||||
|
</button>
|
||||||
|
I
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
11
src/account/pages/PageProps.ts
Normal file
11
src/account/pages/PageProps.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { LazyExoticComponent } from "react";
|
||||||
|
import type { I18n } from "keycloakify/account/i18n";
|
||||||
|
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
|
||||||
|
|
||||||
|
export type PageProps<KcContext, I18nExtended extends I18n> = {
|
||||||
|
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
|
||||||
|
kcContext: KcContext;
|
||||||
|
i18n: I18nExtended;
|
||||||
|
doUseDefaultCss: boolean;
|
||||||
|
classes?: Partial<Record<ClassKey, string>>;
|
||||||
|
};
|
105
src/account/pages/Password.tsx
Normal file
105
src/account/pages/Password.tsx
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/account/pages/PageProps";
|
||||||
|
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
|
||||||
|
import type { KcContext } from "../kcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { getClassName } = useGetClassName({
|
||||||
|
doUseDefaultCss,
|
||||||
|
"classes": {
|
||||||
|
...classes,
|
||||||
|
"kcBodyClass": clsx(classes?.kcBodyClass, "password")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, password, account } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} active="password">
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-md-10">
|
||||||
|
<h2>{msg("changePasswordHtmlTitle")}</h2>
|
||||||
|
</div>
|
||||||
|
<div className="col-md-2 subtitle">
|
||||||
|
<span className="subtitle">${msg("allFieldsRequired")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form action={url.passwordUrl} className="form-horizontal" method="post">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
value={account.username ?? ""}
|
||||||
|
autoComplete="username"
|
||||||
|
readOnly
|
||||||
|
style={{ "display": "none;" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{password.passwordSet && (
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="password" className="control-label">
|
||||||
|
{msg("password")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="password" className="form-control" id="password" name="password" autoFocus autoComplete="current-password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input type="hidden" id="stateChecker" name="stateChecker" value="${stateChecker}" />
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="password-new" className="control-label">
|
||||||
|
{msg("passwordNew")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="password" className="form-control" id="password-new" name="password-new" autoComplete="new-password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div className="col-sm-2 col-md-2">
|
||||||
|
<label htmlFor="password-confirm" className="control-label two-lines">
|
||||||
|
{msg("passwordConfirm")}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-sm-10 col-md-10">
|
||||||
|
<input type="password" className="form-control" id="password-confirm" name="password-confirm" autoComplete="new-password" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<div id="kc-form-buttons" className="col-md-offset-2 col-md-10 submit">
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
getClassName("kcButtonClass"),
|
||||||
|
getClassName("kcButtonPrimaryClass"),
|
||||||
|
getClassName("kcButtonLargeClass")
|
||||||
|
)}
|
||||||
|
name="submitAction"
|
||||||
|
value="Save"
|
||||||
|
>
|
||||||
|
{msg("doSave")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
@ -1,155 +0,0 @@
|
|||||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
|
||||||
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
|
||||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
|
||||||
import { URL } from "url";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
type ParsedPackageJson = {
|
|
||||||
name: string;
|
|
||||||
version: string;
|
|
||||||
homepage?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const reactProjectDirPath = process.cwd();
|
|
||||||
|
|
||||||
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
|
|
||||||
|
|
||||||
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
|
|
||||||
|
|
||||||
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
|
||||||
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
|
||||||
|
|
||||||
function sanitizeThemeName(name: string) {
|
|
||||||
return name
|
|
||||||
.replace(/^@(.*)/, "$1")
|
|
||||||
.split("/")
|
|
||||||
.join("-");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function main() {
|
|
||||||
console.log("🔏 Building the keycloak theme...⌚");
|
|
||||||
|
|
||||||
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
|
|
||||||
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
|
|
||||||
const themeName = sanitizeThemeName(parsedPackageJson.name);
|
|
||||||
|
|
||||||
const { doBundleEmailTemplate } = generateKeycloakThemeResources({
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
keycloakThemeEmailDirPath,
|
|
||||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
|
||||||
themeName,
|
|
||||||
...(() => {
|
|
||||||
const url = (() => {
|
|
||||||
const { homepage } = parsedPackageJson;
|
|
||||||
|
|
||||||
if (homepage !== undefined) {
|
|
||||||
return new URL(homepage);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
|
||||||
|
|
||||||
if (fs.existsSync(cnameFilePath)) {
|
|
||||||
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
})();
|
|
||||||
|
|
||||||
return {
|
|
||||||
"urlPathname": url === undefined ? "/" : url.pathname.replace(/([^/])$/, "$1/"),
|
|
||||||
"urlOrigin": !doUseExternalAssets
|
|
||||||
? undefined
|
|
||||||
: (() => {
|
|
||||||
if (url === undefined) {
|
|
||||||
console.error("ERROR: You must specify 'homepage' in your package.json");
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.origin;
|
|
||||||
})(),
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
//We have to leave it at that otherwise we break our default theme.
|
|
||||||
//Problem is that we can't guarantee that the the old resources
|
|
||||||
//will still be available on the newer keycloak version.
|
|
||||||
"keycloakVersion": "11.0.3",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { jarFilePath } = generateJavaStackFiles({
|
|
||||||
"version": parsedPackageJson.version,
|
|
||||||
themeName,
|
|
||||||
"homepage": parsedPackageJson.homepage,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
doBundleEmailTemplate,
|
|
||||||
});
|
|
||||||
|
|
||||||
child_process.execSync("mvn package", {
|
|
||||||
"cwd": keycloakThemeBuildingDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
//We want, however, to test in a container running the latest Keycloak version
|
|
||||||
const containerKeycloakVersion = "18.0.2";
|
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer({
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
themeName,
|
|
||||||
"keycloakVersion": containerKeycloakVersion,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
|
||||||
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
|
|
||||||
"",
|
|
||||||
//TODO: Restore when we find a good Helm chart for Keycloak.
|
|
||||||
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
|
|
||||||
"",
|
|
||||||
"value.yaml: ",
|
|
||||||
" extraInitContainers: |",
|
|
||||||
" - name: realm-ext-provider",
|
|
||||||
" image: curlimages/curl",
|
|
||||||
" imagePullPolicy: IfNotPresent",
|
|
||||||
" command:",
|
|
||||||
" - sh",
|
|
||||||
" args:",
|
|
||||||
" - -c",
|
|
||||||
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
|
|
||||||
" volumeMounts:",
|
|
||||||
" - name: extensions",
|
|
||||||
" mountPath: /extensions",
|
|
||||||
" ",
|
|
||||||
" extraVolumeMounts: |",
|
|
||||||
" - name: extensions",
|
|
||||||
" mountPath: /opt/keycloak/providers",
|
|
||||||
" extraEnv: |",
|
|
||||||
" - name: KEYCLOAK_USER",
|
|
||||||
" value: admin",
|
|
||||||
" - name: KEYCLOAK_PASSWORD",
|
|
||||||
" value: xxxxxxxxx",
|
|
||||||
" - name: JAVA_OPTS",
|
|
||||||
" value: -Dkeycloak.profile=preview",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
|
|
||||||
"",
|
|
||||||
`👉 $ ./${pathRelative(reactProjectDirPath, pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename))} 👈`,
|
|
||||||
"",
|
|
||||||
"Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags",
|
|
||||||
"",
|
|
||||||
"Once your container is up and running: ",
|
|
||||||
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
|
||||||
'- Create a realm named "myrealm"',
|
|
||||||
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
|
|
||||||
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
|
|
||||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
|
||||||
"",
|
|
||||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,139 +0,0 @@
|
|||||||
import cheerio from "cheerio";
|
|
||||||
import { replaceImportsFromStaticInJsCode, replaceImportsInInlineCssCode, generateCssCodeToDefineGlobals } from "../replaceImportFromStatic";
|
|
||||||
import fs from "fs";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
import { objectKeys } from "tsafe/objectKeys";
|
|
||||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
|
||||||
|
|
||||||
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
|
|
||||||
export const pageIds = [
|
|
||||||
"login.ftl",
|
|
||||||
"register.ftl",
|
|
||||||
"register-user-profile.ftl",
|
|
||||||
"info.ftl",
|
|
||||||
"error.ftl",
|
|
||||||
"login-reset-password.ftl",
|
|
||||||
"login-verify-email.ftl",
|
|
||||||
"terms.ftl",
|
|
||||||
"login-otp.ftl",
|
|
||||||
"login-update-profile.ftl",
|
|
||||||
"login-update-password.ftl",
|
|
||||||
"login-idp-link-confirm.ftl",
|
|
||||||
"login-idp-link-email.ftl",
|
|
||||||
"login-page-expired.ftl",
|
|
||||||
"login-config-totp.ftl",
|
|
||||||
"logout-confirm.ftl",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export type PageId = typeof pageIds[number];
|
|
||||||
|
|
||||||
export function generateFtlFilesCodeFactory(params: {
|
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
|
||||||
indexHtmlCode: string;
|
|
||||||
urlPathname: string;
|
|
||||||
urlOrigin: undefined | string;
|
|
||||||
}) {
|
|
||||||
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
|
|
||||||
|
|
||||||
const $ = cheerio.load(indexHtmlCode);
|
|
||||||
|
|
||||||
$("script:not([src])").each((...[, element]) => {
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
|
||||||
"jsCode": $(element).html()!,
|
|
||||||
urlOrigin,
|
|
||||||
});
|
|
||||||
|
|
||||||
$(element).text(fixedJsCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("style").each((...[, element]) => {
|
|
||||||
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
|
||||||
"cssCode": $(element).html()!,
|
|
||||||
"urlPathname": params.urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
});
|
|
||||||
|
|
||||||
$(element).text(fixedCssCode);
|
|
||||||
});
|
|
||||||
|
|
||||||
(
|
|
||||||
[
|
|
||||||
["link", "href"],
|
|
||||||
["script", "src"],
|
|
||||||
] as const
|
|
||||||
).forEach(([selector, attrName]) =>
|
|
||||||
$(selector).each((...[, element]) => {
|
|
||||||
const href = $(element).attr(attrName);
|
|
||||||
|
|
||||||
if (href === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$(element).attr(
|
|
||||||
attrName,
|
|
||||||
urlOrigin !== undefined
|
|
||||||
? href.replace(/^\//, `${urlOrigin}/`)
|
|
||||||
: href.replace(new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/"),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
|
||||||
const replaceValueBySearchValue = {
|
|
||||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
|
||||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
|
||||||
.toString("utf8")
|
|
||||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
|
||||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
|
||||||
"<#if scripts??>",
|
|
||||||
" <#list scripts as script>",
|
|
||||||
' <script src="${script}" type="text/javascript"></script>',
|
|
||||||
" </#list>",
|
|
||||||
"</#if>",
|
|
||||||
].join("\n"),
|
|
||||||
};
|
|
||||||
|
|
||||||
$("head").prepend(
|
|
||||||
[
|
|
||||||
...(Object.keys(cssGlobalsToDefine).length === 0
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
"",
|
|
||||||
"<style>",
|
|
||||||
generateCssCodeToDefineGlobals({
|
|
||||||
cssGlobalsToDefine,
|
|
||||||
urlPathname,
|
|
||||||
}).cssCodeToPrependInHead,
|
|
||||||
"</style>",
|
|
||||||
"",
|
|
||||||
]),
|
|
||||||
"<script>",
|
|
||||||
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
|
||||||
"</script>",
|
|
||||||
"",
|
|
||||||
objectKeys(replaceValueBySearchValue)[1],
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const partiallyFixedIndexHtmlCode = $.html();
|
|
||||||
|
|
||||||
function generateFtlFilesCode(params: { pageId: string }): {
|
|
||||||
ftlCode: string;
|
|
||||||
} {
|
|
||||||
const { pageId } = params;
|
|
||||||
|
|
||||||
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
|
||||||
|
|
||||||
let ftlCode = $.html();
|
|
||||||
|
|
||||||
Object.entries({
|
|
||||||
...replaceValueBySearchValue,
|
|
||||||
//If updated, don't forget to change in the ftl script as well.
|
|
||||||
"PAGE_ID_xIgLsPgGId9D8e": pageId,
|
|
||||||
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
|
||||||
|
|
||||||
return { ftlCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { generateFtlFilesCode };
|
|
||||||
}
|
|
@ -1,168 +0,0 @@
|
|||||||
import { transformCodebase } from "../tools/transformCodebase";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, basename as pathBasename } from "path";
|
|
||||||
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
|
|
||||||
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
|
|
||||||
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
|
||||||
import * as child_process from "child_process";
|
|
||||||
import { resourcesCommonPath, resourcesPath, subDirOfPublicDirBasename } from "../../lib/getKcContext/kcContextMocks/urlResourcesPath";
|
|
||||||
import { isInside } from "../tools/isInside";
|
|
||||||
|
|
||||||
export function generateKeycloakThemeResources(params: {
|
|
||||||
themeName: string;
|
|
||||||
reactAppBuildDirPath: string;
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
|
||||||
keycloakThemeEmailDirPath: string;
|
|
||||||
urlPathname: string;
|
|
||||||
//If urlOrigin is not undefined then it means --externals-assets
|
|
||||||
urlOrigin: undefined | string;
|
|
||||||
extraPagesId: string[];
|
|
||||||
extraThemeProperties: string[];
|
|
||||||
keycloakVersion: string;
|
|
||||||
}): { doBundleEmailTemplate: boolean } {
|
|
||||||
const {
|
|
||||||
themeName,
|
|
||||||
reactAppBuildDirPath,
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
keycloakThemeEmailDirPath,
|
|
||||||
urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
extraPagesId,
|
|
||||||
extraThemeProperties,
|
|
||||||
keycloakVersion,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
|
|
||||||
|
|
||||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"destDirPath": urlOrigin === undefined ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
|
||||||
"srcDirPath": reactAppBuildDirPath,
|
|
||||||
"transformSourceCode": ({ filePath, sourceCode }) => {
|
|
||||||
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
|
||||||
if (
|
|
||||||
urlOrigin === undefined &&
|
|
||||||
isInside({
|
|
||||||
"dirPath": pathJoin(reactAppBuildDirPath, subDirOfPublicDirBasename),
|
|
||||||
filePath,
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
|
|
||||||
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
|
||||||
"cssCode": sourceCode.toString("utf8"),
|
|
||||||
});
|
|
||||||
|
|
||||||
allCssGlobalsToDefine = {
|
|
||||||
...allCssGlobalsToDefine,
|
|
||||||
...cssGlobalsToDefine,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
"modifiedSourceCode": Buffer.from(fixedCssCode, "utf8"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/\.js?$/i.test(filePath)) {
|
|
||||||
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
|
||||||
"jsCode": sourceCode.toString("utf8"),
|
|
||||||
urlOrigin,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
"modifiedSourceCode": Buffer.from(fixedJsCode, "utf8"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return urlOrigin === undefined ? { "modifiedSourceCode": sourceCode } : undefined;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
let doBundleEmailTemplate: boolean;
|
|
||||||
|
|
||||||
email: {
|
|
||||||
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
|
|
||||||
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`,
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
doBundleEmailTemplate = false;
|
|
||||||
break email;
|
|
||||||
}
|
|
||||||
|
|
||||||
doBundleEmailTemplate = true;
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": keycloakThemeEmailDirPath,
|
|
||||||
"destDirPath": pathJoin(themeDirPath, "..", "email"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
|
||||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
|
||||||
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
|
||||||
urlPathname,
|
|
||||||
urlOrigin,
|
|
||||||
});
|
|
||||||
|
|
||||||
[...pageIds, ...extraPagesId].forEach(pageId => {
|
|
||||||
const { ftlCode } = generateFtlFilesCode({ pageId });
|
|
||||||
|
|
||||||
fs.mkdirSync(themeDirPath, { "recursive": true });
|
|
||||||
|
|
||||||
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
|
|
||||||
});
|
|
||||||
|
|
||||||
{
|
|
||||||
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
"destDirPath": tmpDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
|
|
||||||
"destDirPath": themeResourcesDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
|
||||||
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(resourcesCommonPath)),
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": themeResourcesDirPath,
|
|
||||||
"destDirPath": pathJoin(reactAppPublicDirPath, resourcesPath),
|
|
||||||
});
|
|
||||||
|
|
||||||
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, subDirOfPublicDirBasename);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
|
|
||||||
Buffer.from(
|
|
||||||
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" "),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
|
||||||
|
|
||||||
child_process.execSync(`rm -r ${tmpDirPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(themeDirPath, "theme.properties"),
|
|
||||||
Buffer.from("parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { doBundleEmailTemplate };
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin } from "path";
|
|
||||||
|
|
||||||
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
|
||||||
|
|
||||||
const containerName = "keycloak-testing-container";
|
|
||||||
|
|
||||||
/** Files for being able to run a hot reload keycloak container */
|
|
||||||
export function generateStartKeycloakTestingContainer(params: { keycloakVersion: string; themeName: string; keycloakThemeBuildingDirPath: string }) {
|
|
||||||
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
"#!/bin/bash",
|
|
||||||
"",
|
|
||||||
`docker rm ${containerName} || true`,
|
|
||||||
"",
|
|
||||||
`cd ${keycloakThemeBuildingDirPath}`,
|
|
||||||
"",
|
|
||||||
"docker run \\",
|
|
||||||
" -p 8080:8080 \\",
|
|
||||||
` --name ${containerName} \\`,
|
|
||||||
" -e KEYCLOAK_ADMIN=admin \\",
|
|
||||||
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
|
|
||||||
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
|
|
||||||
` -v ${pathJoin(
|
|
||||||
keycloakThemeBuildingDirPath,
|
|
||||||
"src",
|
|
||||||
"main",
|
|
||||||
"resources",
|
|
||||||
"theme",
|
|
||||||
themeName,
|
|
||||||
)}:/opt/keycloak/themes/${themeName}:rw \\`,
|
|
||||||
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
|
||||||
` start-dev`,
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
"utf8",
|
|
||||||
),
|
|
||||||
{ "mode": 0o755 },
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
export * from "./build-keycloak-theme";
|
|
||||||
import { main } from "./build-keycloak-theme";
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
main();
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
import * as crypto from "crypto";
|
|
||||||
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOrigin: undefined | string }): { fixedJsCode: string } {
|
|
||||||
/*
|
|
||||||
NOTE:
|
|
||||||
|
|
||||||
When we have urlOrigin defined it means that
|
|
||||||
we are building with --external-assets
|
|
||||||
so we have to make sur that the fixed js code will run
|
|
||||||
inside and outside keycloak.
|
|
||||||
|
|
||||||
When urlOrigin isn't defined we can assume the fixedJsCode
|
|
||||||
will always run in keycloak context.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { jsCode, urlOrigin } = params;
|
|
||||||
|
|
||||||
const fixedJsCode = jsCode
|
|
||||||
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
|
||||||
: `("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`,
|
|
||||||
)
|
|
||||||
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
|
||||||
urlOrigin === undefined
|
|
||||||
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
|
||||||
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedJsCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPathname: string; urlOrigin: undefined | string }): {
|
|
||||||
fixedCssCode: string;
|
|
||||||
} {
|
|
||||||
const { cssCode, urlPathname, urlOrigin } = params;
|
|
||||||
|
|
||||||
const fixedCssCode = cssCode.replace(
|
|
||||||
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
|
|
||||||
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedCssCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
|
||||||
fixedCssCode: string;
|
|
||||||
cssGlobalsToDefine: Record<string, string>;
|
|
||||||
} {
|
|
||||||
const { cssCode } = params;
|
|
||||||
|
|
||||||
const cssGlobalsToDefine: Record<string, string> = {};
|
|
||||||
|
|
||||||
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
|
||||||
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
|
|
||||||
);
|
|
||||||
|
|
||||||
let fixedCssCode = cssCode;
|
|
||||||
|
|
||||||
Object.keys(cssGlobalsToDefine).forEach(
|
|
||||||
cssVariableName =>
|
|
||||||
//NOTE: split/join pattern ~ replace all
|
|
||||||
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`)),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fixedCssCode, cssGlobalsToDefine };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; urlPathname: string }): {
|
|
||||||
cssCodeToPrependInHead: string;
|
|
||||||
} {
|
|
||||||
const { cssGlobalsToDefine, urlPathname } = params;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cssCodeToPrependInHead": [
|
|
||||||
":root {",
|
|
||||||
...Object.keys(cssGlobalsToDefine)
|
|
||||||
.map(cssVariableName =>
|
|
||||||
[
|
|
||||||
`--${cssVariableName}:`,
|
|
||||||
cssGlobalsToDefine[cssVariableName].replace(
|
|
||||||
new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"),
|
|
||||||
"url(${url.resourcesPath}/build/",
|
|
||||||
),
|
|
||||||
].join(" "),
|
|
||||||
)
|
|
||||||
.map(line => ` ${line};`),
|
|
||||||
"}",
|
|
||||||
].join("\n"),
|
|
||||||
};
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
|
||||||
import { keycloakThemeEmailDirPath } from "./build-keycloak-theme";
|
|
||||||
import { join as pathJoin, basename as pathBasename } from "path";
|
|
||||||
import { transformCodebase } from "./tools/transformCodebase";
|
|
||||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
if (require.main === module) {
|
|
||||||
(async () => {
|
|
||||||
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
|
||||||
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
|
|
||||||
|
|
||||||
process.exit(-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { keycloakVersion } = await promptKeycloakVersion();
|
|
||||||
|
|
||||||
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
transformCodebase({
|
|
||||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
|
||||||
"destDirPath": keycloakThemeEmailDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
|
|
||||||
|
|
||||||
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
|
||||||
})();
|
|
||||||
}
|
|
@ -1,33 +1,40 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { keycloakThemeBuildingDirPath } from "./build-keycloak-theme";
|
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"),
|
||||||
|
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
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
64
src/bin/eject-keycloak-page.ts
Normal file
64
src/bin/eject-keycloak-page.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { getProjectRoot } from "./tools/getProjectRoot";
|
||||||
|
import cliSelect from "cli-select";
|
||||||
|
import {
|
||||||
|
loginThemePageIds,
|
||||||
|
accountThemePageIds,
|
||||||
|
type LoginThemePageId,
|
||||||
|
type AccountThemePageId,
|
||||||
|
themeTypes,
|
||||||
|
type ThemeType
|
||||||
|
} from "./keycloakify/generateFtl/generateFtl";
|
||||||
|
import { capitalize } from "tsafe/capitalize";
|
||||||
|
import { readFile, writeFile } from "fs/promises";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
|
||||||
|
import { assert, Equals } from "tsafe/assert";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const projectRootDir = getProjectRoot();
|
||||||
|
|
||||||
|
console.log("Select a theme type");
|
||||||
|
|
||||||
|
const { value: themeType } = await cliSelect<ThemeType>({
|
||||||
|
"values": [...themeTypes]
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Aborting");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("Select a page you would like to eject");
|
||||||
|
|
||||||
|
const { value: pageId } = await cliSelect<LoginThemePageId | AccountThemePageId>({
|
||||||
|
"values": (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return [...loginThemePageIds];
|
||||||
|
case "account":
|
||||||
|
return [...accountThemePageIds];
|
||||||
|
}
|
||||||
|
assert<Equals<typeof themeType, never>>(false);
|
||||||
|
})()
|
||||||
|
}).catch(() => {
|
||||||
|
console.log("Aborting");
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pageBasename = capitalize(kebabCaseToCamelCase(pageId)).replace(/ftl$/, "tsx");
|
||||||
|
|
||||||
|
const targetFilePath = pathJoin(process.cwd(), "src", "keycloak-theme", themeType, "pages", pageBasename);
|
||||||
|
|
||||||
|
if (existsSync(targetFilePath)) {
|
||||||
|
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(targetFilePath, await readFile(pathJoin(projectRootDir, "src", themeType, "pages", pageBasename)));
|
||||||
|
|
||||||
|
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
|
||||||
|
})();
|
@ -1,74 +0,0 @@
|
|||||||
import "minimal-polyfills/Object.fromEntries";
|
|
||||||
import * as fs from "fs";
|
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
|
||||||
import { crawl } from "./tools/crawl";
|
|
||||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
|
||||||
import { getProjectRoot } from "./tools/getProjectRoot";
|
|
||||||
import { rm_rf, rm_r } from "./tools/rm";
|
|
||||||
|
|
||||||
//@ts-ignore
|
|
||||||
const propertiesParser = require("properties-parser");
|
|
||||||
|
|
||||||
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
|
|
||||||
console.log({ keycloakVersion });
|
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
|
|
||||||
|
|
||||||
rm_rf(tmpDirPath);
|
|
||||||
|
|
||||||
downloadBuiltinKeycloakTheme({
|
|
||||||
keycloakVersion,
|
|
||||||
"destDirPath": tmpDirPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
|
||||||
|
|
||||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
|
||||||
|
|
||||||
{
|
|
||||||
const baseThemeDirPath = pathJoin(tmpDirPath, "base");
|
|
||||||
|
|
||||||
crawl(baseThemeDirPath).forEach(filePath => {
|
|
||||||
const match = filePath.match(/^([^/]+)\/messages\/messages_([^.]+)\.properties$/);
|
|
||||||
|
|
||||||
if (match === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, typeOfPage, language] = match;
|
|
||||||
|
|
||||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
|
||||||
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
|
|
||||||
([key, value]: any) => [key, value.replace(/''/g, "'")],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
rm_r(tmpDirPath);
|
|
||||||
|
|
||||||
const targetDirPath = pathJoin(getProjectRoot(), "src", "lib", "i18n", "generated_kcMessages", keycloakVersion);
|
|
||||||
|
|
||||||
fs.mkdirSync(targetDirPath, { "recursive": true });
|
|
||||||
|
|
||||||
Object.keys(record).forEach(pageType => {
|
|
||||||
const filePath = pathJoin(targetDirPath, `${pageType}.ts`);
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
filePath,
|
|
||||||
Buffer.from(
|
|
||||||
[
|
|
||||||
`//This code was automatically generated by running ${pathRelative(getProjectRoot(), __filename)}`,
|
|
||||||
"//PLEASE DO NOT EDIT MANUALLY",
|
|
||||||
"",
|
|
||||||
"/* spell-checker: disable */",
|
|
||||||
`export const kcMessages= ${JSON.stringify(record[pageType], null, 2)};`,
|
|
||||||
"/* spell-checker: enable */",
|
|
||||||
].join("\n"),
|
|
||||||
"utf8",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`${filePath} wrote`);
|
|
||||||
});
|
|
||||||
}
|
|
48
src/bin/initialize-email-theme.ts
Normal file
48
src/bin/initialize-email-theme.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||||
|
import { keycloakThemeEmailDirPath } from "./keycloakify";
|
||||||
|
import { join as pathJoin, relative as pathRelative } from "path";
|
||||||
|
import { transformCodebase } from "./tools/transformCodebase";
|
||||||
|
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { getCliOptions } from "./tools/cliOptions";
|
||||||
|
import { getLogger } from "./tools/logger";
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
|
||||||
|
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||||
|
logger.warn(`There is already a ${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
|
||||||
|
|
||||||
|
process.exit(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { keycloakVersion } = await promptKeycloakVersion();
|
||||||
|
|
||||||
|
const builtinKeycloakThemeTmpDirPath = pathJoin(keycloakThemeEmailDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||||
|
isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||||
|
"destDirPath": keycloakThemeEmailDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const themePropertyFilePath = pathJoin(keycloakThemeEmailDirPath, "theme.properties");
|
||||||
|
|
||||||
|
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
`${pathRelative(process.cwd(), keycloakThemeEmailDirPath)} ready to be customized, feel free to remove every file you do not customize`
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||||
|
})();
|
214
src/bin/keycloakify/BuildOptions.ts
Normal file
214
src/bin/keycloakify/BuildOptions.ts
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
import { id } from "tsafe/id";
|
||||||
|
import { parse as urlParse } from "url";
|
||||||
|
import { typeGuard } from "tsafe/typeGuard";
|
||||||
|
import { symToStr } from "tsafe/symToStr";
|
||||||
|
|
||||||
|
const bundlers = ["mvn", "keycloakify", "none"] as const;
|
||||||
|
type Bundler = (typeof bundlers)[number];
|
||||||
|
type ParsedPackageJson = {
|
||||||
|
name: string;
|
||||||
|
version: string;
|
||||||
|
homepage?: string;
|
||||||
|
keycloakify?: {
|
||||||
|
/** @deprecated: use extraLoginPages instead */
|
||||||
|
extraPages?: string[];
|
||||||
|
extraLoginPages?: string[];
|
||||||
|
extraAccountPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
areAppAndKeycloakServerSharingSameDomain?: boolean;
|
||||||
|
artifactId?: string;
|
||||||
|
groupId?: string;
|
||||||
|
bundler?: Bundler;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const zParsedPackageJson = z.object({
|
||||||
|
"name": z.string(),
|
||||||
|
"version": z.string(),
|
||||||
|
"homepage": z.string().optional(),
|
||||||
|
"keycloakify": z
|
||||||
|
.object({
|
||||||
|
"extraPages": z.array(z.string()).optional(),
|
||||||
|
"extraLoginPages": z.array(z.string()).optional(),
|
||||||
|
"extraAccountPages": z.array(z.string()).optional(),
|
||||||
|
"extraThemeProperties": z.array(z.string()).optional(),
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional(),
|
||||||
|
"artifactId": z.string().optional(),
|
||||||
|
"groupId": z.string().optional(),
|
||||||
|
"bundler": z.enum(bundlers).optional()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
||||||
|
|
||||||
|
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||||
|
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptions {
|
||||||
|
export type Common = {
|
||||||
|
isSilent: boolean;
|
||||||
|
version: string;
|
||||||
|
themeName: string;
|
||||||
|
extraLoginPages: string[] | undefined;
|
||||||
|
extraAccountPages: string[] | undefined;
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
bundler: Bundler;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readBuildOptions(params: {
|
||||||
|
packageJson: string;
|
||||||
|
CNAME: string | undefined;
|
||||||
|
isExternalAssetsCliParamProvided: boolean;
|
||||||
|
isSilent: boolean;
|
||||||
|
}): BuildOptions {
|
||||||
|
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
|
||||||
|
|
||||||
|
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
||||||
|
|
||||||
|
const url = (() => {
|
||||||
|
const { homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
let url: URL | undefined = undefined;
|
||||||
|
|
||||||
|
if (homepage !== undefined) {
|
||||||
|
url = new URL(homepage);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CNAME !== undefined) {
|
||||||
|
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"origin": url.origin,
|
||||||
|
"pathname": (() => {
|
||||||
|
const out = url.pathname.replace(/([^/])$/, "$1/");
|
||||||
|
|
||||||
|
return out === "/" ? undefined : out;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const common: BuildOptions.Common = (() => {
|
||||||
|
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||||
|
|
||||||
|
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {};
|
||||||
|
|
||||||
|
const themeName = name
|
||||||
|
.replace(/^@(.*)/, "$1")
|
||||||
|
.split("/")
|
||||||
|
.join("-");
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeName,
|
||||||
|
"bundler": (() => {
|
||||||
|
const { KEYCLOAKIFY_BUNDLER } = process.env;
|
||||||
|
|
||||||
|
assert(
|
||||||
|
typeGuard<Bundler | undefined>(
|
||||||
|
KEYCLOAKIFY_BUNDLER,
|
||||||
|
[undefined, ...id<readonly string[]>(bundlers)].includes(KEYCLOAKIFY_BUNDLER)
|
||||||
|
),
|
||||||
|
`${symToStr({ KEYCLOAKIFY_BUNDLER })} should be one of ${bundlers.join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return KEYCLOAKIFY_BUNDLER ?? bundler ?? "keycloakify";
|
||||||
|
})(),
|
||||||
|
"artifactId": process.env.KEYCLOAKIFY_ARTIFACT_ID ?? artifactId ?? `${themeName}-keycloak-theme`,
|
||||||
|
"groupId": (() => {
|
||||||
|
const fallbackGroupId = `${themeName}.keycloak`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
process.env.KEYCLOAKIFY_GROUP_ID ??
|
||||||
|
groupId ??
|
||||||
|
(!homepage
|
||||||
|
? fallbackGroupId
|
||||||
|
: urlParse(homepage)
|
||||||
|
.host?.replace(/:[0-9]+$/, "")
|
||||||
|
?.split(".")
|
||||||
|
.reverse()
|
||||||
|
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||||
|
);
|
||||||
|
})(),
|
||||||
|
"version": process.env.KEYCLOAKIFY_VERSION ?? version,
|
||||||
|
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
|
||||||
|
extraAccountPages,
|
||||||
|
extraThemeProperties,
|
||||||
|
isSilent
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (isExternalAssetsCliParamProvided) {
|
||||||
|
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": false
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return id<BuildOptions.ExternalAssets.SameDomain>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
url !== undefined,
|
||||||
|
[
|
||||||
|
"Can't compile in external assets mode if we don't know where",
|
||||||
|
"the app will be hosted.",
|
||||||
|
"You should provide a homepage field in the package.json (or create a",
|
||||||
|
"public/CNAME file.",
|
||||||
|
"Alternatively, if your app and the Keycloak server are on the same domain, ",
|
||||||
|
"eg https://example.com is your app and https://example.com/auth is the keycloak",
|
||||||
|
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
|
||||||
|
"in your package.json"
|
||||||
|
].join(" ")
|
||||||
|
);
|
||||||
|
|
||||||
|
return id<BuildOptions.ExternalAssets.DifferentDomains>({
|
||||||
|
...commonExternalAssets,
|
||||||
|
"areAppAndKeycloakServerSharingSameDomain": false,
|
||||||
|
"urlOrigin": url.origin,
|
||||||
|
"urlPathname": url.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return id<BuildOptions.Standalone>({
|
||||||
|
...common,
|
||||||
|
"isStandalone": true,
|
||||||
|
"urlPathname": url?.pathname
|
||||||
|
});
|
||||||
|
}
|
@ -2,8 +2,7 @@
|
|||||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||||
(()=>{
|
(()=>{
|
||||||
|
|
||||||
const out =
|
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||||
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
|
||||||
|
|
||||||
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||||
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||||
@ -32,63 +31,94 @@ ${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;
|
||||||
</#if>
|
<#else>
|
||||||
<#list fieldNames as fieldName>
|
<#list fieldNames as fieldName>
|
||||||
if(fieldName === "${fieldName}" ){
|
if(fieldName === "${fieldName}" ){
|
||||||
<#attempt>
|
<#attempt>
|
||||||
return "${messagesPerField.printIfExists(fieldName,'1')}" ? x : undefined;
|
<#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>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
}
|
}
|
||||||
</#list>
|
</#list>
|
||||||
throw new Error("There is no " + fieldName + " field");
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
},
|
},
|
||||||
"existsError": function (fieldName) {
|
"existsError": function (fieldName) {
|
||||||
<#if !messagesPerField?? >
|
<#if !messagesPerField?? >
|
||||||
return false;
|
return false;
|
||||||
</#if>
|
<#else>
|
||||||
<#list fieldNames as fieldName>
|
<#list fieldNames as fieldName>
|
||||||
if(fieldName === "${fieldName}" ){
|
if(fieldName === "${fieldName}" ){
|
||||||
<#attempt>
|
<#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>;
|
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
|
||||||
|
</#if>
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
}
|
}
|
||||||
</#list>
|
</#list>
|
||||||
throw new Error("There is no " + fieldName + " field");
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
},
|
},
|
||||||
"get": function (fieldName) {
|
"get": function (fieldName) {
|
||||||
<#if !messagesPerField?? >
|
<#if !messagesPerField?? >
|
||||||
return '';
|
return '';
|
||||||
</#if>
|
<#else>
|
||||||
<#list fieldNames as fieldName>
|
<#list fieldNames as fieldName>
|
||||||
if(fieldName === "${fieldName}" ){
|
if(fieldName === "${fieldName}" ){
|
||||||
<#attempt>
|
<#attempt>
|
||||||
|
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
|
||||||
|
<#if messagesPerField.existsError('username', 'password')>
|
||||||
|
return 'Invalid username or password.';
|
||||||
|
</#if>
|
||||||
|
<#else>
|
||||||
<#if messagesPerField.existsError('${fieldName}')>
|
<#if messagesPerField.existsError('${fieldName}')>
|
||||||
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
return "${messagesPerField.get('${fieldName}')?no_esc}";
|
||||||
</#if>
|
</#if>
|
||||||
|
</#if>
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
}
|
}
|
||||||
</#list>
|
</#list>
|
||||||
throw new Error("There is no " + fieldName + " field");
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
},
|
},
|
||||||
"exists": function (fieldName) {
|
"exists": function (fieldName) {
|
||||||
<#if !messagesPerField?? >
|
<#if !messagesPerField?? >
|
||||||
return false;
|
return false;
|
||||||
</#if>
|
<#else>
|
||||||
<#list fieldNames as fieldName>
|
<#list fieldNames as fieldName>
|
||||||
if(fieldName === "${fieldName}" ){
|
if(fieldName === "${fieldName}" ){
|
||||||
<#attempt>
|
<#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>;
|
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
|
||||||
|
</#if>
|
||||||
<#recover>
|
<#recover>
|
||||||
</#attempt>
|
</#attempt>
|
||||||
}
|
}
|
||||||
</#list>
|
</#list>
|
||||||
throw new Error("There is no " + fieldName + " field");
|
throw new Error("There is no " + fieldName + " field");
|
||||||
|
</#if>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<#if account??>
|
||||||
|
out["url"]["getLogoutUrl"] = function () {
|
||||||
|
<#attempt>
|
||||||
|
return "${url.getLogoutUrl()}";
|
||||||
|
<#recover>
|
||||||
|
</#attempt>
|
||||||
|
};
|
||||||
|
</#if>
|
||||||
|
|
||||||
out["pageId"] = "${pageId}";
|
out["pageId"] = "${pageId}";
|
||||||
|
|
||||||
return out;
|
return out;
|
||||||
@ -152,6 +182,10 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
|||||||
) || (
|
) || (
|
||||||
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
|
||||||
are_same_path(path, ["realm"])
|
are_same_path(path, ["realm"])
|
||||||
|
) || (
|
||||||
|
"error.ftl" == pageId &&
|
||||||
|
are_same_path(path, ["realm"]) &&
|
||||||
|
!["name", "displayName", "displayNameHtml", "internationalizationEnabled", "registrationEmailAsUsername" ]?seq_contains(key)
|
||||||
)
|
)
|
||||||
>
|
>
|
||||||
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]>
|
||||||
@ -272,6 +306,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>
|
196
src/bin/keycloakify/generateFtl/generateFtl.ts
Normal file
196
src/bin/keycloakify/generateFtl/generateFtl.ts
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
import cheerio from "cheerio";
|
||||||
|
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
|
||||||
|
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { objectKeys } from "tsafe/objectKeys";
|
||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export const themeTypes = ["login", "account"] as const;
|
||||||
|
|
||||||
|
export type ThemeType = (typeof themeTypes)[number];
|
||||||
|
|
||||||
|
export const loginThemePageIds = [
|
||||||
|
"login.ftl",
|
||||||
|
"login-username.ftl",
|
||||||
|
"login-password.ftl",
|
||||||
|
"webauthn-authenticate.ftl",
|
||||||
|
"register.ftl",
|
||||||
|
"register-user-profile.ftl",
|
||||||
|
"info.ftl",
|
||||||
|
"error.ftl",
|
||||||
|
"login-reset-password.ftl",
|
||||||
|
"login-verify-email.ftl",
|
||||||
|
"terms.ftl",
|
||||||
|
"login-otp.ftl",
|
||||||
|
"login-update-profile.ftl",
|
||||||
|
"login-update-password.ftl",
|
||||||
|
"login-idp-link-confirm.ftl",
|
||||||
|
"login-idp-link-email.ftl",
|
||||||
|
"login-page-expired.ftl",
|
||||||
|
"login-config-totp.ftl",
|
||||||
|
"logout-confirm.ftl",
|
||||||
|
"update-user-profile.ftl",
|
||||||
|
"idp-review-user-profile.ftl"
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
||||||
|
|
||||||
|
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||||
|
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateFtlFilesCodeFactory(params: {
|
||||||
|
indexHtmlCode: string;
|
||||||
|
//NOTE: Expected to be an empty object if external assets mode is enabled.
|
||||||
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}) {
|
||||||
|
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(indexHtmlCode);
|
||||||
|
|
||||||
|
fix_imports_statements: {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
break fix_imports_statements;
|
||||||
|
}
|
||||||
|
|
||||||
|
$("script:not([src])").each((...[, element]) => {
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": $(element).html()!,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedJsCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("style").each((...[, element]) => {
|
||||||
|
const { fixedCssCode } = replaceImportsInInlineCssCode({
|
||||||
|
"cssCode": $(element).html()!,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
$(element).text(fixedCssCode);
|
||||||
|
});
|
||||||
|
|
||||||
|
(
|
||||||
|
[
|
||||||
|
["link", "href"],
|
||||||
|
["script", "src"]
|
||||||
|
] as const
|
||||||
|
).forEach(([selector, attrName]) =>
|
||||||
|
$(selector).each((...[, element]) => {
|
||||||
|
const href = $(element).attr(attrName);
|
||||||
|
|
||||||
|
if (href === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(element).attr(
|
||||||
|
attrName,
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
|
||||||
|
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Object.keys(cssGlobalsToDefine).length !== 0) {
|
||||||
|
$("head").prepend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"<style>",
|
||||||
|
generateCssCodeToDefineGlobals({
|
||||||
|
cssGlobalsToDefine,
|
||||||
|
buildOptions
|
||||||
|
}).cssCodeToPrependInHead,
|
||||||
|
"</style>",
|
||||||
|
""
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
|
||||||
|
const replaceValueBySearchValue = {
|
||||||
|
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||||
|
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||||
|
.toString("utf8")
|
||||||
|
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||||
|
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||||
|
"<#if scripts??>",
|
||||||
|
" <#list scripts as script>",
|
||||||
|
' <script src="${script}" type="text/javascript"></script>',
|
||||||
|
" </#list>",
|
||||||
|
"</#if>"
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
|
||||||
|
$("head").prepend(
|
||||||
|
[
|
||||||
|
"<script>",
|
||||||
|
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
|
||||||
|
"</script>",
|
||||||
|
"",
|
||||||
|
objectKeys(replaceValueBySearchValue)[1]
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
const partiallyFixedIndexHtmlCode = $.html();
|
||||||
|
|
||||||
|
function generateFtlFilesCode(params: { pageId: string }): {
|
||||||
|
ftlCode: string;
|
||||||
|
} {
|
||||||
|
const { pageId } = params;
|
||||||
|
|
||||||
|
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
|
||||||
|
|
||||||
|
let ftlCode = $.html();
|
||||||
|
|
||||||
|
Object.entries({
|
||||||
|
...replaceValueBySearchValue,
|
||||||
|
//If updated, don't forget to change in the ftl script as well.
|
||||||
|
"PAGE_ID_xIgLsPgGId9D8e": pageId
|
||||||
|
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
|
||||||
|
|
||||||
|
return { ftlCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { generateFtlFilesCode };
|
||||||
|
}
|
@ -1,39 +1,40 @@
|
|||||||
import * as url from "url";
|
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
|
import { themeTypes } from "./generateFtl/generateFtl";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
groupId: string;
|
||||||
|
artifactId?: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
export function generateJavaStackFiles(params: {
|
export function generateJavaStackFiles(params: {
|
||||||
version: string;
|
|
||||||
themeName: string;
|
|
||||||
homepage?: string;
|
|
||||||
keycloakThemeBuildingDirPath: string;
|
keycloakThemeBuildingDirPath: string;
|
||||||
doBundleEmailTemplate: boolean;
|
doBundlesEmailTemplate: boolean;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
}): {
|
}): {
|
||||||
jarFilePath: string;
|
jarFilePath: string;
|
||||||
} {
|
} {
|
||||||
const { themeName, version, homepage, keycloakThemeBuildingDirPath, doBundleEmailTemplate } = params;
|
const {
|
||||||
|
buildOptions: { groupId, themeName, version, artifactId },
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
doBundlesEmailTemplate
|
||||||
|
} = params;
|
||||||
|
|
||||||
{
|
{
|
||||||
const { pomFileCode } = (function generatePomFileCode(): {
|
const { pomFileCode } = (function generatePomFileCode(): {
|
||||||
pomFileCode: string;
|
pomFileCode: string;
|
||||||
} {
|
} {
|
||||||
const groupId = (() => {
|
|
||||||
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
(!homepage
|
|
||||||
? fallbackGroupId
|
|
||||||
: url
|
|
||||||
.parse(homepage)
|
|
||||||
.host?.replace(/:[0-9]+$/, "")
|
|
||||||
?.split(".")
|
|
||||||
.reverse()
|
|
||||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
|
|
||||||
const artefactId = `${themeName}-keycloak-theme`;
|
|
||||||
|
|
||||||
const pomFileCode = [
|
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"`,
|
||||||
@ -41,11 +42,11 @@ export function generateJavaStackFiles(params: {
|
|||||||
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
` xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">`,
|
||||||
` <modelVersion>4.0.0</modelVersion>`,
|
` <modelVersion>4.0.0</modelVersion>`,
|
||||||
` <groupId>${groupId}</groupId>`,
|
` <groupId>${groupId}</groupId>`,
|
||||||
` <artifactId>${artefactId}</artifactId>`,
|
` <artifactId>${artifactId}</artifactId>`,
|
||||||
` <version>${version}</version>`,
|
` <version>${version}</version>`,
|
||||||
` <name>${artefactId}</name>`,
|
` <name>${artifactId}</name>`,
|
||||||
` <description />`,
|
` <description />`,
|
||||||
`</project>`,
|
`</project>`
|
||||||
].join("\n");
|
].join("\n");
|
||||||
|
|
||||||
return { pomFileCode };
|
return { pomFileCode };
|
||||||
@ -69,19 +70,19 @@ export function generateJavaStackFiles(params: {
|
|||||||
"themes": [
|
"themes": [
|
||||||
{
|
{
|
||||||
"name": themeName,
|
"name": themeName,
|
||||||
"types": ["login", ...(doBundleEmailTemplate ? ["email"] : [])],
|
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
|
||||||
},
|
}
|
||||||
],
|
]
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2
|
||||||
),
|
|
||||||
"utf8",
|
|
||||||
),
|
),
|
||||||
|
"utf8"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`),
|
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${version}.jar`)
|
||||||
};
|
};
|
||||||
}
|
}
|
251
src/bin/keycloakify/generateKeycloakThemeResources.ts
Normal file
251
src/bin/keycloakify/generateKeycloakThemeResources.ts
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
import { transformCodebase } from "../tools/transformCodebase";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin, basename as pathBasename } from "path";
|
||||||
|
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
|
||||||
|
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
|
||||||
|
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "./generateFtl";
|
||||||
|
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
|
||||||
|
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
|
||||||
|
import { isInside } from "../tools/isInside";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import { getLogger } from "../tools/logger";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
themeName: string;
|
||||||
|
extraLoginPages?: string[];
|
||||||
|
extraAccountPages?: string[];
|
||||||
|
extraThemeProperties?: string[];
|
||||||
|
isSilent: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||||
|
|
||||||
|
export namespace ExternalAssets {
|
||||||
|
export type CommonExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SameDomain = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DifferentDomains = CommonExternalAssets & {
|
||||||
|
areAppAndKeycloakServerSharingSameDomain: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateKeycloakThemeResources(params: {
|
||||||
|
reactAppBuildDirPath: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
keycloakThemeEmailDirPath: string;
|
||||||
|
keycloakVersion: string;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}): Promise<{ doBundlesEmailTemplate: boolean }> {
|
||||||
|
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
|
||||||
|
|
||||||
|
const logger = getLogger({ isSilent: buildOptions.isSilent });
|
||||||
|
|
||||||
|
const getThemeDirPath = (themeType: ThemeType | "email") =>
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
|
||||||
|
|
||||||
|
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
|
let generateFtlFilesCode_glob: ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"] | undefined = undefined;
|
||||||
|
|
||||||
|
for (const themeType of themeTypes) {
|
||||||
|
const themeDirPath = getThemeDirPath(themeType);
|
||||||
|
|
||||||
|
copy_app_resources_to_theme_path: {
|
||||||
|
const isFirstPass = themeType.indexOf(themeType) === 0;
|
||||||
|
|
||||||
|
if (!isFirstPass && !buildOptions.isStandalone) {
|
||||||
|
break copy_app_resources_to_theme_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
|
||||||
|
"srcDirPath": reactAppBuildDirPath,
|
||||||
|
"transformSourceCode": ({ filePath, sourceCode }) => {
|
||||||
|
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
|
||||||
|
if (
|
||||||
|
buildOptions.isStandalone &&
|
||||||
|
isInside({
|
||||||
|
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
|
||||||
|
filePath
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.css?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
|
||||||
|
"cssCode": sourceCode.toString("utf8")
|
||||||
|
});
|
||||||
|
|
||||||
|
register_css_variables: {
|
||||||
|
if (!isFirstPass) {
|
||||||
|
break register_css_variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
allCssGlobalsToDefine = {
|
||||||
|
...allCssGlobalsToDefine,
|
||||||
|
...cssGlobalsToDefine
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/\.js?$/i.test(filePath)) {
|
||||||
|
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
|
||||||
|
"jsCode": sourceCode.toString("utf8"),
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
|
||||||
|
}
|
||||||
|
|
||||||
|
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateFtlFilesCode = (() => {
|
||||||
|
if (generateFtlFilesCode_glob !== undefined) {
|
||||||
|
return generateFtlFilesCode_glob;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||||
|
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||||
|
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||||
|
"buildOptions": buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
return generateFtlFilesCode;
|
||||||
|
})();
|
||||||
|
|
||||||
|
[
|
||||||
|
...(() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return loginThemePageIds;
|
||||||
|
case "account":
|
||||||
|
return accountThemePageIds;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
...((() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return buildOptions.extraLoginPages;
|
||||||
|
case "account":
|
||||||
|
return buildOptions.extraAccountPages;
|
||||||
|
}
|
||||||
|
})() ?? [])
|
||||||
|
].forEach(pageId => {
|
||||||
|
const { ftlCode } = generateFtlFilesCode({ pageId });
|
||||||
|
|
||||||
|
fs.mkdirSync(themeDirPath, { "recursive": true });
|
||||||
|
|
||||||
|
fs.writeFileSync(pathJoin(themeDirPath, pageId), Buffer.from(ftlCode, "utf8"));
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
const tmpDirPath = pathJoin(themeDirPath, "..", "tmp_xxKdLpdIdLd");
|
||||||
|
|
||||||
|
await downloadBuiltinKeycloakTheme({
|
||||||
|
keycloakVersion,
|
||||||
|
"destDirPath": tmpDirPath,
|
||||||
|
isSilent: buildOptions.isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
|
||||||
|
"destDirPath": themeResourcesDirPath
|
||||||
|
});
|
||||||
|
|
||||||
|
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||||
|
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
|
||||||
|
});
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": themeResourcesDirPath,
|
||||||
|
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
|
||||||
|
});
|
||||||
|
|
||||||
|
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
|
||||||
|
Buffer.from(
|
||||||
|
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(
|
||||||
|
" "
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
|
||||||
|
fs.rmSync(tmpDirPath, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(themeDirPath, "theme.properties"),
|
||||||
|
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doBundlesEmailTemplate: boolean;
|
||||||
|
|
||||||
|
email: {
|
||||||
|
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||||
|
logger.log(
|
||||||
|
[
|
||||||
|
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
|
||||||
|
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
doBundlesEmailTemplate = false;
|
||||||
|
break email;
|
||||||
|
}
|
||||||
|
|
||||||
|
doBundlesEmailTemplate = true;
|
||||||
|
|
||||||
|
transformCodebase({
|
||||||
|
"srcDirPath": keycloakThemeEmailDirPath,
|
||||||
|
"destDirPath": getThemeDirPath("email")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return { doBundlesEmailTemplate };
|
||||||
|
}
|
61
src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
Normal file
61
src/bin/keycloakify/generateStartKeycloakTestingContainer.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import * as fs from "fs";
|
||||||
|
import { join as pathJoin } from "path";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
import type { BuildOptions } from "./BuildOptions";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
themeName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
|
const containerName = "keycloak-testing-container";
|
||||||
|
|
||||||
|
/** Files for being able to run a hot reload keycloak container */
|
||||||
|
export function generateStartKeycloakTestingContainer(params: {
|
||||||
|
keycloakVersion: string;
|
||||||
|
keycloakThemeBuildingDirPath: string;
|
||||||
|
buildOptions: BuildOptionsLike;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
keycloakVersion,
|
||||||
|
buildOptions: { themeName }
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const keycloakThemePath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName).replace(/\\/g, "/");
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
|
||||||
|
|
||||||
|
Buffer.from(
|
||||||
|
[
|
||||||
|
"#!/usr/bin/env bash",
|
||||||
|
"",
|
||||||
|
`docker rm ${containerName} || true`,
|
||||||
|
"",
|
||||||
|
`cd "${keycloakThemeBuildingDirPath.replace(/\\/g, "/")}"`,
|
||||||
|
"",
|
||||||
|
"docker run \\",
|
||||||
|
" -p 8080:8080 \\",
|
||||||
|
` --name ${containerName} \\`,
|
||||||
|
" -e KEYCLOAK_ADMIN=admin \\",
|
||||||
|
" -e KEYCLOAK_ADMIN_PASSWORD=admin \\",
|
||||||
|
" -e JAVA_OPTS=-Dkeycloak.profile=preview \\",
|
||||||
|
` -v "${keycloakThemePath}":"/opt/keycloak/themes/${themeName}":rw \\`,
|
||||||
|
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
|
||||||
|
` start-dev`,
|
||||||
|
""
|
||||||
|
].join("\n"),
|
||||||
|
"utf8"
|
||||||
|
),
|
||||||
|
{ "mode": 0o755 }
|
||||||
|
);
|
||||||
|
}
|
8
src/bin/keycloakify/index.ts
Normal file
8
src/bin/keycloakify/index.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
export * from "./keycloakify";
|
||||||
|
import { main } from "./keycloakify";
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
|
main().catch(e => console.error(e));
|
||||||
|
}
|
151
src/bin/keycloakify/keycloakify.ts
Normal file
151
src/bin/keycloakify/keycloakify.ts
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||||
|
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||||
|
import { join as pathJoin, relative as pathRelative, basename as pathBasename, sep as pathSep } from "path";
|
||||||
|
import * as child_process from "child_process";
|
||||||
|
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import { readBuildOptions } from "./BuildOptions";
|
||||||
|
import { getLogger } from "../tools/logger";
|
||||||
|
import { getCliOptions } from "../tools/cliOptions";
|
||||||
|
import jar from "../tools/jar";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import type { Equals } from "tsafe";
|
||||||
|
|
||||||
|
const reactProjectDirPath = process.cwd();
|
||||||
|
|
||||||
|
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
|
||||||
|
export const keycloakThemeEmailDirPath = pathJoin(reactProjectDirPath, "src", "keycloak-theme", "email");
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
||||||
|
const logger = getLogger({ isSilent });
|
||||||
|
logger.log("🔏 Building the keycloak theme...⌚");
|
||||||
|
|
||||||
|
const buildOptions = readBuildOptions({
|
||||||
|
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
|
||||||
|
"CNAME": (() => {
|
||||||
|
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
|
||||||
|
|
||||||
|
if (!fs.existsSync(cnameFilePath)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||||
|
})(),
|
||||||
|
"isExternalAssetsCliParamProvided": hasExternalAssets,
|
||||||
|
"isSilent": isSilent
|
||||||
|
});
|
||||||
|
|
||||||
|
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
keycloakThemeEmailDirPath,
|
||||||
|
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||||
|
buildOptions,
|
||||||
|
//We have to leave it at that otherwise we break our default theme.
|
||||||
|
//Problem is that we can`t guarantee that the the old resources
|
||||||
|
//will still be available on the newer keycloak version.
|
||||||
|
"keycloakVersion": "11.0.3"
|
||||||
|
});
|
||||||
|
|
||||||
|
const { jarFilePath } = generateJavaStackFiles({
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
doBundlesEmailTemplate,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (buildOptions.bundler) {
|
||||||
|
case "none":
|
||||||
|
logger.log("😱 Skipping bundling step, there will be no jar");
|
||||||
|
break;
|
||||||
|
case "keycloakify":
|
||||||
|
logger.log("🫶 Let keycloakify do its thang");
|
||||||
|
await jar({
|
||||||
|
"rootPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources"),
|
||||||
|
"version": buildOptions.version,
|
||||||
|
"groupId": buildOptions.groupId,
|
||||||
|
"artifactId": buildOptions.artifactId,
|
||||||
|
"targetPath": jarFilePath
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "mvn":
|
||||||
|
logger.log("🫙 Run maven to deliver a jar");
|
||||||
|
child_process.execSync("mvn package", { "cwd": keycloakThemeBuildingDirPath });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
assert<Equals<typeof buildOptions.bundler, never>>(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want, however, to test in a container running the latest Keycloak version
|
||||||
|
const containerKeycloakVersion = "20.0.1";
|
||||||
|
|
||||||
|
generateStartKeycloakTestingContainer({
|
||||||
|
keycloakThemeBuildingDirPath,
|
||||||
|
"keycloakVersion": containerKeycloakVersion,
|
||||||
|
buildOptions
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.log(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||||
|
`It is to be placed in "/opt/keycloak/providers" in the container running a quay.io/keycloak/keycloak Docker image.`,
|
||||||
|
"",
|
||||||
|
//TODO: Restore when we find a good Helm chart for Keycloak.
|
||||||
|
//"Using Helm (https://github.com/codecentric/helm-charts), edit to reflect:",
|
||||||
|
"",
|
||||||
|
"value.yaml: ",
|
||||||
|
" extraInitContainers: |",
|
||||||
|
" - name: realm-ext-provider",
|
||||||
|
" image: curlimages/curl",
|
||||||
|
" imagePullPolicy: IfNotPresent",
|
||||||
|
" command:",
|
||||||
|
" - sh",
|
||||||
|
" args:",
|
||||||
|
" - -c",
|
||||||
|
` - curl -L -f -S -o /extensions/${pathBasename(jarFilePath)} https://AN.URL.FOR/${pathBasename(jarFilePath)}`,
|
||||||
|
" volumeMounts:",
|
||||||
|
" - name: extensions",
|
||||||
|
" mountPath: /extensions",
|
||||||
|
" ",
|
||||||
|
" extraVolumeMounts: |",
|
||||||
|
" - name: extensions",
|
||||||
|
" mountPath: /opt/keycloak/providers",
|
||||||
|
" extraEnv: |",
|
||||||
|
" - name: KEYCLOAK_USER",
|
||||||
|
" value: admin",
|
||||||
|
" - name: KEYCLOAK_PASSWORD",
|
||||||
|
" value: xxxxxxxxx",
|
||||||
|
" - name: JAVA_OPTS",
|
||||||
|
" value: -Dkeycloak.profile=preview",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
`To test your theme locally you can spin up a Keycloak ${containerKeycloakVersion} container image with the theme pre loaded by running:`,
|
||||||
|
"",
|
||||||
|
`👉 $ .${pathSep}${pathRelative(
|
||||||
|
reactProjectDirPath,
|
||||||
|
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename)
|
||||||
|
)} 👈`,
|
||||||
|
"",
|
||||||
|
`Test with different Keycloak versions by editing the .sh file. see available versions here: https://quay.io/repository/keycloak/keycloak?tab=tags`,
|
||||||
|
``,
|
||||||
|
`Once your container is up and running: `,
|
||||||
|
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
|
||||||
|
`- Create a realm: myrealm`,
|
||||||
|
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
|
||||||
|
`- Enable the Account theme: Realm settings -> Themes tab -> Account theme, select ${buildOptions.themeName} `,
|
||||||
|
`- Create a client id myclient`,
|
||||||
|
` Root URL: https://www.keycloak.org/app/`,
|
||||||
|
` Valid redirect URIs: https://www.keycloak.org/app* http://localhost* (localhost is optional)`,
|
||||||
|
` Valid post logout redirect URIs: https://www.keycloak.org/app* http://localhost*`,
|
||||||
|
` Web origins: *`,
|
||||||
|
` Login Theme: ${buildOptions.themeName}`,
|
||||||
|
` Save (button at the bottom of the page)`,
|
||||||
|
``,
|
||||||
|
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||||
|
`- Got to 👉 http://localhost:8080/realms/myrealm/account 👈 to see your account theme`,
|
||||||
|
``,
|
||||||
|
`Video tutorial: https://youtu.be/WMyGZNHQkjU`,
|
||||||
|
``
|
||||||
|
].join("\n")
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Standalone = {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
|
||||||
|
/*
|
||||||
|
NOTE:
|
||||||
|
|
||||||
|
When we have urlOrigin defined it means that
|
||||||
|
we are building with --external-assets
|
||||||
|
so we have to make sur that the fixed js code will run
|
||||||
|
inside and outside keycloak.
|
||||||
|
|
||||||
|
When urlOrigin isn't defined we can assume the fixedJsCode
|
||||||
|
will always run in keycloak context.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { jsCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
|
||||||
|
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
|
||||||
|
(...[, n, u, e]) => `
|
||||||
|
${n}[(function(){
|
||||||
|
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
|
||||||
|
if( pd === undefined || pd.configurable ){
|
||||||
|
${
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
|
||||||
|
set: function (){}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
var p= "";
|
||||||
|
Object.defineProperty(${n}, "p", {
|
||||||
|
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
|
||||||
|
set: function (value){ p = value;}
|
||||||
|
});
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "${u}";
|
||||||
|
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
|
||||||
|
];
|
||||||
|
|
||||||
|
const fixedJsCode = jsCode
|
||||||
|
.replace(...getReplaceArgs("js"))
|
||||||
|
.replace(...getReplaceArgs("css"))
|
||||||
|
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
|
||||||
|
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
|
||||||
|
)
|
||||||
|
//TODO: Write a test case for this
|
||||||
|
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
|
||||||
|
buildOptions.isStandalone
|
||||||
|
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
|
||||||
|
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedJsCode };
|
||||||
|
}
|
64
src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
Normal file
64
src/bin/keycloakify/replacers/replaceImportsInCssCode.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import * as crypto from "crypto";
|
||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInCssCode(params: { cssCode: string }): {
|
||||||
|
fixedCssCode: string;
|
||||||
|
cssGlobalsToDefine: Record<string, string>;
|
||||||
|
} {
|
||||||
|
const { cssCode } = params;
|
||||||
|
|
||||||
|
const cssGlobalsToDefine: Record<string, string> = {};
|
||||||
|
|
||||||
|
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
|
||||||
|
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
|
||||||
|
);
|
||||||
|
|
||||||
|
let fixedCssCode = cssCode;
|
||||||
|
|
||||||
|
Object.keys(cssGlobalsToDefine).forEach(
|
||||||
|
cssVariableName =>
|
||||||
|
//NOTE: split/join pattern ~ replace all
|
||||||
|
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode, cssGlobalsToDefine };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
|
||||||
|
cssCodeToPrependInHead: string;
|
||||||
|
} {
|
||||||
|
const { cssGlobalsToDefine, buildOptions } = params;
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cssCodeToPrependInHead": [
|
||||||
|
":root {",
|
||||||
|
...Object.keys(cssGlobalsToDefine)
|
||||||
|
.map(cssVariableName =>
|
||||||
|
[
|
||||||
|
`--${cssVariableName}:`,
|
||||||
|
cssGlobalsToDefine[cssVariableName].replace(
|
||||||
|
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
|
||||||
|
"url(${url.resourcesPath}/build/"
|
||||||
|
)
|
||||||
|
].join(" ")
|
||||||
|
)
|
||||||
|
.map(line => ` ${line};`),
|
||||||
|
"}"
|
||||||
|
].join("\n")
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
import type { BuildOptions } from "../BuildOptions";
|
||||||
|
import { assert } from "tsafe/assert";
|
||||||
|
import { is } from "tsafe/is";
|
||||||
|
import { Reflect } from "tsafe/Reflect";
|
||||||
|
|
||||||
|
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||||
|
|
||||||
|
export namespace BuildOptionsLike {
|
||||||
|
export type Common = {
|
||||||
|
urlPathname: string | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Standalone = Common & {
|
||||||
|
isStandalone: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ExternalAssets = Common & {
|
||||||
|
isStandalone: false;
|
||||||
|
urlOrigin: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const buildOptions = Reflect<BuildOptions>();
|
||||||
|
|
||||||
|
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
|
||||||
|
|
||||||
|
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
|
||||||
|
fixedCssCode: string;
|
||||||
|
} {
|
||||||
|
const { cssCode, buildOptions } = params;
|
||||||
|
|
||||||
|
const fixedCssCode = cssCode.replace(
|
||||||
|
buildOptions.urlPathname === undefined
|
||||||
|
? /url\(["']?\/([^/][^)"']+)["']?\)/g
|
||||||
|
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
|
||||||
|
(...[, group]) =>
|
||||||
|
`url(${
|
||||||
|
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
|
||||||
|
})`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { fixedCssCode };
|
||||||
|
}
|
@ -1,102 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
|
||||||
import * as fs from "fs";
|
|
||||||
|
|
||||||
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
pathJoin(keycloakifyDirPath, "dist", "package.json"),
|
|
||||||
Buffer.from(
|
|
||||||
JSON.stringify(
|
|
||||||
(() => {
|
|
||||||
const packageJsonParsed = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"));
|
|
||||||
|
|
||||||
return {
|
|
||||||
...packageJsonParsed,
|
|
||||||
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
|
|
||||||
"types": packageJsonParsed["types"].replace(/^dist\//, ""),
|
|
||||||
};
|
|
||||||
})(),
|
|
||||||
null,
|
|
||||||
2,
|
|
||||||
),
|
|
||||||
"utf8",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const commonThirdPartyDeps = (() => {
|
|
||||||
const namespaceModuleNames = ["@emotion"];
|
|
||||||
const standaloneModuleNames = ["react", "@types/react", "powerhooks", "tss-react", "evt"];
|
|
||||||
|
|
||||||
return [
|
|
||||||
...namespaceModuleNames
|
|
||||||
.map(namespaceModuleName =>
|
|
||||||
fs
|
|
||||||
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
|
|
||||||
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`),
|
|
||||||
)
|
|
||||||
.reduce((prev, curr) => [...prev, ...curr], []),
|
|
||||||
...standaloneModuleNames,
|
|
||||||
];
|
|
||||||
})();
|
|
||||||
|
|
||||||
const yarnHomeDirPath = pathJoin(keycloakifyDirPath, ".yarn_home");
|
|
||||||
|
|
||||||
execSync(["rm -rf", "mkdir"].map(cmd => `${cmd} ${yarnHomeDirPath}`).join(" && "));
|
|
||||||
|
|
||||||
const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
|
|
||||||
const { targetModuleName, cwd } = params;
|
|
||||||
|
|
||||||
const cmd = ["yarn", "link", ...(targetModuleName !== undefined ? [targetModuleName] : [])].join(" ");
|
|
||||||
|
|
||||||
console.log(`$ cd ${pathRelative(keycloakifyDirPath, cwd) || "."} && ${cmd}`);
|
|
||||||
|
|
||||||
execSync(cmd, {
|
|
||||||
cwd,
|
|
||||||
"env": {
|
|
||||||
...process.env,
|
|
||||||
"HOME": yarnHomeDirPath,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const testAppNames = [process.argv[2] ?? "keycloakify-demo-app"] as const;
|
|
||||||
|
|
||||||
const getTestAppPath = (testAppName: typeof testAppNames[number]) => pathJoin(keycloakifyDirPath, "..", testAppName);
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName => execSync("yarn install", { "cwd": getTestAppPath(testAppName) }));
|
|
||||||
|
|
||||||
console.log("=== Linking common dependencies ===");
|
|
||||||
|
|
||||||
const total = commonThirdPartyDeps.length;
|
|
||||||
let current = 0;
|
|
||||||
|
|
||||||
commonThirdPartyDeps.forEach(commonThirdPartyDep => {
|
|
||||||
current++;
|
|
||||||
|
|
||||||
console.log(`${current}/${total} ${commonThirdPartyDep}`);
|
|
||||||
|
|
||||||
const localInstallPath = pathJoin(
|
|
||||||
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])],
|
|
||||||
);
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": localInstallPath });
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": getTestAppPath(testAppName),
|
|
||||||
"targetModuleName": commonThirdPartyDep,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("=== Linking in house dependencies ===");
|
|
||||||
|
|
||||||
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
|
|
||||||
|
|
||||||
testAppNames.forEach(testAppName =>
|
|
||||||
execYarnLink({
|
|
||||||
"cwd": getTestAppPath(testAppName),
|
|
||||||
"targetModuleName": "keycloakify",
|
|
||||||
}),
|
|
||||||
);
|
|
5
src/bin/mockTestingResourcesPath.ts
Normal file
5
src/bin/mockTestingResourcesPath.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { pathJoin } from "./tools/pathJoin";
|
||||||
|
|
||||||
|
export const mockTestingSubDirOfPublicDirBasename = "keycloak_static";
|
||||||
|
export const mockTestingResourcesPath = pathJoin(mockTestingSubDirOfPublicDirBasename, "resources");
|
||||||
|
export const mockTestingResourcesCommonPath = pathJoin(mockTestingResourcesPath, "resources_common");
|
@ -24,9 +24,9 @@ export async function promptKeycloakVersion() {
|
|||||||
"count": 10,
|
"count": 10,
|
||||||
"doIgnoreBeta": true,
|
"doIgnoreBeta": true,
|
||||||
"owner": "keycloak",
|
"owner": "keycloak",
|
||||||
"repo": "keycloak",
|
"repo": "keycloak"
|
||||||
}).then(arr => arr.map(({ tag }) => tag))),
|
}).then(arr => arr.map(({ tag }) => tag))),
|
||||||
"11.0.3",
|
"11.0.3"
|
||||||
];
|
];
|
||||||
|
|
||||||
if (process.env["GITHUB_ACTIONS"] === "true") {
|
if (process.env["GITHUB_ACTIONS"] === "true") {
|
||||||
@ -34,7 +34,7 @@ export async function promptKeycloakVersion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { value: keycloakVersion } = await cliSelect<string>({
|
const { value: keycloakVersion } = await cliSelect<string>({
|
||||||
"values": tags,
|
"values": tags
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
console.log("Aborting");
|
console.log("Aborting");
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ export namespace NpmModuleVersion {
|
|||||||
...(() => {
|
...(() => {
|
||||||
const str = match[4];
|
const str = match[4];
|
||||||
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
|
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
|
||||||
})(),
|
})()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
15
src/bin/tools/cliOptions.ts
Normal file
15
src/bin/tools/cliOptions.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import parseArgv from "minimist";
|
||||||
|
|
||||||
|
export type CliOptions = {
|
||||||
|
isSilent: boolean;
|
||||||
|
hasExternalAssets: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCliOptions = (processArgv: string[]): CliOptions => {
|
||||||
|
const argv = parseArgv(processArgv);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isSilent: typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||||
|
hasExternalAssets: typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||||
|
};
|
||||||
|
};
|
55
src/bin/tools/crc32.ts
Normal file
55
src/bin/tools/crc32.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Readable } from "stream";
|
||||||
|
|
||||||
|
const crc32tab = [
|
||||||
|
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
|
||||||
|
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
|
||||||
|
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
|
||||||
|
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
|
||||||
|
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
|
||||||
|
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
|
||||||
|
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
|
||||||
|
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
|
||||||
|
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
|
||||||
|
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
|
||||||
|
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
|
||||||
|
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
|
||||||
|
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
|
||||||
|
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
|
||||||
|
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
|
||||||
|
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
|
||||||
|
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
|
||||||
|
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
|
||||||
|
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
|
||||||
|
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
|
||||||
|
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
|
||||||
|
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param input either a byte stream, a string or a buffer, you want to have the checksum for
|
||||||
|
* @returns a promise for a checksum (uint32)
|
||||||
|
*/
|
||||||
|
export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||||
|
if (typeof input === "string") {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input.charCodeAt(i)) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Buffer) {
|
||||||
|
let crc = ~0;
|
||||||
|
for (let i = 0; i < input.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ input[i]) & 0xff];
|
||||||
|
return Promise.resolve((crc ^ -1) >>> 0);
|
||||||
|
} else if (input instanceof Readable) {
|
||||||
|
return new Promise<number>((resolve, reject) => {
|
||||||
|
let crc = ~0;
|
||||||
|
input.setMaxListeners(Infinity);
|
||||||
|
input.on("end", () => resolve((crc ^ -1) >>> 0));
|
||||||
|
input.on("error", e => reject(e));
|
||||||
|
input.on("data", (chunk: Buffer) => {
|
||||||
|
for (let i = 0; i < chunk.length; i++) crc = (crc >>> 8) ^ crc32tab[(crc ^ chunk[i]) & 0xff];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Unsupported input " + typeof input);
|
||||||
|
}
|
||||||
|
}
|
61
src/bin/tools/deflate.ts
Normal file
61
src/bin/tools/deflate.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { PassThrough, Readable, TransformCallback, Writable } from "stream";
|
||||||
|
import { pipeline } from "stream/promises";
|
||||||
|
import { deflateRaw as deflateRawCb, createDeflateRaw } from "zlib";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
|
import { crc32 } from "./crc32";
|
||||||
|
import tee from "./tee";
|
||||||
|
|
||||||
|
const deflateRaw = promisify(deflateRawCb);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A stream transformer that records the number of bytes
|
||||||
|
* passed in its `size` property.
|
||||||
|
*/
|
||||||
|
class ByteCounter extends PassThrough {
|
||||||
|
size: number = 0;
|
||||||
|
_transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) {
|
||||||
|
if ("length" in chunk) this.size += chunk.length;
|
||||||
|
super._transform(chunk, encoding, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param data buffer containing the data to be compressed
|
||||||
|
* @returns a buffer containing the compressed/deflated data and the crc32 checksum
|
||||||
|
* of the source data
|
||||||
|
*/
|
||||||
|
export async function deflateBuffer(data: Buffer) {
|
||||||
|
const [deflated, checksum] = await Promise.all([deflateRaw(data), crc32(data)]);
|
||||||
|
return { deflated, crc32: checksum };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param input a byte stream, containing data to be compressed
|
||||||
|
* @param sink a method that will accept chunks of compressed data; We don't pass
|
||||||
|
* a writable here, since we don't want the writablestream to be closed after
|
||||||
|
* a single file
|
||||||
|
* @returns a promise, which will resolve with the crc32 checksum and the
|
||||||
|
* compressed size
|
||||||
|
*/
|
||||||
|
export async function deflateStream(input: Readable, sink: (chunk: Buffer) => void) {
|
||||||
|
const deflateWriter = new Writable({
|
||||||
|
write(chunk, _, callback) {
|
||||||
|
sink(chunk);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// tee the input stream, so we can compress and calc crc32 in parallel
|
||||||
|
const [rs1, rs2] = tee(input);
|
||||||
|
const byteCounter = new ByteCounter();
|
||||||
|
const [_, crc] = await Promise.all([
|
||||||
|
// pipe input into zip compressor, count the bytes
|
||||||
|
// returned and pass compressed data to the sink
|
||||||
|
pipeline(rs1, createDeflateRaw(), byteCounter, deflateWriter),
|
||||||
|
// calc checksum
|
||||||
|
crc32(rs2)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { crc32: crc, compressedSize: byteCounter.size };
|
||||||
|
}
|
@ -1,32 +1,277 @@
|
|||||||
import { basename as pathBasename, join as pathJoin } from "path";
|
import { dirname as pathDirname, basename as pathBasename, join as pathJoin, join } from "path";
|
||||||
import { execSync } from "child_process";
|
import { createReadStream, createWriteStream } from "fs";
|
||||||
import fs from "fs";
|
import { stat, mkdir, unlink, writeFile } from "fs/promises";
|
||||||
import { transformCodebase } from "./transformCodebase";
|
import { transformCodebase } from "./transformCodebase";
|
||||||
import { rm_rf, rm, rm_r } from "./rm";
|
import { createHash } from "crypto";
|
||||||
|
import fetch from "make-fetch-happen";
|
||||||
|
import { createInflateRaw } from "zlib";
|
||||||
|
import type { Readable } from "stream";
|
||||||
|
import { homedir } from "os";
|
||||||
|
import { FetchOptions } from "make-fetch-happen";
|
||||||
|
import { exec as execCallback } from "child_process";
|
||||||
|
import { promisify } from "util";
|
||||||
|
|
||||||
/** assert url ends with .zip */
|
const exec = promisify(execCallback);
|
||||||
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
|
||||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
|
||||||
|
|
||||||
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
|
function hash(s: string) {
|
||||||
const zipFilePath = pathBasename(url);
|
return createHash("sha256").update(s).digest("hex");
|
||||||
|
}
|
||||||
rm_rf(tmpDirPath);
|
|
||||||
|
async function maybeStat(path: string) {
|
||||||
fs.mkdirSync(tmpDirPath, { "recursive": true });
|
try {
|
||||||
|
return await stat(path);
|
||||||
execSync(`curl -L ${url} -o ${zipFilePath}`, { "cwd": tmpDirPath });
|
} catch (error) {
|
||||||
|
if ((error as Error & { code: string }).code === "ENOENT") return undefined;
|
||||||
execSync(`unzip -o ${zipFilePath}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
|
throw error;
|
||||||
"cwd": tmpDirPath,
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
rm(pathBasename(url), { "cwd": tmpDirPath });
|
/**
|
||||||
|
* Get an npm configuration value as string, undefined if not set.
|
||||||
transformCodebase({
|
*
|
||||||
"srcDirPath": pathOfDirToExtractInArchive === undefined ? tmpDirPath : pathJoin(tmpDirPath, pathOfDirToExtractInArchive),
|
* @param key
|
||||||
destDirPath,
|
* @returns string or undefined
|
||||||
});
|
*/
|
||||||
|
async function getNmpConfig(key: string): Promise<string | undefined> {
|
||||||
rm_r(tmpDirPath);
|
const { stdout } = await exec(`npm config get ${key}`);
|
||||||
|
const value = stdout.trim();
|
||||||
|
return value && value !== "null" ? value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get proxy configuration from npm config files. Note that we don't care about
|
||||||
|
* proxy config in env vars, because make-fetch-happen will do that for us.
|
||||||
|
*
|
||||||
|
* @returns proxy configuration
|
||||||
|
*/
|
||||||
|
async function getNpmProxyConfig(): Promise<Pick<FetchOptions, "proxy" | "noProxy">> {
|
||||||
|
const proxy = (await getNmpConfig("https-proxy")) ?? (await getNmpConfig("proxy"));
|
||||||
|
const noProxy = (await getNmpConfig("noproxy")) ?? (await getNmpConfig("no-proxy"));
|
||||||
|
|
||||||
|
return { proxy, noProxy };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a file from `url` to `dir`. Will try to avoid downloading existing
|
||||||
|
* files by using the cache directory ~/.keycloakify/cache
|
||||||
|
*
|
||||||
|
* If the target directory does not exist, it will be created.
|
||||||
|
*
|
||||||
|
* If the target file exists, it will be overwritten.
|
||||||
|
*
|
||||||
|
* We use make-fetch-happen's internal file cache here, so we don't need to
|
||||||
|
* worry about redownloading the same file over and over. Unfortunately, that
|
||||||
|
* cache does not have a single file per entry, but bundles and indexes them,
|
||||||
|
* so we still need to write the contents to the target directory (possibly
|
||||||
|
* over and over), cause the current unzip implementation wants random access.
|
||||||
|
*
|
||||||
|
* @param url download url
|
||||||
|
* @param dir target directory
|
||||||
|
* @param filename target filename
|
||||||
|
* @returns promise for the full path of the downloaded file
|
||||||
|
*/
|
||||||
|
async function download(url: string, dir: string, filename: string): Promise<string> {
|
||||||
|
const proxyOpts = await getNpmProxyConfig();
|
||||||
|
const cacheRoot = process.env.XDG_CACHE_HOME ?? homedir();
|
||||||
|
const cachePath = join(cacheRoot, ".keycloakify/cache");
|
||||||
|
const opts: FetchOptions = { cachePath, ...proxyOpts };
|
||||||
|
const response = await fetch(url, opts);
|
||||||
|
const filepath = pathJoin(dir, filename);
|
||||||
|
await mkdir(dir, { recursive: true });
|
||||||
|
await writeFile(filepath, response.body);
|
||||||
|
return filepath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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.setMaxListeners(Infinity);
|
||||||
|
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.setMaxListeners(Infinity);
|
||||||
|
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 the 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 filename = pathBasename(url);
|
||||||
|
const zipFilepath = await download(url, cacheDirPath, filename);
|
||||||
|
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 });
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
function getProjectRootRec(dirPath: string): string {
|
function getProjectRootRec(dirPath: string): string {
|
||||||
if (fs.existsSync(path.join(dirPath, "tsconfig.json"))) {
|
if (fs.existsSync(path.join(dirPath, "package.json"))) {
|
||||||
return dirPath;
|
return dirPath;
|
||||||
}
|
}
|
||||||
return getProjectRootRec(path.join(dirPath, ".."));
|
return getProjectRootRec(path.join(dirPath, ".."));
|
||||||
|
@ -1,9 +1,17 @@
|
|||||||
import { getProjectRoot } from "./getProjectRoot";
|
import { getProjectRoot } from "./getProjectRoot";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import child_process from "child_process";
|
import { constants } from "fs";
|
||||||
|
import { chmod, stat } from "fs/promises";
|
||||||
|
|
||||||
Object.entries<string>(require(pathJoin(getProjectRoot(), "package.json"))["bin"]).forEach(([, scriptPath]) =>
|
(async () => {
|
||||||
child_process.execSync(`chmod +x ${scriptPath}`, {
|
const { bin } = await import(pathJoin(getProjectRoot(), "package.json"));
|
||||||
"cwd": getProjectRoot(),
|
|
||||||
}),
|
const 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);
|
||||||
|
})();
|
||||||
|
102
src/bin/tools/jar.ts
Normal file
102
src/bin/tools/jar.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { Readable, Transform } from "stream";
|
||||||
|
import { dirname, relative, sep } from "path";
|
||||||
|
import { createWriteStream } from "fs";
|
||||||
|
|
||||||
|
import walk from "./walk";
|
||||||
|
import type { ZipSource } from "./zip";
|
||||||
|
import zip from "./zip";
|
||||||
|
import { mkdir } from "fs/promises";
|
||||||
|
|
||||||
|
/** Trim leading whitespace from every line */
|
||||||
|
const trimIndent = (s: string) => s.replace(/(\n)\s+/g, "$1");
|
||||||
|
|
||||||
|
type JarArgs = {
|
||||||
|
rootPath: string;
|
||||||
|
targetPath: string;
|
||||||
|
groupId: string;
|
||||||
|
artifactId: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a jar archive, using the resources found at `rootPath` (a directory) and write the
|
||||||
|
* archive to `targetPath` (a file). Use `groupId`, `artifactId` and `version` to define
|
||||||
|
* the contents of the pom.properties file which is going to be added to the archive.
|
||||||
|
*/
|
||||||
|
export default async function jar({ groupId, artifactId, version, rootPath, targetPath }: JarArgs) {
|
||||||
|
const manifest: ZipSource = {
|
||||||
|
path: "META-INF/MANIFEST.MF",
|
||||||
|
data: Buffer.from(
|
||||||
|
trimIndent(
|
||||||
|
`Manifest-Version: 1.0
|
||||||
|
Archiver-Version: Plexus Archiver
|
||||||
|
Created-By: Keycloakify
|
||||||
|
Built-By: unknown
|
||||||
|
Build-Jdk: 19.0.0`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
const pomProps: ZipSource = {
|
||||||
|
path: `META-INF/maven/${groupId}/${artifactId}/pom.properties`,
|
||||||
|
data: Buffer.from(
|
||||||
|
trimIndent(
|
||||||
|
`# Generated by keycloakify
|
||||||
|
# ${new Date()}
|
||||||
|
artifactId=${artifactId}
|
||||||
|
groupId=${groupId}
|
||||||
|
version=${version}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert every path entry to a ZipSource record, and when all records are
|
||||||
|
* processed, append records for MANIFEST.mf and pom.properties
|
||||||
|
*/
|
||||||
|
const pathToRecord = () =>
|
||||||
|
new Transform({
|
||||||
|
objectMode: true,
|
||||||
|
transform: function (fsPath, _, cb) {
|
||||||
|
const path = relative(rootPath, fsPath).split(sep).join("/");
|
||||||
|
this.push({ path, fsPath });
|
||||||
|
cb();
|
||||||
|
},
|
||||||
|
final: function () {
|
||||||
|
this.push(manifest);
|
||||||
|
this.push(pomProps);
|
||||||
|
this.push(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await mkdir(dirname(targetPath), { recursive: true });
|
||||||
|
|
||||||
|
// Create an async pipeline, wait until everything is fully processed
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
// walk all files in `rootPath` recursively
|
||||||
|
Readable.from(walk(rootPath))
|
||||||
|
// transform every path into a ZipSource object
|
||||||
|
.pipe(pathToRecord())
|
||||||
|
// let the zip lib convert all ZipSource objects into a byte stream
|
||||||
|
.pipe(zip())
|
||||||
|
// write that byte stream to targetPath
|
||||||
|
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
|
||||||
|
.on("finish", () => resolve())
|
||||||
|
.on("error", e => reject(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standalone usage, call e.g. `ts-node jar.ts dirWithSources some-jar.jar`
|
||||||
|
*/
|
||||||
|
if (require.main === module) {
|
||||||
|
const main = () =>
|
||||||
|
jar({
|
||||||
|
rootPath: process.argv[2],
|
||||||
|
targetPath: process.argv[3],
|
||||||
|
artifactId: process.env.ARTIFACT_ID ?? "artifact",
|
||||||
|
groupId: process.env.GROUP_ID ?? "group",
|
||||||
|
version: process.env.VERSION ?? "1.0.0"
|
||||||
|
});
|
||||||
|
main().catch(e => console.error(e));
|
||||||
|
}
|
7
src/bin/tools/kebabCaseToSnakeCase.ts
Normal file
7
src/bin/tools/kebabCaseToSnakeCase.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { capitalize } from "tsafe/capitalize";
|
||||||
|
|
||||||
|
export function kebabCaseToCamelCase(kebabCaseString: string): string {
|
||||||
|
const [first, ...rest] = kebabCaseString.split("-");
|
||||||
|
|
||||||
|
return [first, ...rest.map(capitalize)].join("");
|
||||||
|
}
|
27
src/bin/tools/logger.ts
Normal file
27
src/bin/tools/logger.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
type LoggerOpts = {
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Logger = {
|
||||||
|
log: (message: string, opts?: LoggerOpts) => void;
|
||||||
|
warn: (message: string) => void;
|
||||||
|
error: (message: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getLogger = ({ isSilent }: { isSilent?: boolean } = {}): Logger => {
|
||||||
|
return {
|
||||||
|
log: (message, { force } = {}) => {
|
||||||
|
if (isSilent && !force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(message);
|
||||||
|
},
|
||||||
|
warn: message => {
|
||||||
|
console.warn(message);
|
||||||
|
},
|
||||||
|
error: message => {
|
||||||
|
console.error(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -19,7 +19,7 @@ export function listTagsFactory(params: { octokit: Octokit }) {
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
per_page,
|
per_page,
|
||||||
"page": page++,
|
"page": page++
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const branch of resp.data.map(({ name }) => name)) {
|
for (const branch of resp.data.map(({ name }) => name)) {
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
import { execSync } from "child_process";
|
|
||||||
|
|
||||||
function rmInternal(params: { pathToRemove: string; args: string | undefined; cwd: string | undefined }) {
|
|
||||||
const { pathToRemove, args, cwd } = params;
|
|
||||||
|
|
||||||
execSync(`rm ${args ? `-${args} ` : ""}${pathToRemove.replace(/ /g, "\\ ")}`, cwd !== undefined ? { cwd } : undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rm(pathToRemove: string, options?: { cwd: string }) {
|
|
||||||
rmInternal({
|
|
||||||
pathToRemove,
|
|
||||||
"args": undefined,
|
|
||||||
"cwd": options?.cwd,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rm_r(pathToRemove: string, options?: { cwd: string }) {
|
|
||||||
rmInternal({
|
|
||||||
pathToRemove,
|
|
||||||
"args": "r",
|
|
||||||
"cwd": options?.cwd,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function rm_rf(pathToRemove: string, options?: { cwd: string }) {
|
|
||||||
rmInternal({
|
|
||||||
pathToRemove,
|
|
||||||
"args": "rf",
|
|
||||||
"cwd": options?.cwd,
|
|
||||||
});
|
|
||||||
}
|
|
37
src/bin/tools/tee.ts
Normal file
37
src/bin/tools/tee.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { PassThrough, Readable } from "stream";
|
||||||
|
|
||||||
|
export default function tee(input: Readable) {
|
||||||
|
const a = new PassThrough();
|
||||||
|
const b = new PassThrough();
|
||||||
|
|
||||||
|
let aFull = false;
|
||||||
|
let bFull = false;
|
||||||
|
|
||||||
|
a.on("drain", () => {
|
||||||
|
aFull = false;
|
||||||
|
if (!aFull && !bFull) input.resume();
|
||||||
|
});
|
||||||
|
b.on("drain", () => {
|
||||||
|
bFull = false;
|
||||||
|
if (!aFull && !bFull) input.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("error", e => {
|
||||||
|
a.emit("error", e);
|
||||||
|
b.emit("error", e);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("data", chunk => {
|
||||||
|
aFull = !a.write(chunk);
|
||||||
|
bFull = !b.write(chunk);
|
||||||
|
|
||||||
|
if (aFull || bFull) input.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.on("end", () => {
|
||||||
|
a.end();
|
||||||
|
b.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
return [a, b] as const;
|
||||||
|
}
|
@ -16,8 +16,8 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
|||||||
srcDirPath,
|
srcDirPath,
|
||||||
destDirPath,
|
destDirPath,
|
||||||
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
|
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
|
||||||
"modifiedSourceCode": sourceCode,
|
"modifiedSourceCode": sourceCode
|
||||||
})),
|
}))
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
for (const file_relative_path of crawl(srcDirPath)) {
|
for (const file_relative_path of crawl(srcDirPath)) {
|
||||||
@ -25,7 +25,7 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
|||||||
|
|
||||||
const transformSourceCodeResult = transformSourceCode({
|
const transformSourceCodeResult = transformSourceCode({
|
||||||
"sourceCode": fs.readFileSync(filePath),
|
"sourceCode": fs.readFileSync(filePath),
|
||||||
"filePath": path.join(srcDirPath, file_relative_path),
|
"filePath": path.join(srcDirPath, file_relative_path)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (transformSourceCodeResult === undefined) {
|
if (transformSourceCodeResult === undefined) {
|
||||||
@ -33,14 +33,14 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
|
|||||||
}
|
}
|
||||||
|
|
||||||
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
|
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
|
||||||
"recursive": true,
|
"recursive": true
|
||||||
});
|
});
|
||||||
|
|
||||||
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
|
||||||
|
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
|
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
|
||||||
modifiedSourceCode,
|
modifiedSourceCode
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
19
src/bin/tools/walk.ts
Normal file
19
src/bin/tools/walk.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { readdir } from "fs/promises";
|
||||||
|
import { resolve } from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously and recursively walk a directory tree, yielding every file and directory
|
||||||
|
* found
|
||||||
|
*
|
||||||
|
* @param root the starting directory
|
||||||
|
* @returns AsyncGenerator
|
||||||
|
*/
|
||||||
|
export default async function* walk(root: string): AsyncGenerator<string, void, void> {
|
||||||
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||||
|
const absolutePath = resolve(root, entry.name);
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
yield absolutePath;
|
||||||
|
yield* walk(absolutePath);
|
||||||
|
} else yield absolutePath;
|
||||||
|
}
|
||||||
|
}
|
246
src/bin/tools/zip.ts
Normal file
246
src/bin/tools/zip.ts
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
import { Transform, TransformOptions } from "stream";
|
||||||
|
import { createReadStream } from "fs";
|
||||||
|
import { stat } from "fs/promises";
|
||||||
|
import { Blob } from "buffer";
|
||||||
|
|
||||||
|
import { deflateBuffer, deflateStream } from "./deflate";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zip source
|
||||||
|
* @property filename the name of the entry in the archie
|
||||||
|
* @property path of the source file, if the source is an actual file
|
||||||
|
* @property data the actual data buffer, if the source is constructed in-memory
|
||||||
|
*/
|
||||||
|
export type ZipSource = { path: string } & ({ fsPath: string } | { data: Buffer });
|
||||||
|
|
||||||
|
export type ZipRecord = {
|
||||||
|
path: string;
|
||||||
|
compression: "deflate" | undefined;
|
||||||
|
uncompressedSize: number;
|
||||||
|
compressedSize?: number;
|
||||||
|
crc32?: number;
|
||||||
|
offset?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the actual byte size of an string
|
||||||
|
*/
|
||||||
|
function utf8size(s: string) {
|
||||||
|
return new Blob([s]).size;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param record
|
||||||
|
* @returns a buffer representing a Zip local header
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Local_file_header
|
||||||
|
*/
|
||||||
|
function localHeader(record: ZipRecord) {
|
||||||
|
const { path, compression, uncompressedSize } = record;
|
||||||
|
const filenameSize = utf8size(path);
|
||||||
|
const buf = Buffer.alloc(30 + filenameSize);
|
||||||
|
|
||||||
|
buf.writeUInt32LE(0x04_03_4b_50, 0); // local header signature
|
||||||
|
buf.writeUInt16LE(10, 4); // min version
|
||||||
|
// we write 0x08 because crc and compressed size are unknown at
|
||||||
|
buf.writeUInt16LE(0x08, 6); // general purpose bit flag
|
||||||
|
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 8);
|
||||||
|
buf.writeUInt16LE(0, 10); // modified time
|
||||||
|
buf.writeUInt16LE(0, 12); // modified date
|
||||||
|
buf.writeUInt32LE(0, 14); // crc unknown
|
||||||
|
buf.writeUInt32LE(0, 18); // compressed size unknown
|
||||||
|
buf.writeUInt32LE(uncompressedSize, 22);
|
||||||
|
buf.writeUInt16LE(filenameSize, 26);
|
||||||
|
buf.writeUInt16LE(0, 28); // extra field length
|
||||||
|
buf.write(path, 30, "utf-8");
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param record
|
||||||
|
* @returns a buffer representing a Zip central header
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#Central_directory_file_header
|
||||||
|
*/
|
||||||
|
function centralHeader(record: ZipRecord) {
|
||||||
|
const { path, compression, crc32, compressedSize, uncompressedSize, offset } = record;
|
||||||
|
const filenameSize = utf8size(path);
|
||||||
|
const buf = Buffer.alloc(46 + filenameSize);
|
||||||
|
const isFile = !path.endsWith("/");
|
||||||
|
|
||||||
|
if (typeof offset === "undefined") throw new Error("Illegal argument");
|
||||||
|
|
||||||
|
// we don't want to deal with possibly messed up file or directory
|
||||||
|
// permissions, so we ignore the original permissions
|
||||||
|
const externalAttr = isFile ? 0x81a40000 : 0x41ed0000;
|
||||||
|
|
||||||
|
buf.writeUInt32LE(0x0201_4b50, 0); // central header signature
|
||||||
|
buf.writeUInt16LE(10, 4); // version
|
||||||
|
buf.writeUInt16LE(10, 6); // min version
|
||||||
|
buf.writeUInt16LE(0, 8); // general purpose bit flag
|
||||||
|
buf.writeUInt16LE(compression ? ({ "deflate": 8 } as const)[compression] : 0, 10);
|
||||||
|
buf.writeUInt16LE(0, 12); // modified time
|
||||||
|
buf.writeUInt16LE(0, 14); // modified date
|
||||||
|
buf.writeUInt32LE(crc32 || 0, 16);
|
||||||
|
buf.writeUInt32LE(compressedSize || 0, 20);
|
||||||
|
buf.writeUInt32LE(uncompressedSize, 24);
|
||||||
|
buf.writeUInt16LE(filenameSize, 28);
|
||||||
|
buf.writeUInt16LE(0, 30); // extra field length
|
||||||
|
buf.writeUInt16LE(0, 32); // comment field length
|
||||||
|
buf.writeUInt16LE(0, 34); // disk number
|
||||||
|
buf.writeUInt16LE(0, 36); // internal
|
||||||
|
buf.writeUInt32LE(externalAttr, 38); // external
|
||||||
|
buf.writeUInt32LE(offset, 42); // offset where file starts
|
||||||
|
buf.write(path, 46, "utf-8");
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a buffer representing an Zip End-Of-Central-Directory block
|
||||||
|
* @link https://en.wikipedia.org/wiki/ZIP_(file_format)#End_of_central_directory_record_(EOCD)
|
||||||
|
*/
|
||||||
|
function eocd({ offset, cdSize, nRecords }: { offset: number; cdSize: number; nRecords: number }) {
|
||||||
|
const buf = Buffer.alloc(22);
|
||||||
|
buf.writeUint32LE(0x06054b50, 0); // eocd signature
|
||||||
|
buf.writeUInt16LE(0, 4); // disc number
|
||||||
|
buf.writeUint16LE(0, 6); // disc where central directory starts
|
||||||
|
buf.writeUint16LE(nRecords, 8); // records on this disc
|
||||||
|
buf.writeUInt16LE(nRecords, 10); // records total
|
||||||
|
buf.writeUInt32LE(cdSize, 12); // byte size of cd
|
||||||
|
buf.writeUInt32LE(offset, 16); // cd offset
|
||||||
|
buf.writeUint16LE(0, 20); // comment length
|
||||||
|
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns a stream Transform, which reads a stream of ZipRecords and
|
||||||
|
* writes a bytestream
|
||||||
|
*/
|
||||||
|
export default function zip() {
|
||||||
|
/**
|
||||||
|
* This is called when the input stream of ZipSource items is finished.
|
||||||
|
* Will write central directory and end-of-central-direcotry blocks.
|
||||||
|
*/
|
||||||
|
const final = () => {
|
||||||
|
// write central directory
|
||||||
|
let cdSize = 0;
|
||||||
|
for (const record of records) {
|
||||||
|
const head = centralHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
cdSize += head.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write end-of-central-directory
|
||||||
|
zipTransform.push(eocd({ offset, cdSize, nRecords: records.length }));
|
||||||
|
// signal stream end
|
||||||
|
zipTransform.push(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a directory entry to the archive
|
||||||
|
* @param path
|
||||||
|
*/
|
||||||
|
const writeDir = async (path: string) => {
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path: path + "/",
|
||||||
|
offset,
|
||||||
|
compression: undefined,
|
||||||
|
uncompressedSize: 0
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write a file entry to the archive
|
||||||
|
* @param archivePath path of the file in archive
|
||||||
|
* @param fsPath path to file on filesystem
|
||||||
|
* @param size of the actual, uncompressed, file
|
||||||
|
*/
|
||||||
|
const writeFile = async (archivePath: string, fsPath: string, size: number) => {
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path: archivePath,
|
||||||
|
offset,
|
||||||
|
compression: "deflate",
|
||||||
|
uncompressedSize: size
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
|
||||||
|
const { crc32, compressedSize } = await deflateStream(createReadStream(fsPath), chunk => zipTransform.push(chunk));
|
||||||
|
|
||||||
|
record.crc32 = crc32;
|
||||||
|
record.compressedSize = compressedSize;
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length + compressedSize;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write archive record based on filesystem file or directory
|
||||||
|
* @param archivePath path of item in archive
|
||||||
|
* @param fsPath path to item on filesystem
|
||||||
|
*/
|
||||||
|
const writeFromPath = async (archivePath: string, fsPath: string) => {
|
||||||
|
const fileStats = await stat(fsPath);
|
||||||
|
fileStats.isDirectory() ? await writeDir(archivePath) /**/ : await writeFile(archivePath, fsPath, fileStats.size) /**/;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write archive record based on data in a buffer
|
||||||
|
* @param path
|
||||||
|
* @param data
|
||||||
|
*/
|
||||||
|
const writeFromBuffer = async (path: string, data: Buffer) => {
|
||||||
|
const { deflated, crc32 } = await deflateBuffer(data);
|
||||||
|
const record: ZipRecord = {
|
||||||
|
path,
|
||||||
|
compression: "deflate",
|
||||||
|
crc32,
|
||||||
|
uncompressedSize: data.length,
|
||||||
|
compressedSize: deflated.length,
|
||||||
|
offset
|
||||||
|
};
|
||||||
|
const head = localHeader(record);
|
||||||
|
zipTransform.push(head);
|
||||||
|
zipTransform.push(deflated);
|
||||||
|
records.push(record);
|
||||||
|
offset += head.length + deflated.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write an archive record
|
||||||
|
* @param source
|
||||||
|
*/
|
||||||
|
const writeRecord = async (source: ZipSource) => {
|
||||||
|
if ("fsPath" in source) await writeFromPath(source.path, source.fsPath);
|
||||||
|
else if ("data" in source) await writeFromBuffer(source.path, source.data);
|
||||||
|
else throw new Error("Illegal argument " + typeof source + " " + JSON.stringify(source));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The actual stream transform function
|
||||||
|
* @param source
|
||||||
|
* @param _ encoding, ignored
|
||||||
|
* @param cb
|
||||||
|
*/
|
||||||
|
const transform: TransformOptions["transform"] = async (source: ZipSource, _, cb) => {
|
||||||
|
await writeRecord(source);
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** offset and records keep local state during processing */
|
||||||
|
let offset = 0;
|
||||||
|
const records: ZipRecord[] = [];
|
||||||
|
|
||||||
|
const zipTransform = new Transform({
|
||||||
|
readableObjectMode: false,
|
||||||
|
writableObjectMode: true,
|
||||||
|
transform,
|
||||||
|
final
|
||||||
|
});
|
||||||
|
|
||||||
|
return zipTransform;
|
||||||
|
}
|
11
src/bin/tsconfig.json
Normal file
11
src/bin/tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsproject.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"target": "ES5",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"lib": ["es2015", "DOM", "ES2019.Object"],
|
||||||
|
"outDir": "../../dist/bin",
|
||||||
|
"rootDir": "."
|
||||||
|
}
|
||||||
|
}
|
1
src/index.ts
Normal file
1
src/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { createKeycloakAdapter } from "keycloakify/lib/keycloakJsAdapter";
|
@ -1,32 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const Error = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Error } & KcProps) => {
|
|
||||||
const { msg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { message, client } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("errorTitle")}
|
|
||||||
formNode={
|
|
||||||
<div id="kc-error-message">
|
|
||||||
<p className="instruction">{message.summary}</p>
|
|
||||||
{client !== undefined && client.baseUrl !== undefined && (
|
|
||||||
<p>
|
|
||||||
<a id="backToApplication" href={client.baseUrl}>
|
|
||||||
{msg("backToApplication")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,49 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import { assert } from "../tools/assert";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const Info = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Info } & KcProps) => {
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
assert(kcContext.message !== undefined);
|
|
||||||
|
|
||||||
const { messageHeader, message, requiredActions, skipLink, pageRedirectUri, actionUri, client } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={messageHeader !== undefined ? <>{messageHeader}</> : <>{message.summary}</>}
|
|
||||||
formNode={
|
|
||||||
<div id="kc-info-message">
|
|
||||||
<p className="instruction">
|
|
||||||
{message.summary}
|
|
||||||
|
|
||||||
{requiredActions !== undefined && (
|
|
||||||
<b>{requiredActions.map(requiredAction => msgStr(`requiredAction.${requiredAction}` as const)).join(",")}</b>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
{!skipLink && pageRedirectUri !== undefined ? (
|
|
||||||
<p>
|
|
||||||
<a href={pageRedirectUri}>{msg("backToApplication")}</a>
|
|
||||||
</p>
|
|
||||||
) : actionUri !== undefined ? (
|
|
||||||
<p>
|
|
||||||
<a href={actionUri}>{msg("proceedWithAction")}</a>
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
client.baseUrl !== undefined && (
|
|
||||||
<p>
|
|
||||||
<a href={client.baseUrl}>{msg("backToApplication")}</a>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,56 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import { Login } from "./Login";
|
|
||||||
import { Register } from "./Register";
|
|
||||||
import { RegisterUserProfile } from "./RegisterUserProfile";
|
|
||||||
import { Info } from "./Info";
|
|
||||||
import { Error } from "./Error";
|
|
||||||
import { LoginResetPassword } from "./LoginResetPassword";
|
|
||||||
import { LoginVerifyEmail } from "./LoginVerifyEmail";
|
|
||||||
import { Terms } from "./Terms";
|
|
||||||
import { LoginOtp } from "./LoginOtp";
|
|
||||||
import { LoginUpdatePassword } from "./LoginUpdatePassword";
|
|
||||||
import { LoginUpdateProfile } from "./LoginUpdateProfile";
|
|
||||||
import { LoginIdpLinkConfirm } from "./LoginIdpLinkConfirm";
|
|
||||||
import { LoginPageExpired } from "./LoginPageExpired";
|
|
||||||
import { LoginIdpLinkEmail } from "./LoginIdpLinkEmail";
|
|
||||||
import { LoginConfigTotp } from "./LoginConfigTotp";
|
|
||||||
import { LogoutConfirm } from "./LogoutConfirm";
|
|
||||||
|
|
||||||
export const KcApp = memo(({ kcContext, ...props }: { kcContext: KcContextBase } & KcProps) => {
|
|
||||||
switch (kcContext.pageId) {
|
|
||||||
case "login.ftl":
|
|
||||||
return <Login {...{ kcContext, ...props }} />;
|
|
||||||
case "register.ftl":
|
|
||||||
return <Register {...{ kcContext, ...props }} />;
|
|
||||||
case "register-user-profile.ftl":
|
|
||||||
return <RegisterUserProfile {...{ kcContext, ...props }} />;
|
|
||||||
case "info.ftl":
|
|
||||||
return <Info {...{ kcContext, ...props }} />;
|
|
||||||
case "error.ftl":
|
|
||||||
return <Error {...{ kcContext, ...props }} />;
|
|
||||||
case "login-reset-password.ftl":
|
|
||||||
return <LoginResetPassword {...{ kcContext, ...props }} />;
|
|
||||||
case "login-verify-email.ftl":
|
|
||||||
return <LoginVerifyEmail {...{ kcContext, ...props }} />;
|
|
||||||
case "terms.ftl":
|
|
||||||
return <Terms {...{ kcContext, ...props }} />;
|
|
||||||
case "login-otp.ftl":
|
|
||||||
return <LoginOtp {...{ kcContext, ...props }} />;
|
|
||||||
case "login-update-password.ftl":
|
|
||||||
return <LoginUpdatePassword {...{ kcContext, ...props }} />;
|
|
||||||
case "login-update-profile.ftl":
|
|
||||||
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
|
|
||||||
case "login-idp-link-confirm.ftl":
|
|
||||||
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
|
|
||||||
case "login-idp-link-email.ftl":
|
|
||||||
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />;
|
|
||||||
case "login-page-expired.ftl":
|
|
||||||
return <LoginPageExpired {...{ kcContext, ...props }} />;
|
|
||||||
case "login-config-totp.ftl":
|
|
||||||
return <LoginConfigTotp {...{ kcContext, ...props }} />;
|
|
||||||
case "logout-confirm.ftl":
|
|
||||||
return <LogoutConfirm {...{ kcContext, ...props }} />;
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,203 +0,0 @@
|
|||||||
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
|
|
||||||
/** Class names can be provided as an array or separated by whitespace */
|
|
||||||
export type KcPropsGeneric<CssClasses extends string> = {
|
|
||||||
[key in CssClasses]: readonly string[] | string | undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type KcTemplateClassKey =
|
|
||||||
| "stylesCommon"
|
|
||||||
| "styles"
|
|
||||||
| "scripts"
|
|
||||||
| "kcHtmlClass"
|
|
||||||
| "kcLoginClass"
|
|
||||||
| "kcHeaderClass"
|
|
||||||
| "kcHeaderWrapperClass"
|
|
||||||
| "kcFormCardClass"
|
|
||||||
| "kcFormCardAccountClass"
|
|
||||||
| "kcFormHeaderClass"
|
|
||||||
| "kcLocaleWrapperClass"
|
|
||||||
| "kcContentWrapperClass"
|
|
||||||
| "kcLabelWrapperClass"
|
|
||||||
| "kcFormGroupClass"
|
|
||||||
| "kcResetFlowIcon"
|
|
||||||
| "kcFeedbackSuccessIcon"
|
|
||||||
| "kcFeedbackWarningIcon"
|
|
||||||
| "kcFeedbackErrorIcon"
|
|
||||||
| "kcFeedbackInfoIcon"
|
|
||||||
| "kcFormSocialAccountContentClass"
|
|
||||||
| "kcFormSocialAccountClass"
|
|
||||||
| "kcSignUpClass"
|
|
||||||
| "kcInfoAreaWrapperClass";
|
|
||||||
|
|
||||||
export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>;
|
|
||||||
|
|
||||||
export const defaultKcTemplateProps = {
|
|
||||||
"stylesCommon": [
|
|
||||||
"node_modules/patternfly/dist/css/patternfly.min.css",
|
|
||||||
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
|
|
||||||
"lib/zocial/zocial.css",
|
|
||||||
],
|
|
||||||
"styles": ["css/login.css"],
|
|
||||||
"scripts": [],
|
|
||||||
"kcHtmlClass": ["login-pf"],
|
|
||||||
"kcLoginClass": ["login-pf-page"],
|
|
||||||
"kcContentWrapperClass": ["row"],
|
|
||||||
"kcHeaderClass": ["login-pf-page-header"],
|
|
||||||
"kcHeaderWrapperClass": [],
|
|
||||||
"kcFormCardClass": ["card-pf"],
|
|
||||||
"kcFormCardAccountClass": ["login-pf-accounts"],
|
|
||||||
"kcFormSocialAccountClass": ["login-pf-social-section"],
|
|
||||||
"kcFormSocialAccountContentClass": ["col-xs-12", "col-sm-6"],
|
|
||||||
"kcFormHeaderClass": ["login-pf-header"],
|
|
||||||
"kcLocaleWrapperClass": [],
|
|
||||||
"kcFeedbackErrorIcon": ["pficon", "pficon-error-circle-o"],
|
|
||||||
"kcFeedbackWarningIcon": ["pficon", "pficon-warning-triangle-o"],
|
|
||||||
"kcFeedbackSuccessIcon": ["pficon", "pficon-ok"],
|
|
||||||
"kcFeedbackInfoIcon": ["pficon", "pficon-info"],
|
|
||||||
"kcResetFlowIcon": ["pficon", "pficon-arrow fa-2x"],
|
|
||||||
"kcFormGroupClass": ["form-group"],
|
|
||||||
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
|
||||||
"kcSignUpClass": ["login-pf-signup"],
|
|
||||||
"kcInfoAreaWrapperClass": [],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
|
|
||||||
|
|
||||||
/** Tu use if you don't want any default */
|
|
||||||
export const allClearKcTemplateProps = allPropertiesValuesToUndefined(defaultKcTemplateProps);
|
|
||||||
|
|
||||||
assert<typeof allClearKcTemplateProps extends KcTemplateProps ? true : false>();
|
|
||||||
|
|
||||||
export type KcProps = KcPropsGeneric<
|
|
||||||
| KcTemplateClassKey
|
|
||||||
| "kcLogoLink"
|
|
||||||
| "kcLogoClass"
|
|
||||||
| "kcContainerClass"
|
|
||||||
| "kcContentClass"
|
|
||||||
| "kcFeedbackAreaClass"
|
|
||||||
| "kcLocaleClass"
|
|
||||||
| "kcAlertIconClasserror"
|
|
||||||
| "kcFormAreaClass"
|
|
||||||
| "kcFormSocialAccountListClass"
|
|
||||||
| "kcFormSocialAccountDoubleListClass"
|
|
||||||
| "kcFormSocialAccountListLinkClass"
|
|
||||||
| "kcWebAuthnKeyIcon"
|
|
||||||
| "kcFormClass"
|
|
||||||
| "kcFormGroupErrorClass"
|
|
||||||
| "kcLabelClass"
|
|
||||||
| "kcInputClass"
|
|
||||||
| "kcInputErrorMessageClass"
|
|
||||||
| "kcInputWrapperClass"
|
|
||||||
| "kcFormOptionsClass"
|
|
||||||
| "kcFormButtonsClass"
|
|
||||||
| "kcFormSettingClass"
|
|
||||||
| "kcTextareaClass"
|
|
||||||
| "kcInfoAreaClass"
|
|
||||||
| "kcFormGroupHeader"
|
|
||||||
| "kcButtonClass"
|
|
||||||
| "kcButtonPrimaryClass"
|
|
||||||
| "kcButtonDefaultClass"
|
|
||||||
| "kcButtonLargeClass"
|
|
||||||
| "kcButtonBlockClass"
|
|
||||||
| "kcInputLargeClass"
|
|
||||||
| "kcSrOnlyClass"
|
|
||||||
| "kcSelectAuthListClass"
|
|
||||||
| "kcSelectAuthListItemClass"
|
|
||||||
| "kcSelectAuthListItemInfoClass"
|
|
||||||
| "kcSelectAuthListItemLeftClass"
|
|
||||||
| "kcSelectAuthListItemBodyClass"
|
|
||||||
| "kcSelectAuthListItemDescriptionClass"
|
|
||||||
| "kcSelectAuthListItemHeadingClass"
|
|
||||||
| "kcSelectAuthListItemHelpTextClass"
|
|
||||||
| "kcAuthenticatorDefaultClass"
|
|
||||||
| "kcAuthenticatorPasswordClass"
|
|
||||||
| "kcAuthenticatorOTPClass"
|
|
||||||
| "kcAuthenticatorWebAuthnClass"
|
|
||||||
| "kcAuthenticatorWebAuthnPasswordlessClass"
|
|
||||||
| "kcSelectOTPListClass"
|
|
||||||
| "kcSelectOTPListItemClass"
|
|
||||||
| "kcAuthenticatorOtpCircleClass"
|
|
||||||
| "kcSelectOTPItemHeadingClass"
|
|
||||||
| "kcFormOptionsWrapperClass"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const defaultKcProps = {
|
|
||||||
...defaultKcTemplateProps,
|
|
||||||
"kcLogoLink": "http://www.keycloak.org",
|
|
||||||
"kcLogoClass": "login-pf-brand",
|
|
||||||
"kcContainerClass": "container-fluid",
|
|
||||||
"kcContentClass": ["col-sm-8", "col-sm-offset-2", "col-md-6", "col-md-offset-3", "col-lg-6", "col-lg-offset-3"],
|
|
||||||
"kcFeedbackAreaClass": ["col-md-12"],
|
|
||||||
"kcLocaleClass": ["col-xs-12", "col-sm-1"],
|
|
||||||
"kcAlertIconClasserror": ["pficon", "pficon-error-circle-o"],
|
|
||||||
|
|
||||||
"kcFormAreaClass": ["col-sm-10", "col-sm-offset-1", "col-md-8", "col-md-offset-2", "col-lg-8", "col-lg-offset-2"],
|
|
||||||
"kcFormSocialAccountListClass": ["login-pf-social", "list-unstyled", "login-pf-social-all"],
|
|
||||||
"kcFormSocialAccountDoubleListClass": ["login-pf-social-double-col"],
|
|
||||||
"kcFormSocialAccountListLinkClass": ["login-pf-social-link"],
|
|
||||||
"kcWebAuthnKeyIcon": ["pficon", "pficon-key"],
|
|
||||||
|
|
||||||
"kcFormClass": ["form-horizontal"],
|
|
||||||
"kcFormGroupErrorClass": ["has-error"],
|
|
||||||
"kcLabelClass": ["control-label"],
|
|
||||||
"kcInputClass": ["form-control"],
|
|
||||||
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
|
|
||||||
"kcInputWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
|
||||||
"kcFormOptionsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
|
||||||
"kcFormButtonsClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
|
|
||||||
"kcFormSettingClass": ["login-pf-settings"],
|
|
||||||
"kcTextareaClass": ["form-control"],
|
|
||||||
|
|
||||||
"kcInfoAreaClass": ["col-xs-12", "col-sm-4", "col-md-4", "col-lg-5", "details"],
|
|
||||||
|
|
||||||
// user-profile grouping
|
|
||||||
"kcFormGroupHeader": ["pf-c-form__group"],
|
|
||||||
|
|
||||||
// css classes for form buttons main class used for all buttons
|
|
||||||
"kcButtonClass": ["btn"],
|
|
||||||
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
|
|
||||||
"kcButtonPrimaryClass": ["btn-primary"],
|
|
||||||
"kcButtonDefaultClass": ["btn-default"],
|
|
||||||
// classes defining size of the button
|
|
||||||
"kcButtonLargeClass": ["btn-lg"],
|
|
||||||
"kcButtonBlockClass": ["btn-block"],
|
|
||||||
|
|
||||||
// css classes for input
|
|
||||||
"kcInputLargeClass": ["input-lg"],
|
|
||||||
|
|
||||||
// css classes for form accessability
|
|
||||||
"kcSrOnlyClass": ["sr-only"],
|
|
||||||
|
|
||||||
// css classes for select-authenticator form
|
|
||||||
"kcSelectAuthListClass": ["list-group", "list-view-pf"],
|
|
||||||
"kcSelectAuthListItemClass": ["list-group-item", "list-view-pf-stacked"],
|
|
||||||
"kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
|
|
||||||
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
|
|
||||||
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
|
|
||||||
"kcSelectAuthListItemDescriptionClass": ["list-view-pf-description"],
|
|
||||||
"kcSelectAuthListItemHeadingClass": ["list-group-item-heading"],
|
|
||||||
"kcSelectAuthListItemHelpTextClass": ["list-group-item-text"],
|
|
||||||
|
|
||||||
// css classes for the authenticators
|
|
||||||
"kcAuthenticatorDefaultClass": ["fa", "list-view-pf-icon-lg"],
|
|
||||||
"kcAuthenticatorPasswordClass": ["fa", "fa-unlock list-view-pf-icon-lg"],
|
|
||||||
"kcAuthenticatorOTPClass": ["fa", "fa-mobile", "list-view-pf-icon-lg"],
|
|
||||||
"kcAuthenticatorWebAuthnClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
|
|
||||||
"kcAuthenticatorWebAuthnPasswordlessClass": ["fa", "fa-key", "list-view-pf-icon-lg"],
|
|
||||||
|
|
||||||
//css classes for the OTP Login Form
|
|
||||||
"kcSelectOTPListClass": ["card-pf", "card-pf-view", "card-pf-view-select", "card-pf-view-single-select"],
|
|
||||||
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
|
|
||||||
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
|
|
||||||
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
|
|
||||||
"kcFormOptionsWrapperClass": [],
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
assert<typeof defaultKcProps extends KcProps ? true : false>();
|
|
||||||
|
|
||||||
/** Tu use if you don't want any default */
|
|
||||||
export const allClearKcProps = allPropertiesValuesToUndefined(defaultKcProps);
|
|
||||||
|
|
||||||
assert<typeof allClearKcProps extends KcProps ? true : false>();
|
|
@ -1,193 +0,0 @@
|
|||||||
import { useState, memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
|
||||||
import type { FormEventHandler } from "react";
|
|
||||||
|
|
||||||
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
|
|
||||||
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
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, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayInfo={social.displayInfo}
|
|
||||||
displayWide={realm.password && social.providers !== undefined}
|
|
||||||
headerNode={msg("doLogIn")}
|
|
||||||
formNode={
|
|
||||||
<div id="kc-form" className={cx(realm.password && social.providers !== undefined && props.kcContentWrapperClass)}>
|
|
||||||
<div
|
|
||||||
id="kc-form-wrapper"
|
|
||||||
className={cx(realm.password && social.providers && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}
|
|
||||||
>
|
|
||||||
{realm.password && (
|
|
||||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
{(() => {
|
|
||||||
const label = !realm.loginWithEmailAllowed
|
|
||||||
? "username"
|
|
||||||
: realm.registrationEmailAsUsername
|
|
||||||
? "email"
|
|
||||||
: "usernameOrEmail";
|
|
||||||
|
|
||||||
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
|
|
||||||
{msg(label)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
tabIndex={1}
|
|
||||||
id={autoCompleteHelper}
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
//NOTE: This is used by Google Chrome auto fill so we use it to tell
|
|
||||||
//the browser how to pre fill the form but before submit we put it back
|
|
||||||
//to username because it is what keycloak expects.
|
|
||||||
name={autoCompleteHelper}
|
|
||||||
defaultValue={login.username ?? ""}
|
|
||||||
type="text"
|
|
||||||
{...(usernameEditDisabled
|
|
||||||
? { "disabled": true }
|
|
||||||
: {
|
|
||||||
"autoFocus": true,
|
|
||||||
"autoComplete": "off",
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("password")}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
tabIndex={2}
|
|
||||||
id="password"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
|
|
||||||
<div id="kc-form-options">
|
|
||||||
{realm.rememberMe && !usernameEditDisabled && (
|
|
||||||
<div className="checkbox">
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
tabIndex={3}
|
|
||||||
id="rememberMe"
|
|
||||||
name="rememberMe"
|
|
||||||
type="checkbox"
|
|
||||||
{...(login.rememberMe
|
|
||||||
? {
|
|
||||||
"checked": true,
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
/>
|
|
||||||
{msg("rememberMe")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
|
||||||
{realm.resetPasswordAllowed && (
|
|
||||||
<span>
|
|
||||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>
|
|
||||||
{msg("doForgotPassword")}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id-hidden-input"
|
|
||||||
name="credentialId"
|
|
||||||
{...(auth?.selectedCredential !== undefined
|
|
||||||
? {
|
|
||||||
"value": auth.selectedCredential,
|
|
||||||
}
|
|
||||||
: {})}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
tabIndex={4}
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonPrimaryClass,
|
|
||||||
props.kcButtonBlockClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
name="login"
|
|
||||||
id="kc-login"
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doLogIn")}
|
|
||||||
disabled={isLoginButtonDisabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{realm.password && social.providers !== undefined && (
|
|
||||||
<div id="kc-social-providers" className={cx(props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass)}>
|
|
||||||
<ul
|
|
||||||
className={cx(
|
|
||||||
props.kcFormSocialAccountListClass,
|
|
||||||
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{social.providers.map(p => (
|
|
||||||
<li key={p.providerId} className={cx(props.kcFormSocialAccountListLinkClass)}>
|
|
||||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
|
|
||||||
<span>{p.displayName}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
infoNode={
|
|
||||||
realm.password &&
|
|
||||||
realm.registrationAllowed &&
|
|
||||||
!registrationDisabled && (
|
|
||||||
<div id="kc-registration">
|
|
||||||
<span>
|
|
||||||
{msg("noAccount")}
|
|
||||||
<a tabIndex={6} href={url.registrationUrl}>
|
|
||||||
{msg("doRegister")}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,183 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginConfigTotp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginConfigTotp } & KcProps) => {
|
|
||||||
const { url, isAppInitiatedAction, totp, mode, messagesPerField } = kcContext;
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
|
|
||||||
HmacSHA1: "SHA1",
|
|
||||||
HmacSHA256: "SHA256",
|
|
||||||
HmacSHA512: "SHA512",
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("loginTotpTitle")}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<ol id="kc-totp-settings">
|
|
||||||
<li>
|
|
||||||
<p>{msg("loginTotpStep1")}</p>
|
|
||||||
|
|
||||||
<ul id="kc-totp-supported-apps">
|
|
||||||
{totp.policy.supportedApplications.map(app => (
|
|
||||||
<li>{app}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{mode && mode == "manual" ? (
|
|
||||||
<>
|
|
||||||
<li>
|
|
||||||
<p>{msg("loginTotpManualStep2")}</p>
|
|
||||||
<p>
|
|
||||||
<span id="kc-totp-secret-key">{totp.totpSecretEncoded}</span>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<a href={totp.qrUrl} id="mode-barcode">
|
|
||||||
{msg("loginTotpScanBarcode")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<p>{msg("loginTotpManualStep3")}</p>
|
|
||||||
<p>
|
|
||||||
<ul>
|
|
||||||
<li id="kc-totp-type">
|
|
||||||
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
|
|
||||||
</li>
|
|
||||||
<li id="kc-totp-algorithm">
|
|
||||||
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm}
|
|
||||||
</li>
|
|
||||||
<li id="kc-totp-digits">
|
|
||||||
{msg("loginTotpDigits")}: {totp.policy.digits}
|
|
||||||
</li>
|
|
||||||
{totp.policy.type === "totp" ? (
|
|
||||||
<li id="kc-totp-period">
|
|
||||||
{msg("loginTotpInterval")}: {totp.policy.period}
|
|
||||||
</li>
|
|
||||||
) : (
|
|
||||||
<li id="kc-totp-counter">
|
|
||||||
{msg("loginTotpCounter")}: {totp.policy.initialCounter}
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<li>
|
|
||||||
<p>{msg("loginTotpStep2")}</p>
|
|
||||||
<img id="kc-totp-secret-qr-code" src={`data:image/png;base64, ${totp.totpSecretQrCode}`} alt="Figure: Barcode" />
|
|
||||||
<br />
|
|
||||||
<p>
|
|
||||||
<a href={totp.manualUrl} id="mode-manual">
|
|
||||||
{msg("loginTotpUnableToScan")}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
<li>
|
|
||||||
<p>{msg("loginTotpStep3")}</p>
|
|
||||||
<p>{msg("loginTotpStep3DeviceName")}</p>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<form action={url.loginAction} className={cx(props.kcFormClass)} id="kc-totp-settings-form" method="post">
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<label htmlFor="totp" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("authenticatorCode")}
|
|
||||||
</label>{" "}
|
|
||||||
<span className="required">*</span>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="totp"
|
|
||||||
name="totp"
|
|
||||||
autoComplete="off"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
aria-invalid={messagesPerField.existsError("totp")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{messagesPerField.existsError("totp") && (
|
|
||||||
<span id="input-error-otp-code" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
|
||||||
{messagesPerField.get("totp")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<input type="hidden" id="totpSecret" name="totpSecret" value={totp.totpSecret} />
|
|
||||||
{mode && <input type="hidden" id="mode" value={mode} />}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<label htmlFor="userLabel" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("loginTotpDeviceName")}
|
|
||||||
</label>{" "}
|
|
||||||
{totp.otpCredentials.length >= 1 && <span className="required">*</span>}
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="userLabel"
|
|
||||||
name="userLabel"
|
|
||||||
autoComplete="off"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
aria-invalid={messagesPerField.existsError("userLabel")}
|
|
||||||
/>
|
|
||||||
{messagesPerField.existsError("userLabel") && (
|
|
||||||
<span id="input-error-otp-label" className={cx(props.kcInputErrorMessageClass)} aria-live="polite">
|
|
||||||
{messagesPerField.get("userLabel")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isAppInitiatedAction ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
|
||||||
id="saveTOTPBtn"
|
|
||||||
value={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonDefaultClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
id="cancelTOTPBtn"
|
|
||||||
name="cancel-aia"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
${msg("doCancel")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
|
||||||
id="saveTOTPBtn"
|
|
||||||
value={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,46 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginIdpLinkConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm } & KcProps) => {
|
|
||||||
const { url, idpAlias } = kcContext;
|
|
||||||
|
|
||||||
const { msg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("confirmLinkIdpTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-register-form" action={url.loginAction} method="post">
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
name="submitAction"
|
|
||||||
id="updateProfile"
|
|
||||||
value="updateProfile"
|
|
||||||
>
|
|
||||||
{msg("confirmLinkIdpReviewProfile")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
name="submitAction"
|
|
||||||
id="linkAccount"
|
|
||||||
value="linkAccount"
|
|
||||||
>
|
|
||||||
{msg("confirmLinkIdpContinue", idpAlias)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,32 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const LoginIdpLinkEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginIdpLinkEmail } & KcProps) => {
|
|
||||||
const { url, realm, brokerContext, idpAlias } = kcContext;
|
|
||||||
|
|
||||||
const { msg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("emailLinkIdpTitle", idpAlias)}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<p id="instruction1" className="instruction">
|
|
||||||
{msg("emailLinkIdp1", idpAlias, brokerContext.username, realm.displayName)}
|
|
||||||
</p>
|
|
||||||
<p id="instruction2" className="instruction">
|
|
||||||
{msg("emailLinkIdp2")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp3")}
|
|
||||||
</p>
|
|
||||||
<p id="instruction3" className="instruction">
|
|
||||||
{msg("emailLinkIdp4")} <a href={url.loginAction}>{msg("doClickHere")}</a> {msg("emailLinkIdp5")}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,111 +0,0 @@
|
|||||||
import { useEffect, memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { headInsert } from "../tools/headInsert";
|
|
||||||
import { pathJoin } from "../tools/pathJoin";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginOtp = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginOtp } & KcProps) => {
|
|
||||||
const { otpLogin, url } = kcContext;
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isCleanedUp = false;
|
|
||||||
|
|
||||||
headInsert({
|
|
||||||
"type": "javascript",
|
|
||||||
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js"),
|
|
||||||
}).then(() => {
|
|
||||||
if (isCleanedUp) return;
|
|
||||||
|
|
||||||
evaluateInlineScript();
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isCleanedUp = true;
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("doLogIn")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-otp-login-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
|
||||||
{otpLogin.userOtpCredentials.length > 1 && (
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
{otpLogin.userOtpCredentials.map(otpCredential => (
|
|
||||||
<div key={otpCredential.id} className={cx(props.kcSelectOTPListClass)}>
|
|
||||||
<input type="hidden" value="${otpCredential.id}" />
|
|
||||||
<div className={cx(props.kcSelectOTPListItemClass)}>
|
|
||||||
<span className={cx(props.kcAuthenticatorOtpCircleClass)} />
|
|
||||||
<h2 className={cx(props.kcSelectOTPItemHeadingClass)}>{otpCredential.userLabel}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="otp" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("loginOtpOneTime")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input id="otp" name="otp" autoComplete="off" type="text" className={cx(props.kcInputClass)} autoFocus />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
name="login"
|
|
||||||
id="kc-login"
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doLogIn")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
declare const $: any;
|
|
||||||
|
|
||||||
function evaluateInlineScript() {
|
|
||||||
$(document).ready(function () {
|
|
||||||
// Card Single Select
|
|
||||||
$(".card-pf-view-single-select").click(function (this: any) {
|
|
||||||
if ($(this).hasClass("active")) {
|
|
||||||
$(this).removeClass("active");
|
|
||||||
$(this).children().removeAttr("name");
|
|
||||||
} else {
|
|
||||||
$(".card-pf-view-single-select").removeClass("active");
|
|
||||||
$(".card-pf-view-single-select").children().removeAttr("name");
|
|
||||||
$(this).addClass("active");
|
|
||||||
$(this).children().attr("name", "selectedCredentialId");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var defaultCred = $(".card-pf-view-single-select")[0];
|
|
||||||
if (defaultCred) {
|
|
||||||
defaultCred.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const LoginPageExpired = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginPageExpired } & KcProps) => {
|
|
||||||
const { url } = kcContext;
|
|
||||||
|
|
||||||
const { msg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("pageExpiredTitle")}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<p id="instruction1" className="instruction">
|
|
||||||
{msg("pageExpiredMsg1")}
|
|
||||||
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
|
||||||
{msg("doClickHere")}
|
|
||||||
</a>{" "}
|
|
||||||
.<br />
|
|
||||||
{msg("pageExpiredMsg2")}{" "}
|
|
||||||
<a id="loginContinueLink" href={url.loginAction}>
|
|
||||||
{msg("doClickHere")}
|
|
||||||
</a>{" "}
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,66 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginResetPassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginResetPassword } & KcProps) => {
|
|
||||||
const { url, realm, auth } = kcContext;
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("emailForgotTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-reset-password-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
|
||||||
{!realm.loginWithEmailAllowed
|
|
||||||
? msg("username")
|
|
||||||
: !realm.registrationEmailAsUsername
|
|
||||||
? msg("usernameOrEmail")
|
|
||||||
: msg("email")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
autoFocus
|
|
||||||
defaultValue={auth !== undefined && auth.showUsername ? auth.attemptedUsername : undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcFormGroupClass, props.kcFormSettingClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
|
||||||
<span>
|
|
||||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
infoNode={msg("emailInstruction")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,117 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginUpdatePassword = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdatePassword } & KcProps) => {
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { url, messagesPerField, isAppInitiatedAction, username } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("updatePasswordTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-passwd-update-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
value={username}
|
|
||||||
readOnly={true}
|
|
||||||
autoComplete="username"
|
|
||||||
style={{ display: "none" }}
|
|
||||||
/>
|
|
||||||
<input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} />
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password-new" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("passwordNew")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password-new"
|
|
||||||
name="password-new"
|
|
||||||
autoFocus
|
|
||||||
autoComplete="new-password"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("passwordConfirm")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password-confirm"
|
|
||||||
name="password-confirm"
|
|
||||||
autoComplete="new-password"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
|
||||||
{isAppInitiatedAction && (
|
|
||||||
<div className="checkbox">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked />
|
|
||||||
{msgStr("logoutOtherSessions")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
{isAppInitiatedAction ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
defaultValue={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
name="cancel-aia"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
{msg("doCancel")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonPrimaryClass,
|
|
||||||
props.kcButtonBlockClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
type="submit"
|
|
||||||
defaultValue={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,120 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const LoginUpdateProfile = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginUpdateProfile } & KcProps) => {
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { url, user, messagesPerField, isAppInitiatedAction } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("loginProfileTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-update-profile-form" className={cx(props.kcFormClass)} action={url.loginAction} method="post">
|
|
||||||
{user.editUsernameAllowed && (
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("username")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
name="username"
|
|
||||||
defaultValue={user.username ?? ""}
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="email" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("email")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input type="text" id="email" name="email" defaultValue={user.email ?? ""} className={cx(props.kcInputClass)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("firstName")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="firstName"
|
|
||||||
name="firstName"
|
|
||||||
defaultValue={user.firstName ?? ""}
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("lastName")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input type="text" id="lastName" name="lastName" defaultValue={user.lastName ?? ""} className={cx(props.kcInputClass)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
{isAppInitiatedAction ? (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
defaultValue={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
name="cancel-aia"
|
|
||||||
value="true"
|
|
||||||
>
|
|
||||||
{msg("doCancel")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<input
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonPrimaryClass,
|
|
||||||
props.kcButtonBlockClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
type="submit"
|
|
||||||
defaultValue={msgStr("doSubmit")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,32 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const LoginVerifyEmail = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LoginVerifyEmail } & KcProps) => {
|
|
||||||
const { msg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { url, user } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("emailVerifyTitle")}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
|
|
||||||
<p className="instruction">
|
|
||||||
{msg("emailVerifyInstruction2")}
|
|
||||||
<br />
|
|
||||||
<a href={url.loginAction}>{msg("doClickHere")}</a>
|
|
||||||
|
|
||||||
{msg("emailVerifyInstruction3")}
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,61 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
|
|
||||||
export const LogoutConfirm = memo(({ kcContext, ...props }: { kcContext: KcContextBase.LogoutConfirm } & KcProps) => {
|
|
||||||
const { url, client, logoutConfirm } = kcContext;
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("logoutConfirmTitle")}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<div id="kc-logout-confirm" className="content-area">
|
|
||||||
<p className="instruction">{msg("logoutConfirmHeader")}</p>
|
|
||||||
<form className="form-actions" action={url.logoutConfirmAction} method="POST">
|
|
||||||
<input type="hidden" name="session_code" value={logoutConfirm.code} />
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options">
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}></div>
|
|
||||||
</div>
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormGroupClass)}>
|
|
||||||
<input
|
|
||||||
tabIndex={4}
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonPrimaryClass,
|
|
||||||
props.kcButtonBlockClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
name="confirmLogout"
|
|
||||||
id="kc-logout"
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doLogout")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<div id="kc-info-message">
|
|
||||||
{!logoutConfirm.skipLink && client.baseUrl && (
|
|
||||||
<p>
|
|
||||||
<a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,156 +0,0 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
|
|
||||||
export const Register = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Register } & KcProps) => {
|
|
||||||
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("registerTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="firstName" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("firstName")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="firstName"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="firstName"
|
|
||||||
defaultValue={register.formData.firstName ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="lastName" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("lastName")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="lastName"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="lastName"
|
|
||||||
defaultValue={register.formData.lastName ?? ""}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="email" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("email")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="email"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="email"
|
|
||||||
defaultValue={register.formData.email ?? ""}
|
|
||||||
autoComplete="email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{!realm.registrationEmailAsUsername && (
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("username")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="username"
|
|
||||||
defaultValue={register.formData.username ?? ""}
|
|
||||||
autoComplete="username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{passwordRequired && (
|
|
||||||
<>
|
|
||||||
<div className={cx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("password")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
className={cx(props.kcInputClass)}
|
|
||||||
name="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={cx(
|
|
||||||
props.kcFormGroupClass,
|
|
||||||
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass),
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={cx(props.kcLabelWrapperClass)}>
|
|
||||||
<label htmlFor="password-confirm" className={cx(props.kcLabelClass)}>
|
|
||||||
{msg("passwordConfirm")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<input type="password" id="password-confirm" className={cx(props.kcInputClass)} name="password-confirm" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{recaptchaRequired && (
|
|
||||||
<div className="form-group">
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
|
||||||
<span>
|
|
||||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doRegister")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,217 +0,0 @@
|
|||||||
import { useMemo, memo, useEffect, useState, Fragment } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
import type { ReactComponent } from "../tools/ReactComponent";
|
|
||||||
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
|
|
||||||
import { useFormValidationSlice } from "../useFormValidationSlice";
|
|
||||||
|
|
||||||
export const RegisterUserProfile = memo(({ kcContext, ...props_ }: { kcContext: KcContextBase.RegisterUserProfile } & KcProps) => {
|
|
||||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
|
|
||||||
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx, css } = useCssAndCx();
|
|
||||||
|
|
||||||
const props = useMemo(
|
|
||||||
() => ({
|
|
||||||
...props_,
|
|
||||||
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
|
|
||||||
}),
|
|
||||||
[cx, css],
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
displayMessage={messagesPerField.exists("global")}
|
|
||||||
displayRequiredFields={true}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
headerNode={msg("registerTitle")}
|
|
||||||
formNode={
|
|
||||||
<form id="kc-register-form" className={cx(props.kcFormClass)} action={url.registrationAction} method="post">
|
|
||||||
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} {...props} />
|
|
||||||
{recaptchaRequired && (
|
|
||||||
<div className="form-group">
|
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
|
||||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
|
||||||
<div id="kc-form-options" className={cx(props.kcFormOptionsClass)}>
|
|
||||||
<div className={cx(props.kcFormOptionsWrapperClass)}>
|
|
||||||
<span>
|
|
||||||
<a href={url.loginUrl}>{msg("backToLogin")}</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(props.kcFormButtonsClass)}>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doRegister")}
|
|
||||||
disabled={!isFomSubmittable}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
type UserProfileFormFieldsProps = { kcContext: KcContextBase.RegisterUserProfile } & KcProps &
|
|
||||||
Partial<Record<"BeforeField" | "AfterField", ReactComponent<{ attribute: Attribute }>>> & {
|
|
||||||
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange, ...props }: UserProfileFormFieldsProps) => {
|
|
||||||
const { cx, css } = useCssAndCx();
|
|
||||||
|
|
||||||
const { advancedMsg } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const {
|
|
||||||
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
|
|
||||||
formValidationReducer,
|
|
||||||
attributesWithPassword,
|
|
||||||
} = useFormValidationSlice({
|
|
||||||
kcContext,
|
|
||||||
});
|
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,72 +0,0 @@
|
|||||||
import { useReducer, useEffect, memo } from "react";
|
|
||||||
import { Template } from "./Template";
|
|
||||||
import type { KcProps } from "./KcProps";
|
|
||||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|
||||||
import { getMsg } from "../i18n";
|
|
||||||
import { useCssAndCx } from "tss-react";
|
|
||||||
import { kcMessages, getCurrentKcLanguageTag } from "../i18n";
|
|
||||||
import type { KcLanguageTag } from "../i18n";
|
|
||||||
|
|
||||||
/** Allow to avoid bundling the terms and download it on demand*/
|
|
||||||
export function useDownloadTerms(params: {
|
|
||||||
kcContext: KcContextBase;
|
|
||||||
downloadTermMarkdown: (params: { currentKcLanguageTag: KcLanguageTag }) => Promise<string>;
|
|
||||||
}) {
|
|
||||||
const { kcContext, downloadTermMarkdown } = params;
|
|
||||||
|
|
||||||
const [, forceUpdate] = useReducer(x => x + 1, 0);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentKcLanguageTag = getCurrentKcLanguageTag(kcContext);
|
|
||||||
|
|
||||||
downloadTermMarkdown({ currentKcLanguageTag }).then(thermMarkdown => {
|
|
||||||
kcMessages[currentKcLanguageTag].termsText = thermMarkdown;
|
|
||||||
forceUpdate();
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Terms = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Terms } & KcProps) => {
|
|
||||||
const { msg, msgStr } = getMsg(kcContext);
|
|
||||||
|
|
||||||
const { cx } = useCssAndCx();
|
|
||||||
|
|
||||||
const { url } = kcContext;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
{...{ kcContext, ...props }}
|
|
||||||
doFetchDefaultThemeResources={true}
|
|
||||||
displayMessage={false}
|
|
||||||
headerNode={msg("termsTitle")}
|
|
||||||
formNode={
|
|
||||||
<>
|
|
||||||
<div id="kc-terms-text">{msg("termsText")}</div>
|
|
||||||
<form className="form-actions" action={url.loginAction} method="POST">
|
|
||||||
<input
|
|
||||||
className={cx(
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonClass,
|
|
||||||
props.kcButtonPrimaryClass,
|
|
||||||
props.kcButtonLargeClass,
|
|
||||||
)}
|
|
||||||
name="accept"
|
|
||||||
id="kc-accept"
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doAccept")}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
className={cx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
|
|
||||||
name="cancel"
|
|
||||||
id="kc-decline"
|
|
||||||
type="submit"
|
|
||||||
value={msgStr("doDecline")}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
<div className="clearfix" />
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,108 +0,0 @@
|
|||||||
import type { KcContextBase, Attribute } from "./KcContextBase";
|
|
||||||
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
|
|
||||||
import type { DeepPartial } from "../tools/DeepPartial";
|
|
||||||
import { deepAssign } from "../tools/deepAssign";
|
|
||||||
import { id } from "tsafe/id";
|
|
||||||
import { exclude } from "tsafe/exclude";
|
|
||||||
import { assert } from "tsafe/assert";
|
|
||||||
import type { ExtendsKcContextBase } from "./getKcContextFromWindow";
|
|
||||||
import { getKcContextFromWindow } from "./getKcContextFromWindow";
|
|
||||||
import { pathJoin } from "../tools/pathJoin";
|
|
||||||
import { pathBasename } from "../tools/pathBasename";
|
|
||||||
import { resourcesCommonPath } from "./kcContextMocks/urlResourcesPath";
|
|
||||||
|
|
||||||
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
|
|
||||||
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
|
||||||
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
|
|
||||||
}): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } {
|
|
||||||
const { mockPageId, mockData } = params ?? {};
|
|
||||||
|
|
||||||
if (mockPageId !== undefined) {
|
|
||||||
//TODO maybe trow if no mock fo custom page
|
|
||||||
|
|
||||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
|
||||||
|
|
||||||
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
|
||||||
|
|
||||||
if (kcContextDefaultMock === undefined && partialKcContextCustomMock === undefined) {
|
|
||||||
console.warn(
|
|
||||||
[
|
|
||||||
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
|
|
||||||
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
|
|
||||||
`Please check the documentation of the getKcContext function`,
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const kcContext: any = {};
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (partialKcContextCustomMock !== undefined) {
|
|
||||||
deepAssign({
|
|
||||||
"target": kcContext,
|
|
||||||
"source": partialKcContextCustomMock,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
|
|
||||||
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
|
|
||||||
|
|
||||||
const { attributes } = kcContextDefaultMock.profile;
|
|
||||||
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes = [];
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName = {};
|
|
||||||
|
|
||||||
const partialAttributes = [
|
|
||||||
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? []),
|
|
||||||
].filter(exclude(undefined));
|
|
||||||
|
|
||||||
attributes.forEach(attribute => {
|
|
||||||
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
|
|
||||||
|
|
||||||
const augmentedAttribute: Attribute = {} as any;
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": augmentedAttribute,
|
|
||||||
"source": attribute,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (partialAttribute !== undefined) {
|
|
||||||
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
|
|
||||||
|
|
||||||
deepAssign({
|
|
||||||
"target": augmentedAttribute,
|
|
||||||
"source": partialAttribute,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
|
|
||||||
});
|
|
||||||
|
|
||||||
partialAttributes.forEach(partialAttribute => {
|
|
||||||
const { name } = partialAttribute;
|
|
||||||
|
|
||||||
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
|
||||||
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
|
||||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kcContext };
|
|
||||||
}
|
|
||||||
|
|
||||||
const kcContext = getKcContextFromWindow<KcContextExtended>();
|
|
||||||
|
|
||||||
if (kcContext !== undefined) {
|
|
||||||
const { url } = kcContext;
|
|
||||||
|
|
||||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
return { kcContext };
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
import type { KcContextBase } from "./KcContextBase";
|
|
||||||
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
|
||||||
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
|
|
||||||
|
|
||||||
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
|
|
||||||
? KcContextBase
|
|
||||||
: AndByDiscriminatingKey<"pageId", KcContextExtended & KcContextBase.Common, KcContextBase>;
|
|
||||||
|
|
||||||
export function getKcContextFromWindow<KcContextExtended extends { pageId: string } = never>(): ExtendsKcContextBase<KcContextExtended> | undefined {
|
|
||||||
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
export type { KcContextBase, Attribute, Validators } from "./KcContextBase";
|
|
||||||
export type { ExtendsKcContextBase } from "./getKcContextFromWindow";
|
|
||||||
export { getKcContext } from "./getKcContext";
|
|
@ -1 +0,0 @@
|
|||||||
export * from "./kcContextMocks";
|
|
@ -1,5 +0,0 @@
|
|||||||
import { pathJoin } from "../../tools/pathJoin";
|
|
||||||
|
|
||||||
export const subDirOfPublicDirBasename = "keycloak_static";
|
|
||||||
export const resourcesPath = pathJoin(subDirOfPublicDirBasename, "resources");
|
|
||||||
export const resourcesCommonPath = pathJoin(resourcesPath, "resources_common");
|
|
File diff suppressed because it is too large
Load Diff
@ -1,253 +0,0 @@
|
|||||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
|
||||||
//PLEASE DO NOT EDIT MANUALLY
|
|
||||||
|
|
||||||
/* spell-checker: disable */
|
|
||||||
export const kcMessages = {
|
|
||||||
"ca": {
|
|
||||||
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
|
|
||||||
"invalidPasswordMinLengthMessage": "Contrasenya incorrecta: longitud mínima {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
|
|
||||||
},
|
|
||||||
"de": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Ungültiges Passwort: muss mindestens {0} Zeichen beinhalten.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Kleinbuchstaben beinhalten.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Ungültiges Passwort: muss mindestens {0} Ziffern beinhalten.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Ungültiges Passwort: muss mindestens {0} Großbuchstaben beinhalten.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Ungültiges Passwort: muss mindestens {0} Sonderzeichen beinhalten.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Ungültiges Passwort: darf nicht identisch mit dem Benutzernamen sein.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
|
|
||||||
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
|
|
||||||
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
|
|
||||||
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht.",
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Invalid password: minimum length {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Invalid password: must contain at least {0} lower case characters.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Invalid password: must contain at least {0} numerical digits.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Invalid password: must contain at least {0} upper case characters.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Invalid password: must contain at least {0} special characters.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Invalid password: must not be equal to the username.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Invalid password: fails to match regex pattern(s).",
|
|
||||||
"invalidPasswordHistoryMessage": "Invalid password: must not be equal to any of last {0} passwords.",
|
|
||||||
"invalidPasswordBlacklistedMessage": "Invalid password: password is blacklisted.",
|
|
||||||
"invalidPasswordGenericMessage": "Invalid password: new password does not match password policies.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'Custom configured LDAP filter does not start with "(" or does not end with ")".',
|
|
||||||
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout must be a number",
|
|
||||||
"ldapErrorReadTimeoutNotNumber": "Read Timeout must be a number",
|
|
||||||
"ldapErrorMissingClientId": "Client ID needs to be provided in config when Realm Roles Mapping is not used.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
|
|
||||||
"Not possible to preserve group inheritance and use UID membership type together.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Can not set write only when LDAP provider mode is not WRITABLE",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "Can not set write-only and read-only together",
|
|
||||||
"ldapErrorCantEnableStartTlsAndConnectionPooling": "Can not enable both StartTLS and connection pooling.",
|
|
||||||
"ldapErrorCantEnableUnsyncedAndImportOff": "Can not disable Importing users when LDAP provider mode is UNSYNCED",
|
|
||||||
"ldapErrorMissingGroupsPathGroup": "Groups path group does not exist - please create the group on specified path first",
|
|
||||||
"clientRedirectURIsFragmentError": "Redirect URIs must not contain an URI fragment",
|
|
||||||
"clientRootURLFragmentError": "Root URL must not contain an URL fragment",
|
|
||||||
"clientRootURLIllegalSchemeError": "Root URL uses an illegal scheme",
|
|
||||||
"clientBaseURLIllegalSchemeError": "Base URL uses an illegal scheme",
|
|
||||||
"clientRedirectURIsIllegalSchemeError": "A redirect URI uses an illegal scheme",
|
|
||||||
"clientBaseURLInvalid": "Base URL is not a valid URL",
|
|
||||||
"clientRootURLInvalid": "Root URL is not a valid URL",
|
|
||||||
"clientRedirectURIsInvalid": "A redirect URI is not a valid URI",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "Client contained an invalid redirect URI.",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs must contain a valid host component.",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.",
|
|
||||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Contraseña incorrecta: longitud mínima {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras minúsculas.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Contraseña incorrecta: debe contener al menos {0} letras mayúsculas.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
|
|
||||||
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Mot de passe invalide : longueur minimale requise de {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en minuscule.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Mot de passe invalide : doit contenir au moins {0} chiffre(s).",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Mot de passe invalide : doit contenir au moins {0} lettre(s) en majuscule.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.",
|
|
||||||
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe.",
|
|
||||||
},
|
|
||||||
"it": {},
|
|
||||||
"ja": {
|
|
||||||
"invalidPasswordMinLengthMessage": "無効なパスワード: 最小{0}の長さが必要です。",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。",
|
|
||||||
"invalidPasswordMinDigitsMessage": "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の大文字を含む必要があります。",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。",
|
|
||||||
"invalidPasswordNotUsernameMessage": "無効なパスワード: ユーザー名と同じパスワードは禁止されています。",
|
|
||||||
"invalidPasswordRegexPatternMessage": "無効なパスワード: 正規表現パターンと一致しません。",
|
|
||||||
"invalidPasswordHistoryMessage": "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。",
|
|
||||||
"invalidPasswordBlacklistedMessage": "無効なパスワード: パスワードがブラックリストに含まれています。",
|
|
||||||
"invalidPasswordGenericMessage": "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。",
|
|
||||||
"ldapErrorInvalidCustomFilter": "LDAPフィルターのカスタム設定が、「(」から開始または「)」で終了となっていません。",
|
|
||||||
"ldapErrorConnectionTimeoutNotNumber": "接続タイムアウトは数字でなければなりません",
|
|
||||||
"ldapErrorReadTimeoutNotNumber": "読み取りタイムアウトは数字でなければなりません",
|
|
||||||
"ldapErrorMissingClientId": "レルムロール・マッピングを使用しない場合は、クライアントIDは設定内で提供される必要があります。",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
|
|
||||||
"グループの継承を維持することと、UIDメンバーシップ・タイプを使用することは同時にできません。",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "LDAPプロバイダー・モードがWRITABLEではない場合は、write onlyを設定することはできません。",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "write-onlyとread-onlyを一緒に設定することはできません。",
|
|
||||||
"ldapErrorCantEnableStartTlsAndConnectionPooling": "StartTLSと接続プーリングの両方を有効にできません。",
|
|
||||||
"clientRedirectURIsFragmentError": "リダイレクトURIにURIフラグメントを含めることはできません。",
|
|
||||||
"clientRootURLFragmentError": "ルートURLにURLフラグメントを含めることはできません。",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "クライアントに無効なリダイレクトURIが含まれていました。",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "クライアントのリダイレクトURIには有効なホスト・コンポーネントが含まれている必要があります。",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。",
|
|
||||||
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。",
|
|
||||||
},
|
|
||||||
"lt": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Per trumpas slaptažodis: mažiausias ilgis {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} mažąją raidę.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} skaitmenį.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} didžiąją raidę.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
|
|
||||||
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'Sukonfigūruotas LDAP filtras neprasideda "(" ir nesibaigia ")" simboliais.',
|
|
||||||
"ldapErrorMissingClientId": "Privaloma nurodyti kliento ID kai srities rolių susiejimas nėra nenaudojamas.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Grupių paveldėjimo ir UID narystės tipas kartu negali būti naudojami.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Negalima nustatyti rašymo rėžimo kuomet LDAP teikėjo rėžimas ne WRITABLE",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "Negalima nustatyti tik rašyti ir tik skaityti kartu",
|
|
||||||
"clientRedirectURIsFragmentError": "Nurodykite URI fragmentą, kurio negali būti peradresuojamuose URI adresuose",
|
|
||||||
"clientRootURLFragmentError": "Nurodykite URL fragmentą, kurio negali būti šakniniame URL adrese",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "Klientas pateikė neteisingą nukreipimo nuorodą.",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "Kliento nukreipimo nuorodos privalo būti nurodytos su serverio vardo komponentu.",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
|
|
||||||
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
|
|
||||||
},
|
|
||||||
"nl": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Ongeldig wachtwoord: de minimale lengte is {0} karakters.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} kleine letters bevatten.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Ongeldig wachtwoord: het moet minstens {0} getallen bevatten.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} hoofdletters bevatten.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Ongeldig wachtwoord: het moet minstens {0} speciale karakters bevatten.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
|
|
||||||
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
|
|
||||||
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'LDAP filter met aangepaste configuratie start niet met "(" of eindigt niet met ")".',
|
|
||||||
"ldapErrorConnectionTimeoutNotNumber": "Verbindingstimeout moet een getal zijn",
|
|
||||||
"ldapErrorReadTimeoutNotNumber": "Lees-timeout moet een getal zijn",
|
|
||||||
"ldapErrorMissingClientId": "Client ID moet ingesteld zijn als Realm Roles Mapping niet gebruikt wordt.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Kan groepsovererving niet behouden bij UID-lidmaatschapstype.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Alleen-schrijven niet mogelijk als LDAP provider mode niet WRITABLE is",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "Alleen-schrijven en alleen-lezen mogen niet tegelijk ingesteld zijn",
|
|
||||||
"clientRedirectURIsFragmentError": "Redirect URIs mogen geen URI fragment bevatten",
|
|
||||||
"clientRootURLFragmentError": "Root URL mag geen URL fragment bevatten",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "Client heeft een ongeldige redirect URI.",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "Client redirect URIs moeten een geldige host-component bevatten.",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
|
|
||||||
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI.",
|
|
||||||
},
|
|
||||||
"no": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Ugyldig passord: minimum lengde {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} små bokstaver.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Ugyldig passord: må inneholde minst {0} sifre.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Ugyldig passord: må inneholde minst {0} store bokstaver.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Ugyldig passord: må inneholde minst {0} spesialtegn.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Ugyldig passord: kan ikke være likt brukernavn.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Ugyldig passord: tilfredsstiller ikke kravene for passord-mønster.",
|
|
||||||
"invalidPasswordHistoryMessage": "Ugyldig passord: kan ikke være likt noen av de {0} foregående passordene.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'Tilpasset konfigurasjon av LDAP-filter starter ikke med "(" eller slutter ikke med ")".',
|
|
||||||
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only",
|
|
||||||
},
|
|
||||||
"pl": {},
|
|
||||||
"pt-BR": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Senha inválida: deve conter ao menos {0} caracteres.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres minúsculos.",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Senha inválida: deve conter ao menos {0} digitos numéricos.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres maiúsculos.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter ao menos {0} caracteres especiais.",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Senha inválida: falha ao passar por padrões.",
|
|
||||||
"invalidPasswordHistoryMessage": "Senha inválida: não deve ser igual às últimas {0} senhas.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'Filtro LDAP não inicia com "(" ou não termina com ")".',
|
|
||||||
"ldapErrorMissingClientId": "ID do cliente precisa ser definido na configuração quando mapeamentos de Roles do Realm não é utilizado.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType":
|
|
||||||
"Não é possível preservar herança de grupos e usar tipo de associação de UID ao mesmo tempo.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo",
|
|
||||||
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos",
|
|
||||||
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos",
|
|
||||||
},
|
|
||||||
"ru": {
|
|
||||||
"invalidPasswordMinLengthMessage": "Некорректный пароль: длина пароля должна быть не менее {0} символов(а).",
|
|
||||||
"invalidPasswordMinDigitsMessage": "Некорректный пароль: должен содержать не менее {0} цифр(ы).",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в нижнем регистре.",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} символов(а) в верхнем регистре.",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "Некорректный пароль: пароль должен содержать не менее {0} спецсимволов(а).",
|
|
||||||
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.",
|
|
||||||
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не прошел проверку по регулярному выражению.",
|
|
||||||
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролем(ями).",
|
|
||||||
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.",
|
|
||||||
"ldapErrorInvalidCustomFilter": 'Сконфигурированный пользователем фильтр LDAP не должен начинаться с "(" или заканчиваться на ")".',
|
|
||||||
"ldapErrorMissingClientId": "Client ID должен быть настроен в конфигурации, если не используется сопоставление ролей в realm.",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Не удалось унаследовать группу и использовать членство UID типа вместе.",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": 'Невозможно установить режим "только на запись", когда LDAP провайдер не в режиме WRITABLE',
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": 'Невозможно одновременно установить режимы "только на чтение" и "только на запись"',
|
|
||||||
"clientRedirectURIsFragmentError": "URI перенаправления не должен содержать фрагмент URI",
|
|
||||||
"clientRootURLFragmentError": "Корневой URL не должен содержать фрагмент URL ",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "Клиент содержит некорректный URI перенаправления.",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "URI перенаправления клиента должен содержать корректный компонент хоста.",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
|
|
||||||
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI.",
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
|
||||||
"invalidPasswordMinLengthMessage": "无效的密码:最短长度 {0}.",
|
|
||||||
"invalidPasswordMinLowerCaseCharsMessage": "无效的密码:至少包含 {0} 小写字母",
|
|
||||||
"invalidPasswordMinDigitsMessage": "无效的密码:至少包含 {0} 个数字",
|
|
||||||
"invalidPasswordMinUpperCaseCharsMessage": "无效的密码:最短长度 {0} 大写字母",
|
|
||||||
"invalidPasswordMinSpecialCharsMessage": "无效的密码:最短长度 {0} 特殊字符",
|
|
||||||
"invalidPasswordNotUsernameMessage": "无效的密码: 不可以与用户名相同",
|
|
||||||
"invalidPasswordRegexPatternMessage": "无效的密码: 无法与正则表达式匹配",
|
|
||||||
"invalidPasswordHistoryMessage": "无效的密码:不能与最后使用的 {0} 个密码相同",
|
|
||||||
"ldapErrorInvalidCustomFilter": '定制的 LDAP过滤器不是以 "(" 开头或以 ")"结尾.',
|
|
||||||
"ldapErrorConnectionTimeoutNotNumber": "Connection Timeout 必须是个数字",
|
|
||||||
"ldapErrorMissingClientId": "当域角色映射未启用时,客户端 ID 需要指定。",
|
|
||||||
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "无法在使用UID成员类型的同时维护组继承属性。",
|
|
||||||
"ldapErrorCantWriteOnlyForReadOnlyLdap": "当LDAP提供方不是可写模式时,无法设置只写",
|
|
||||||
"ldapErrorCantWriteOnlyAndReadOnly": "无法同时设置只读和只写",
|
|
||||||
"clientRedirectURIsFragmentError": "重定向URL不应包含URI片段",
|
|
||||||
"clientRootURLFragmentError": "根URL 不应包含 URL 片段",
|
|
||||||
"pairwiseMalformedClientRedirectURI": "客户端包含一个无效的重定向URL",
|
|
||||||
"pairwiseClientRedirectURIsMissingHost": "客户端重定向URL需要有一个有效的主机",
|
|
||||||
"pairwiseClientRedirectURIsMultipleHosts":
|
|
||||||
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
|
|
||||||
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
|
|
||||||
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
|
|
||||||
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* spell-checker: enable */
|
|
@ -1,853 +0,0 @@
|
|||||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
|
||||||
//PLEASE DO NOT EDIT MANUALLY
|
|
||||||
|
|
||||||
/* spell-checker: disable */
|
|
||||||
export const kcMessages = {
|
|
||||||
"ca": {
|
|
||||||
"emailVerificationSubject": "Verificació d'email",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Algú ha creat un compte de {2} amb aquesta adreça de correu electrònic. Si has estat tu, fes clic a l'enllaç següent per verificar la teva adreça de correu electrònic.\n\n{0}\n\nAquest enllaç expirarà en {1} minuts.\n\nSi tu no has creat aquest compte, simplement ignora aquest missatge.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Algú ha creat un compte de {2} amb aquesta adreça de correu electrònic. Si has estat tu, fes clic a l\'enllaç següent per verificar la teva adreça de correu electrònic.</p><p><a href="{0}">{0}</a></p><p> Aquest enllaç expirarà en {1} minuts.</p><p> Si tu no has creat aquest compte, simplement ignora aquest missatge.</p>',
|
|
||||||
"passwordResetSubject": "Reinicia contrasenya",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Algú ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l'enllaç següent per a reiniciar-les.\n\n{0}\n\nAquest enllaç expirarà en {1} minuts.\n\nSi no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzarà cap canvi.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Algú ha demanat de canviar les credencials del teu compte de {2}. Si has estat tu, fes clic a l\'enllaç següent per a reiniciar-les.</p><p><a href="{0}">{0}</a></p><p>Aquest enllaç expirarà en {1} minuts.</p><p>Si no vols reiniciar les teves credencials, simplement ignora aquest missatge i no es realitzarà cap canvi.</p>',
|
|
||||||
"executeActionsSubject": "Actualitza el teu compte",
|
|
||||||
"executeActionsBody":
|
|
||||||
"L'administrador ha sol·licitat que actualitzis el teu compte de {2}. Fes clic a l'enllaç inferior per iniciar aquest procés.\n\n{0}\n\nAquest enllaç expirarà en {1} minutes.\n\nSi no estàs al tant que l'administrador hagi sol·licitat això, simplement ignora aquest missatge i no es realitzarà cap canvi.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
"<p>L'administrador ha sol·licitat que actualitzis el teu compte de {2}. Fes clic a l'enllaç inferior per iniciar aquest procés.</p><p><a href=\"{0}\">{0}</a></p><p>Aquest enllaç expirarà en {1} minutes.</p><p>Si no estàs al tant que l'administrador hagi sol·licitat això, simplement ignora aquest missatge i no es realitzarà cap canvi.</p>",
|
|
||||||
"eventLoginErrorSubject": "Fallada en l'inici de sessió",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"S'ha detectat un intent d'accés fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>S'ha detectat un intent d'accés fallit al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Esborrat OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP s'ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, per favor contacta amb l'administrador.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP s'ha eliminat del teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador. </ P>",
|
|
||||||
"eventUpdatePasswordSubject": "Actualització de contrasenya",
|
|
||||||
"eventUpdatePasswordBody":
|
|
||||||
"La teva contrasenya s'ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>La teva contrasenya s'ha actualitzat el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Actualització de OTP",
|
|
||||||
"eventUpdateTotpBody": "OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
|
|
||||||
},
|
|
||||||
"cs": {
|
|
||||||
"emailVerificationSubject": "Ověření e-mailu",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Někdo vytvořil účet {2} s touto e-mailovou adresou. Pokud jste to vy, klikněte na níže uvedený odkaz a ověřte svou e-mailovou adresu \n\n{0}\n\nTento odkaz vyprší za {1} minuty.\n\nPokud jste tento účet nevytvořili, tuto zprávu ignorujte.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Někdo vytvořil účet {2} s touto e-mailovou adresou. Pokud jste to vy, klikněte na níže uvedený odkaz a ověřte svou e-mailovou adresu. </p><p><a href="{0}">Odkaz na ověření e-mailové adresy</a></p><p>Platnost odkazu vyprší za {1} minut.</p><p>Pokud jste tento účet nevytvořili, tuto zprávu ignorujte.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - testovací zpráva",
|
|
||||||
"emailTestBody": "Toto je testovací zpráva",
|
|
||||||
"emailTestBodyHtml": "<p>Toto je testovací zpráva </p>",
|
|
||||||
"identityProviderLinkSubject": "Odkaz {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Někdo chce propojit váš účet "{1}" s účtem "{0}" uživatele {2}. Pokud jste to vy, klikněte na níže uvedený odkaz a propojte účty. \n\n{3}\n\nPlatnost tohoto odkazu je {5}.\n\nPokud nechcete propojit účet, tuto zprávu ignorujte. Pokud propojíte účty, budete se moci přihlásit jako {1} pomocí {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Někdo právě požádal o změnu hesla u vašeho účtu {2}. Pokud jste to vy, pro jeho změnu klikněte na odkaz níže.</p><p><a href="{0}">Odkaz na změnu hesla.</a></p><p>Platnost tohoto odkazu je {3}.</p><p>Pokud heslo změnit nechcete, tuto zprávu ignorujte a nic se nezmění.</p>',
|
|
||||||
"passwordResetSubject": "Zapomenuté heslo",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Někdo právě požádal o změnu hesla u vašeho účtu {2}. Pokud jste to vy, pro jeho změnu klikněte na odkaz níže.\n\n{0}\n\nPlatnost tohoto odkazu je {3}.\n\nPokud heslo změnit nechcete, tuto zprávu ignorujte a nic se nezmění.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p> Někdo právě požádal o změnu pověření vašeho účtu {2}. Pokud jste to vy, klikněte na odkaz níže, abyste je resetovali.</p><p><a href="{0}">Odkaz na obnovení pověření </a></p><p> Platnost tohoto odkazu vyprší během {1} minut.</p><p> Pokud nechcete obnovit vaše pověření, ignorujte tuto zprávu a nic se nezmění.</p>',
|
|
||||||
"executeActionsSubject": "Aktualizujte svůj účet",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Váš administrátor vás požádal o provedení následujících akcí u účtu {2}: {3}. Začněte kliknutím na níže uvedený odkaz.\n\n{0}\n\nPlatnost tohoto odkazu je {4}.\n\nPokud si nejste jisti, zda je tento požadavek v pořádku, ignorujte tuto zprávu.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Váš administrátor vás požádal o provedení následujících akcí u účtu {2}: {3}. Začněte kliknutím na níže uvedený odkaz.</p><p><a href="{0}">Odkaz na aktualizaci účtu.</a></p><p>Platnost tohoto odkazu je {4}.</p><p>Pokud si nejste jisti, zda je tento požadavek v pořádku, ignorujte tuto zprávu.</p>',
|
|
||||||
"eventLoginErrorSubject": "Chyba přihlášení",
|
|
||||||
"eventLoginErrorBody": "Někdo se neúspěšně pokusil přihlásit k účtu {0} z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Někdo se neúspěšně pokusil přihlásit k účtu {0} z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Odebrat TOTP",
|
|
||||||
"eventRemoveTotpBody": "V účtu {0} bylo odebráno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
|
|
||||||
"eventRemoveTotpBodyHtml": "<p>V účtu {0} bylo odebráno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Aktualizace hesla",
|
|
||||||
"eventUpdatePasswordBody": "V účtu {0} bylo změněno heslo z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>V účtu {0} bylo změněno heslo z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Aktualizace OTP",
|
|
||||||
"eventUpdateTotpBody": "V účtu {0} bylo změněno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>V účtu {0} bylo změněno nastavení OTP z {1}. Pokud jste to nebyli vy, kontaktujte administrátora.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Konfigurace OTP",
|
|
||||||
"requiredAction.terms_and_conditions": "Smluvní podmínky",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Aktualizace hesla",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Aktualizace profilu",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Ověření e-mailu",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "sekund",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.2": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.3": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.4": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minut",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuta",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.2": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.3": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.4": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "hodin",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "hodina",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.2": "hodiny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.3": "hodiny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.4": "hodiny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "dní",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "den",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.2": "dny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.3": "dny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.4": "dny",
|
|
||||||
},
|
|
||||||
"de": {
|
|
||||||
"emailVerificationSubject": "E-Mail verifizieren",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Jemand hat ein {2} Konto mit dieser E-Mail-Adresse erstellt. Falls Sie das waren, dann klicken Sie auf den Link, um die E-Mail-Adresse zu verifizieren.\n\n{0}\n\nDieser Link wird in {1} Minuten ablaufen.\n\nFalls Sie dieses Konto nicht erstellt haben, dann können sie diese Nachricht ignorieren.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Jemand hat ein {2} Konto mit dieser E-Mail-Adresse erstellt. Falls das Sie waren, klicken Sie auf den Link, um die E-Mail-Adresse zu verifizieren.</p><p><a href="{0}">Link zur Bestätigung der E-Mail-Adresse</a></p><p>Dieser Link wird in {1} Minuten ablaufen.</p><p>Falls Sie dieses Konto nicht erstellt haben, dann können sie diese Nachricht ignorieren.</p>',
|
|
||||||
"identityProviderLinkSubject": "Link {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
"Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{3}\n\n Die Gültigkeit des Links wird in {4} Minuten verfallen.\n\nSollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} über {0} ermöglicht.",
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Es wurde beantragt Ihren Account {1} mit dem Account {0} von Benutzer {2} zu verlinken. Sollten Sie dies beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{3}">Link zur Bestätigung der Kontoverknüpfung</a></p><p>Die Gültigkeit des Links wird in {4} Minuten verfallen.</p><p>Sollten Sie Ihren Account nicht verlinken wollen, ignorieren Sie diese Nachricht. Wenn Sie die Accounts verlinken wird ein Login auf {1} über {0} ermöglicht.</p>',
|
|
||||||
"passwordResetSubject": "Passwort zurücksetzen",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Es wurde eine Änderung der Anmeldeinformationen für Ihren Account {2} angefordert. Wenn Sie diese Änderung beantragt haben, klicken Sie auf den unten stehenden Link.\n\n{0}\n\nDie Gültigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie keine Änderung vollziehen wollen können Sie diese Nachricht ignorieren und an Ihrem Account wird nichts geändert.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Es wurde eine Änderung der Anmeldeinformationen für Ihren Account {2} angefordert. Wenn Sie diese Änderung beantragt haben, klicken Sie auf den unten stehenden Link.</p><p><a href="{0}">Link zum Zurücksetzen von Anmeldeinformationen</a></p><p>Die Gültigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie keine Änderung vollziehen wollen können Sie diese Nachricht ignorieren und an Ihrem Account wird nichts geändert.</p>',
|
|
||||||
"executeActionsSubject": "Aktualisieren Sie Ihr Konto",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.\n\n{0}\n\nDie Gültigkeit des Links wird in {1} Minuten verfallen.\n\nSollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unverändert.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Ihr Administrator hat Sie aufgefordert Ihren Account {2} zu aktualisieren. Klicken Sie auf den unten stehenden Link um den Prozess zu starten.</p><p><a href="{0}">Link zum Account-Update</a></p><p>Die Gültigkeit des Links wird in {1} Minuten verfallen.</p><p>Sollten Sie sich dieser Aufforderung nicht bewusst sein, ignorieren Sie diese Nachricht und Ihr Account bleibt unverändert.</p>',
|
|
||||||
"eventLoginErrorSubject": "Fehlgeschlagene Anmeldung",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Jemand hat um {0} von {1} versucht, sich mit Ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Jemand hat um {0} von {1} versucht, sich mit Ihrem Konto anzumelden. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
|
||||||
"eventRemoveTotpSubject": "OTP Entfernt",
|
|
||||||
"eventRemoveTotpBody":
|
|
||||||
"OTP wurde von Ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP wurde von Ihrem Konto am {0} von {1} entfernt. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Passwort Aktualisiert",
|
|
||||||
"eventUpdatePasswordBody": "Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
|
||||||
"eventUpdateTotpSubject": "OTP Aktualisiert",
|
|
||||||
"eventUpdateTotpBody": "OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
|
|
||||||
},
|
|
||||||
"en": {
|
|
||||||
"emailVerificationSubject": "Verify email",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn't create this account, just ignore this message.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {3}.</p><p>If you didn\'t create this account, just ignore this message.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - SMTP test message",
|
|
||||||
"emailTestBody": "This is a test message",
|
|
||||||
"emailTestBodyHtml": "<p>This is a test message</p>",
|
|
||||||
"identityProviderLinkSubject": "Link {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Someone wants to link your "{1}" account with "{0}" account of user {2} . If this was you, click the link below to link accounts\n\n{3}\n\nThis link will expire within {5}.\n\nIf you don\'t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Someone wants to link your <b>{1}</b> account with <b>{0}</b> account of user {2} . If this was you, click the link below to link accounts</p><p><a href="{3}">Link to confirm account linking</a></p><p>This link will expire within {5}.</p><p>If you don\'t want to link account, just ignore this message. If you link accounts, you will be able to login to {1} through {0}.</p>',
|
|
||||||
"passwordResetSubject": "Reset password",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Someone just requested to change your {2} account's credentials. If this was you, click on the link below to reset them.\n\n{0}\n\nThis link and code will expire within {3}.\n\nIf you don't want to reset your credentials, just ignore this message and nothing will be changed.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
"<p>Someone just requested to change your {2} account's credentials. If this was you, click on the link below to reset them.</p><p><a href=\"{0}\">Link to reset credentials</a></p><p>This link will expire within {3}.</p><p>If you don't want to reset your credentials, just ignore this message and nothing will be changed.</p>",
|
|
||||||
"executeActionsSubject": "Update Your Account",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Your administrator has just requested that you update your {2} account by performing the following action(s): {3}. Click on the link below to start this process.</p><p><a href="{0}">Link to account update</a></p><p>This link will expire within {4}.</p><p>If you are unaware that your administrator has requested this, just ignore this message and nothing will be changed.</p>',
|
|
||||||
"eventLoginErrorSubject": "Login error",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>A failed login attempt was detected to your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Remove OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.",
|
|
||||||
"eventRemoveTotpBodyHtml": "<p>OTP was removed from your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Update password",
|
|
||||||
"eventUpdatePasswordBody": "Your password was changed on {0} from {1}. If this was not you, please contact an administrator.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Your password was changed on {0} from {1}. If this was not you, please contact an administrator.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Update OTP",
|
|
||||||
"eventUpdateTotpBody": "OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>OTP was updated for your account on {0} from {1}. If this was not you, please contact an administrator.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Configure OTP",
|
|
||||||
"requiredAction.terms_and_conditions": "Terms and Conditions",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Update Password",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Update Profile",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Verify Email",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "seconds",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "second",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minutes",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minute",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "hours",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "hour",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "days",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "day",
|
|
||||||
"emailVerificationBodyCode": "Please verify your email address by entering in the following code.\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"es": {
|
|
||||||
"emailVerificationSubject": "Verificación de email",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Alguien ha creado una cuenta de {2} con esta dirección de email. Si has sido tú, haz click en el enlace siguiente para verificar tu dirección de email.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi tú no has creado esta cuenta, simplemente ignora este mensaje.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Alguien ha creado una cuenta de {2} con esta dirección de email. Si has sido tú, haz click en el enlace siguiente para verificar tu dirección de email.</p><p><a href="{0}">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si tú no has creado esta cuenta, simplemente ignora este mensaje.</p>',
|
|
||||||
"passwordResetSubject": "Reiniciar contraseña",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Alguien ha solicitado cambiar las credenciales de tu cuenta de {2}. Si has sido tú, haz clic en el enlace siguiente para reiniciarlas.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi no quieres reiniciar tus credenciales, simplemente ignora este mensaje y no se realizará ningún cambio.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Alguien ha solicitado cambiar las credenciales de tu cuenta de {2}. Si has sido tú, haz clic en el enlace siguiente para reiniciarlas.</p><p><a href="{0}">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si no quieres reiniciar tus credenciales, simplemente ignora este mensaje y no se realizará ningún cambio.</p>',
|
|
||||||
"executeActionsSubject": "Actualiza tu cuenta",
|
|
||||||
"executeActionsBody":
|
|
||||||
"El administrador ha solicitado que actualices tu cuenta de {2}. Haz clic en el enlace inferior para iniciar este proceso.\n\n{0}\n\nEste enlace expirará en {1} minutos.\n\nSi no estás al tanto de que el administrador haya solicitado esto, simplemente ignora este mensaje y no se realizará ningún cambio.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>El administrador ha solicitado que actualices tu cuenta de {2}. Haz clic en el enlace inferior para iniciar este proceso.</p><p><a href="{0}">{0}</a></p><p>Este enlace expirará en {1} minutos.</p><p>Si no estás al tanto de que el administrador haya solicitado esto, simplemente ignora este mensaje y no se realizará ningún cambio.</p>',
|
|
||||||
"eventLoginErrorSubject": "Fallo en el inicio de sesión",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Se ha detectado un intento de acceso fallido a tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Se ha detectado un intento de acceso fallido a tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Borrado OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP fue eliminado de tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP fue eliminado de tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Actualización de contraseña",
|
|
||||||
"eventUpdatePasswordBody": "Tu contraseña se ha actualizado el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Tu contraseña se ha actualizado el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Actualización de OTP",
|
|
||||||
"eventUpdateTotpBody": "OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
|
|
||||||
},
|
|
||||||
"fr": {
|
|
||||||
"emailVerificationSubject": "Vérification du courriel",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Quelqu'un vient de créer un compte \"{2}\" avec votre courriel. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous afin de vérifier votre adresse de courriel\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon, veuillez ignorer ce message.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Quelqu\'un vient de créer un compte "{2}" avec votre courriel. Si vous êtes à l\'origine de cette requête, veuillez cliquer sur le lien ci-dessous afin de vérifier votre adresse de courriel</p><p><a href="{0}">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon, veuillez ignorer ce message.</p>',
|
|
||||||
"passwordResetSubject": "Réinitialiser le mot de passe",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Quelqu'un vient de demander une réinitialisation de mot de passe pour votre compte {2}. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous pour le mettre à jour.\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSinon, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
"<p>Quelqu'un vient de demander une réinitialisation de mot de passe pour votre compte {2}. Si vous êtes à l'origine de cette requête, veuillez cliquer sur le lien ci-dessous pour le mettre à jour.</p><p><a href=\"{0}\">Lien pour réinitialiser votre mot de passe</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Sinon, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.</p>",
|
|
||||||
"executeActionsSubject": "Mettre à jour votre compte",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Votre administrateur vient de demander une mise à jour de votre compte {2}. Veuillez cliquer sur le lien ci-dessous afin de commencer le processus.\n\n{0}\n\nCe lien expire dans {1} minute(s).\n\nSi vous n'êtes pas à l'origine de cette requête, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
"<p>Votre administrateur vient de demander une mise à jour de votre compte {2}. Veuillez cliquer sur le lien ci-dessous afin de commencer le processus.</p><p><a href=\"{0}\">{0}</a></p><p>Ce lien expire dans {1} minute(s).</p><p>Si vous n'êtes pas à l'origine de cette requête, veuillez ignorer ce message ; aucun changement ne sera effectué sur votre compte.</p>",
|
|
||||||
"eventLoginErrorSubject": "Erreur de connexion",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Une tentative de connexion a été détectée sur votre compte {0} depuis {1}. Si vous n'êtes pas à l'origine de cette requête, veuillez contacter votre administrateur.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Une tentative de connexion a été détectée sur votre compte {0} depuis {1}. Si vous n'êtes pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Suppression du OTP",
|
|
||||||
"eventRemoveTotpBody":
|
|
||||||
"Le OTP a été supprimé de votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>Le OTP a été supprimé de votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Mise à jour du mot de passe",
|
|
||||||
"eventUpdatePasswordBody":
|
|
||||||
"Votre mot de passe pour votre compte {0} a été modifié depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Votre mot de passe pour votre compte {0} a été modifié depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Mise à jour du OTP",
|
|
||||||
"eventUpdateTotpBody":
|
|
||||||
"Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
|
|
||||||
},
|
|
||||||
"it": {
|
|
||||||
"emailVerificationSubject": "Verifica l'email",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Qualcuno ha creato un account {2} con questo indirizzo email. Se sei stato tu, fai clic sul link seguente per verificare il tuo indirizzo email\n\n{0}\n\nQuesto link scadrà in {3}.\n\nSe non sei stato tu a creare questo account, ignora questo messaggio.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Qualcuno ha creato un account {2} con questo indirizzo email. Se sei stato tu, fai clic sul link seguente per verificare il tuo indirizzo email</p><p><a href="{0}">{0}</a></p><p>Questo link scadrà in {3}.</p><p>Se non sei stato tu a creare questo account, ignora questo messaggio.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - messaggio di test SMTP",
|
|
||||||
"emailTestBody": "Questo è un messaggio di test",
|
|
||||||
"emailTestBodyHtml": "<p>Questo è un messaggio di test</p>",
|
|
||||||
"identityProviderLinkSubject": "Link {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Qualcuno vuole associare il tuo account "{1}" con l\'account "{0}" dell\'utente {2}. Se sei stato tu, fai clic sul link seguente per associare gli account\n\n{3}\n\nQuesto link scadrà in {5}.\n\nSe non vuoi associare l\'account, ignora questo messaggio. Se associ gli account, potrai accedere a {1} attraverso {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
"<p>Qualcuno vuole associare il tuo account <b>{1}</b> con l'account <b>{0}</b> dell'utente {2}. Se sei stato tu, fai clic sul link seguente per associare gli account</p><p><a href=\"{3}\">{3}</a></p><p>Questo link scadrà in {5}.</p><p>Se non vuoi associare l'account, ignora questo messaggio. Se associ gli account, potrai accedere a {1} attraverso {0}.</p>",
|
|
||||||
"passwordResetSubject": "Reimposta la password",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Qualcuno ha appena richiesto di cambiare le credenziali di accesso al tuo account {2}. Se sei stato tu, fai clic sul link seguente per reimpostarle.\n\n{0}\n\nQuesto link e codice scadranno in {3}.\n\nSe non vuoi reimpostare le tue credenziali di accesso, ignora questo messaggio e non verrà effettuato nessun cambio.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Qualcuno ha appena richiesto di cambiare le credenziali di accesso al tuo account {2}. Se sei stato tu, fai clic sul link seguente per reimpostarle.</p><p><a href="{0}">{0}</a></p><p>Questo link scadrà in {3}.</p><p>Se non vuoi reimpostare le tue credenziali di accesso, ignora questo messaggio e non verrà effettuato nessun cambio.</p>',
|
|
||||||
"executeActionsSubject": "Aggiorna il tuo account",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Il tuo amministratore ha appena richiesto un aggiornamento del tuo account {2} ed è necessario che tu esegua la/le seguente/i azione/i: {3}. Fai clic sul link seguente per iniziare questo processo.\n\n{0}\n\nQuesto link scadrà in {4}.\n\nSe non sei a conoscenza della richiesta del tuo amministratore, ignora questo messaggio e non verrà effettuato nessun cambio.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Il tuo amministratore ha appena richiesto un aggiornamento del tuo account {2} ed è necessario che tu esegua la/le seguente/i azione/i: {3}. Fai clic sul link seguente per iniziare questo processo.</p><p><a href="{0}">Link to account update</a></p><p>Questo link scadrà in {4}.</p><p>Se non sei a conoscenza della richiesta del tuo amministratore, ignora questo messaggio e non verrà effettuato nessun cambio.</p>',
|
|
||||||
"eventLoginErrorSubject": "Errore di accesso",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"È stato rilevato un tentativo fallito di accesso al tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>È stato rilevato un tentativo fallito di accesso al tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Rimozione OTP (password temporanea valida una volta sola)",
|
|
||||||
"eventRemoveTotpBody":
|
|
||||||
"La OTP (password temporanea valida una volta sola) è stata rimossa dal tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>La OTP (password temporanea valida una volta sola) è stata rimossa dal tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Aggiornamento password",
|
|
||||||
"eventUpdatePasswordBody": "La tua password è stata cambiata il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>La tua password è stata cambiata il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Aggiornamento OTP (password temporanea valida una volta sola)",
|
|
||||||
"eventUpdateTotpBody":
|
|
||||||
"La OTP (password temporanea valida una volta sola) è stata aggiornata per il tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>La OTP (password temporanea valida una volta sola) è stata aggiornata per il tuo account il {0} da {1}. Se non sei stato tu, per favore contatta l'amministratore.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Configurazione OTP",
|
|
||||||
"requiredAction.terms_and_conditions": "Termini e condizioni",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Aggiornamento password",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Aggiornamento profilo",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Verifica dell'indirizzo email",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "secondi",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "secondo",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minuti",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuto",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "ore",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "ora",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "giorni",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "giorno",
|
|
||||||
"emailVerificationBodyCode": "Per favore verifica il tuo indirizzo email inserendo il codice seguente.\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>Per favore verifica il tuo indirizzo email inserendo il codice seguente.</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"ja": {
|
|
||||||
"emailVerificationSubject": "Eメールの確認",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"このメールアドレスで{2}アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。\n\n{0}\n\nこのリンクは{3}だけ有効です。\n\nもしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>このメールアドレスで{2}アカウントが作成されました。以下のリンクをクリックしてメールアドレスの確認を完了してください。</p><p><a href="{0}">メールアドレスの確認</a></p><p>このリンクは{3}だけ有効です。</p><p>もしこのアカウントの作成に心当たりがない場合は、このメールを無視してください。</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - SMTPテストメッセージ",
|
|
||||||
"emailTestBody": "これはテストメッセージです",
|
|
||||||
"emailTestBodyHtml": "<p>これはテストメッセージです</p>",
|
|
||||||
"identityProviderLinkSubject": "リンク {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'あなたの"{1}"アカウントと{2}ユーザーの"{0}"アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。\n\n{3}\n\nこのリンクは{5}だけ有効です。\n\nもしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0}経由で{1}にログインすることができるようになります。',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>あなたの<b>{1}</b>アカウントと{2}ユーザーの<b>{0}</b>アカウントのリンクが要求されました。以下のリンクをクリックしてアカウントのリンクを行ってください。</p><p><a href="{3}">アカウントリンクの確認</a></p><p>このリンクは{5}だけ有効です。</p><p>もしアカウントのリンクを行わない場合は、このメッセージを無視してください。アカウントのリンクを行うことで、{0}経由で{1}にログインすることができるようになります。</p>',
|
|
||||||
"passwordResetSubject": "パスワードのリセット",
|
|
||||||
"passwordResetBody":
|
|
||||||
"あなたの{2}アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。\n\n{0}\n\nこのリンクは{3}だけ有効です。\n\nもしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>あなたの{2}アカウントのパスワードの変更が要求されています。以下のリンクをクリックしてパスワードのリセットを行ってください。</p><p><a href="{0}">パスワードのリセット</a></p><p>このリンクは{3}だけ有効です。</p><p>もしパスワードのリセットを行わない場合は、このメッセージを無視してください。何も変更されません。</p>',
|
|
||||||
"executeActionsSubject": "アカウントの更新",
|
|
||||||
"executeActionsBody":
|
|
||||||
"次のアクションを実行することにより、管理者よりあなたの{2}アカウントの更新が要求されています: {3}。以下のリンクをクリックしてこのプロセスを開始してください。\n\n{0}\n\nこのリンクは{4}だけ有効です。\n\n管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>次のアクションを実行することにより、管理者よりあなたの{2}アカウントの更新が要求されています: {3}。以下のリンクをクリックしてこのプロセスを開始してください。</p><p><a href="{0}">アカウントの更新</a></p><p>このリンクは{4}だけ有効です。</p><p>管理者からのこの変更要求についてご存知ない場合は、このメッセージを無視してください。何も変更されません。</p>',
|
|
||||||
"eventLoginErrorSubject": "ログインエラー",
|
|
||||||
"eventLoginErrorBody": "{0}に{1}からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は、管理者に連絡してください。",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>{0}に{1}からのログイン失敗があなたのアカウントで検出されました。心当たりがない場合は管理者に連絡してください。</p>",
|
|
||||||
"eventRemoveTotpSubject": "OTPの削除",
|
|
||||||
"eventRemoveTotpBody": "{0}に{1}からの操作でOTPが削除されました。心当たりがない場合は、管理者に連絡してください。",
|
|
||||||
"eventRemoveTotpBodyHtml": "<p>{0}に{1}からの操作でOTPが削除されました。心当たりがない場合は、管理者に連絡してください。</p>",
|
|
||||||
"eventUpdatePasswordSubject": "パスワードの更新",
|
|
||||||
"eventUpdatePasswordBody": "{0}に{1}からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>{0}に{1}からの操作であなたのパスワードが変更されました。心当たりがない場合は、管理者に連絡してください。</p>",
|
|
||||||
"eventUpdateTotpSubject": "OTPの更新",
|
|
||||||
"eventUpdateTotpBody": "{0}に{1}からの操作でOTPが更新されました。心当たりがない場合は、管理者に連絡してください。",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>{0}に{1}からの操作でOTPが更新されました。心当たりがない場合は、管理者に連絡してください。</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "OTPの設定",
|
|
||||||
"requiredAction.terms_and_conditions": "利用規約",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "パスワードの更新",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "プロファイルの更新",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Eメールの確認",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "秒",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "秒",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "分",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "分",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "時間",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "時間",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "日",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "日",
|
|
||||||
"emailVerificationBodyCode": "次のコードを入力してメールアドレスを確認してください。\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>次のコードを入力してメールアドレスを確認してください。</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"lt": {
|
|
||||||
"emailVerificationSubject": "El. pašto patvirtinimas",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Paskyra {2} sukurta naudojant šį el. pašto adresą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą\n\n{0}\n\nŠi nuoroda galioja {1} min.\n\nJei paskyros nekūrėte, tuomet ignuoruokite šį laišką. ",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Paskyra {2} sukurta naudojant šį el. pašto adresą. Jei tao buvote Jūs, tuomet paspauskite žemiau esančią nuorodą</p><p><a href=LT"{0}">{0}</a></p><p>Ši nuoroda galioja {1} min.</p><p>nJei paskyros nekūrėte, tuomet ignuoruokite šį laišką.</p>',
|
|
||||||
"identityProviderLinkSubject": "Sąsaja {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Kažas pageidauja susieti Jūsų "{1}" paskyrą su "{0}" {2} naudotojo paskyrą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą norėdami susieti paskyras\n\n{3}\n\nŠi nuoroda galioja {4} min.\n\nJei paskyrų susieti nenorite, tuomet ignoruokite šį laišką. Jei paskyras susiesite, tuomet prie {1} galėsiste prisijungti per {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>žas pageidauja susieti Jūsų <b>{1}</b> paskyrą su <b>{0}</b> {2} naudotojo paskyrą. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą norėdami susieti paskyras</p><p><a href=LT"{3}">{3}</a></p><p>Ši nuoroda galioja {4} min.</p><p>Jei paskyrų susieti nenorite, tuomet ignoruokite šį laišką. Jei paskyras susiesite, tuomet prie {1} galėsiste prisijungti per {0}.</p>',
|
|
||||||
"passwordResetSubject": "Slaptažodžio atkūrimas",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Kažkas pageidauja pakeisti Jūsų paskyros {2} slaptažodį. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą slaptažodžio pakeitimui.\n\n{0}\n\nŠi nuoroda ir kodas galioja {1} min.\n\nJei nepageidajate keisti slaptažodžio, tuomet ignoruokite šį laišką ir niekas nebus pakeista.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Kažkas pageidauja pakeisti Jūsų paskyros {2} slaptažodį. Jei tai buvote Jūs, tuomet paspauskite žemiau esančią nuorodą slaptažodžio pakeitimui.</p><p><a href=LT"{0}">{0}</a></p><p>Ši nuoroda ir kodas galioja {1} min.</p><p>Jei nepageidajate keisti slaptažodžio, tuomet ignoruokite šį laišką ir niekas nebus pakeista.</p>',
|
|
||||||
"executeActionsSubject": "Atnaujinkite savo paskyrą",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Sistemos administratorius pageidauja, kad Jūs atnaujintumėte savo {2} paskyrą. Paspauskite žemiau esančią nuorodą paskyros duomenų atnaujinimui.\n\n{0}\n\nŠi nuoroda galioja {1} min.\n\nJei Jūs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite šį laišką ir niekas nebus pakeista.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Sistemos administratorius pageidauja, kad Jūs atnaujintumėte savo {2} paskyrą. Paspauskite žemiau esančią nuorodą paskyros duomenų atnaujinimui.</p><p><a href=LT"{0}">{0}</a></p><p>Ši nuoroda galioja {1} min.</p><p>Jei Jūs neasate tikri, kad tai administratoriaus pageidavimas, tuomet ignoruokite šį laišką ir niekas nebus pakeista.</p>',
|
|
||||||
"eventLoginErrorSubject": "Nesėkmingas bandymas prisijungti prie jūsų paskyros",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Bandymas prisijungti prie jūsų paskyros {0} iš {1} nesėkmingas. Jei tai nebuvote jūs, tuomet susisiekite su administratoriumi",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Bandymas prisijungti prie jūsų paskyros {0} iš {1} nesėkmingas. Jei tai nebuvote jūs, tuomet susisiekite su administratoriumi</p>",
|
|
||||||
"eventRemoveTotpSubject": "OTP pašalinimas",
|
|
||||||
"eventRemoveTotpBody":
|
|
||||||
"Kažkas pageidauja atsieti TOPT Jūsų {1} paskyroje su {0}. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>Kažkas pageidauja atsieti TOPT Jūsų <b>{1}</b> paskyroje su <b>{0}</b>. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Slaptažodžio atnaujinimas",
|
|
||||||
"eventUpdatePasswordBody": "{1} paskyroje {0} pakeisas jūsų slaptažodis. Jei Jūs nekeitėte, tuomet susisiekite su administratoriumi",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>{1} paskyroje {0} pakeisas jūsų slaptažodis. Jei Jūs nekeitėte, tuomet susisiekite su administratoriumi</p>",
|
|
||||||
"eventUpdateTotpSubject": "OTP atnaujinimas",
|
|
||||||
"eventUpdateTotpBody": "OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>",
|
|
||||||
},
|
|
||||||
"nl": {
|
|
||||||
"emailVerificationSubject": "Bevestig e-mailadres",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Iemand heeft een {2} account aangemaakt met dit e-mailadres. Als u dit was, klikt u op de onderstaande koppeling om uw e-mailadres te bevestigen \n\n{0}\n\nDeze koppeling zal binnen {3} vervallen.\n\nU kunt dit bericht negeren indien u dit account niet heeft aangemaakt.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Iemand heeft een {2} account aangemaakt met dit e-mailadres. Als u dit was, klikt u op de onderstaande koppeling om uw e-mailadres te bevestigen</p><p><a href="{0}">Koppeling naar e-mailadres bevestiging</a></p><p>Deze koppeling zal binnen {3} vervallen.</p<p>U kunt dit bericht negeren indien u dit account niet heeft aangemaakt.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - SMTP testbericht",
|
|
||||||
"emailTestBody": "Dit is een testbericht",
|
|
||||||
"emailTestBodyHtml": "<p>Dit is een testbericht</p>",
|
|
||||||
"identityProviderLinkSubject": "Koppel {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Iemand wil uw "{1}" account koppelen met "{0}" account van gebruiker {2}. Als u dit was, klik dan op de onderstaande link om de accounts te koppelen\n\n{3}\n\nDeze link zal over {5} vervallen.\n\nAls u de accounts niet wilt koppelen, negeer dan dit bericht. Als u accounts koppelt, dan kunt u bij {1} inloggen via {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Iemand wil uw "{1}" account koppelen met "{0}" account van gebruiker {2}. Als u dit was, klik dan op de onderstaande link om de accounts te koppelen</p><p><a href="{3}">Link om accounts te koppelen</a></p><p>Deze link zal over {5} vervallen.</p><p>Als u de accounts niet wilt koppelen, negeer dan dit bericht. Als u accounts koppelt, dan kunt u bij {1} inloggen via {0}.</p>',
|
|
||||||
"passwordResetSubject": "Wijzig wachtwoord",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Iemand verzocht de aanmeldgegevens van uw {2} account te wijzigen. Als u dit was, klik dan op de onderstaande koppeling om ze te wijzigen.\n\n{0}\n\nDe link en de code zullen binnen {3} vervallen.\n\nAls u uw aanmeldgegevens niet wilt wijzigen, negeer dan dit bericht en er zal niets gewijzigd worden.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Iemand verzocht de aanmeldgegevens van uw {2} account te wijzigen. Als u dit was, klik dan op de onderstaande koppeling om ze te wijzigen.</p><p><a href="{0}">Wijzig aanmeldgegevens</a></p><p>De link en de code zullen binnen {3} vervallen.</p><p>Als u uw aanmeldgegevens niet wilt wijzigen, negeer dan dit bericht en er zal niets gewijzigd worden.</p>',
|
|
||||||
"executeActionsSubject": "Wijzig uw account",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Uw beheerder heeft u verzocht uw {2} account te wijzigen. Klik op de onderstaande koppeling om dit proces te starten. \n\n{0}\n\nDeze link zal over {4} vervallen. \n\nAls u niet over dit verzoek op de hoogte was, negeer dan dit bericht om uw account ongewijzigd te laten.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Uw beheerder heeft u verzocht uw {2} account te wijzigen. Klik op de onderstaande koppeling om dit proces te starten.</p><p><a href="{0}">Link naar account wijziging</a></p><p>Deze link zal over {4} vervallen.</p><p>Als u niet over dit verzoek op de hoogte was, negeer dan dit bericht om uw account ongewijzigd te laten.</p>',
|
|
||||||
"eventLoginErrorSubject": "Inlogfout",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Er is een foutieve inlogpoging gedetecteerd op uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met de beheerder.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Er is een foutieve inlogpoging gedetecteerd op uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met de beheerder.</p>",
|
|
||||||
"eventRemoveTotpSubject": "OTP verwijderd",
|
|
||||||
"eventRemoveTotpBody": "OTP is verwijderd van uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP is verwijderd van uw account om {0} vanuit {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Wachtwoord gewijzigd",
|
|
||||||
"eventUpdatePasswordBody": "Uw wachtwoord is gewijzigd om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Uw wachtwoord is gewijzigd om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
|
|
||||||
"eventUpdateTotpSubject": "OTP gewijzigd",
|
|
||||||
"eventUpdateTotpBody": "OTP is gewijzigd voor uw account om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP is gewijzigd voor uw account om {0} door {1}. Als u dit niet was, neem dan contact op met uw beheerder.</p>",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "seconden",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "seconde",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minuten",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuut",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "uur",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "uur",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "dagen",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "dag",
|
|
||||||
},
|
|
||||||
"no": {
|
|
||||||
"emailVerificationSubject": "Bekreft e-postadresse",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Noen har opprettet en {2} konto med denne e-postadressen. Hvis dette var deg, klikk på lenken nedenfor for å bekrefte e-postadressen din\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke opprettet denne kontoen, vennligst ignorer denne meldingen.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Noen har opprettet en {2} konto med denne e-postadressen. Hvis dette var deg, klikk på lenken nedenfor for å bekrefte e-postadressen din</p><p><a href="{0}">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke opprettet denne kontoen, vennligst ignorer denne meldingen.</p>',
|
|
||||||
"identityProviderLinkSubject": "Lenke {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
"Noen vil koble din <b>{1}</b> konto med <b>{0}</b> konto til bruker {2}. Hvis dette var deg, klikk på lenken nedenfor for å koble kontoene\n\n{3}\n\nDenne lenken vil utløpe om {4} minutter\n\nHvis du ikke vil koble kontoene, vennligst ignorer denne meldingen. Hvis du kobler kontoene sammen vil du kunne logge inn til {1} gjennom {0}.",
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Noen vil koble din <b>{1}</b> konto med <b>{0}</b> konto til bruker {2}. Hvis dette var deg, klikk på lenken nedenfor for å koble kontoene.</p><p><a href="{3}">{3}</a></p><p>Denne lenken vil utløpe om {4} minutter.</p><p>Hvis du ikke vil koble kontoene, vennligst ignorer denne meldingen. Hvis du kobler kontoene sammen vil du kunne logge inn til {1} gjennom {0}.</p>',
|
|
||||||
"passwordResetSubject": "Tilbakestill passord",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Noen har bedt om å endre innloggingsdetaljene til din konto {2}. Hvis dette var deg, klikk på lenken nedenfor for å tilbakestille dem.\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke vil tilbakestille din innloggingsdata, vennligst ignorer denne meldingen og ingenting vil bli endret.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Noen har bedt om å endre innloggingsdetaljene til din konto {2}. Hvis dette var deg, klikk på lenken nedenfor for å tilbakestille dem.</p><p><a href="{0}">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke vil tilbakestille din innloggingsdata, vennligst ignorer denne meldingen og ingenting vil bli endret.</p>',
|
|
||||||
"executeActionsSubject": "Oppdater kontoen din",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Administrator har anmodet at du oppdaterer din {2} konto. Klikk på lenken nedenfor for å starte denne prosessen\n\n{0}\n\nDenne lenken vil utløpe om {1} minutter.\n\nHvis du ikke var klar over at administrator har bedt om dette, vennligst ignorer denne meldingen og ingenting vil bli endret.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Administrator har anmodet at du oppdaterer din {2} konto. Klikk på linken nedenfor for å starte denne prosessen.</p><p><a href="{0}">{0}</a></p><p>Denne lenken vil utløpe om {1} minutter.</p><p>Hvis du ikke var klar over at administrator har bedt om dette, ignorer denne meldingen og ingenting vil bli endret. </p>',
|
|
||||||
"eventLoginErrorSubject": "Innlogging feilet",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Et mislykket innloggingsforsøk ble oppdaget på din konto på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Et mislykket innloggingsforsøk ble oppdaget på din konto på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Fjern engangskode",
|
|
||||||
"eventRemoveTotpBody": "Engangskode ble fjernet fra kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>Engangskode ble fjernet fra kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Oppdater passord",
|
|
||||||
"eventUpdatePasswordBody": "Ditt passord ble endret i {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Ditt passord ble endret i {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>",
|
|
||||||
"eventUpdateTotpSubject": "Oppdater engangskode",
|
|
||||||
"eventUpdateTotpBody": "Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>",
|
|
||||||
},
|
|
||||||
"pl": {
|
|
||||||
"emailVerificationSubject": "Zweryfikuj email",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Ktoś utworzył już konto {2} z tym adresem e-mail. Jeśli to Ty, kliknij poniższy link, aby zweryfikować swój adres e-mail \n\n{0}\n\nLink ten wygaśnie w ciągu {3}.\n\nJeśli nie utworzyłeś tego konta, po prostu zignoruj tę wiadomość.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Ktoś utworzył już konto {2} z tym adresem e-mail. Jeśli to Ty, kliknij <a href="{0}">ten link</a> aby zweryfikować swój adres e-mail</p><p>Link ten wygaśnie w ciągu {3}</p><p>Jeśli nie utworzyłeś tego konta, po prostu zignoruj tę wiadomość.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - wiadomość testowa SMTP",
|
|
||||||
"emailTestBody": "To jest wiadomość testowa",
|
|
||||||
"emailTestBodyHtml": "<p>To jest wiadomość testowa</p>",
|
|
||||||
"identityProviderLinkSubject": "Link {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Ktoś chce połączyć Twoje konto "{1}" z kontem "{0}" użytkownika {2}. Jeśli to Ty, kliknij poniższy link by połączyć konta\n\n{3}\n\nTen link wygaśnie w ciągu {5}.\n\nJeśli nie chcesz połączyć konta to zignoruj tę wiadomość. Jeśli połączysz konta, będziesz mógł się zalogować na {1} przez {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Ktoś chce połączyć Twoje konto <b>{1}</b> z kontem <b>{0}</b> użytkownika {2}. Jeśli to Ty, kliknij <a href="{3}">ten link</a> by połączyć konta.</p><p>Ten link wygaśnie w ciągu {5}.</p><p>Jeśli nie chcesz połączyć konta to zignoruj tę wiadomość. Jeśli połączysz konta, będziesz mógł się zalogować na {1} przez {0}.</p>',
|
|
||||||
"passwordResetSubject": "Zresetuj hasło",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Ktoś właśnie poprosił o zmianę danych logowania Twojego konta {2}. Jeśli to Ty, kliknij poniższy link, aby je zresetować.\n\n{0}\n\nTen link i kod stracą ważność w ciągu {3}.\n\nJeśli nie chcesz zresetować swoich danych logowania, po prostu zignoruj tę wiadomość i nic się nie zmieni.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Ktoś właśnie poprosił o zmianę poświadczeń Twojego konta {2}. Jeśli to Ty, kliknij poniższy link, aby je zresetować.</p><p><a href="{0}">Link do resetowania poświadczeń</a></p><p>Ten link wygaśnie w ciągu {3}.</p><p>Jeśli nie chcesz resetować swoich poświadczeń, po prostu zignoruj tę wiadomość i nic się nie zmieni.</p>',
|
|
||||||
"executeActionsSubject": "Zaktualizuj swoje konto",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Administrator właśnie zażądał aktualizacji konta {2} poprzez wykonanie następujących działań: {3}. Kliknij poniższy link, aby rozpocząć ten proces.\n\n{0}\n\nTen link wygaśnie w ciągu {4}.\n\nJeśli nie masz pewności, że administrator tego zażądał, po prostu zignoruj tę wiadomość i nic się nie zmieni.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Administrator właśnie zażądał aktualizacji konta {2} poprzez wykonanie następujących działań: {3}. Kliknij <a href="{0}">ten link</a>, aby rozpocząć proces.</p><p>Link ten wygaśnie w ciągu {4}.</p><p>Jeśli nie masz pewności, że administrator tego zażądał, po prostu zignoruj tę wiadomość i nic się nie zmieni.</p>',
|
|
||||||
"eventLoginErrorSubject": "Błąd logowania",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Nieudana próba logowania została wykryta na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Nieudana próba logowania została wykryta na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Usuń hasło jednorazowe (OTP)",
|
|
||||||
"eventRemoveTotpBody":
|
|
||||||
"Hasło jednorazowe (OTP) zostało usunięte z Twojego konta w {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>Hasło jednorazowe (OTP) zostało usunięte z Twojego konta w {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Aktualizuj hasło",
|
|
||||||
"eventUpdatePasswordBody": "Twoje hasło zostało zmienione {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Twoje hasło zostało zmienione {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Aktualizuj hasło jednorazowe (OTP)",
|
|
||||||
"eventUpdateTotpBody":
|
|
||||||
"Hasło jednorazowe (OTP) zostało zaktualizowane na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>Hasło jednorazowe (OTP) zostało zaktualizowane na Twoim koncie {0} z {1}. Jeśli to nie Ty, skontaktuj się z administratorem.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Konfiguracja hasła jednorazowego (OTP)",
|
|
||||||
"requiredAction.terms_and_conditions": "Regulamin",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Aktualizacja hasła",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Aktualizacja profilu",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Weryfikacja adresu e-mail",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "sekund",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.2": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.3": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.4": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minut",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuta",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.2": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.3": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.4": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "godzin",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "godzina",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.2": "godziny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.3": "godziny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.4": "godziny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "dni",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "dzień",
|
|
||||||
"emailVerificationBodyCode": "Potwierdź swój adres e-mail wprowadzając następujący kod.\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>Potwierdź swój adres e-mail, wprowadzając następujący kod.</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"pt-BR": {
|
|
||||||
"emailVerificationSubject": "Verificação de e-mail",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Alguém criou uma conta {2} com este endereço de e-mail. Se foi você, clique no link abaixo para verificar o seu endereço de email\n\n{0}\n\nEste link irá expirar dentro de {3}.\n\nSe não foi você que criou esta conta, basta ignorar esta mensagem.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Alguém criou uma conta {2} com este endereço de e-mail. Se foi você, clique no link abaixo para verificar o seu endereço de email</p><p><a href="{0}">{0}</a></p><p>Este link irá expirar dentro de {3}.</p><p>Se não foi você que criou esta conta, basta ignorar esta mensagem.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - SMTP mensagem de teste",
|
|
||||||
"emailTestBody": "Esta é uma mensagem de teste",
|
|
||||||
"emailTestBodyHtml": "<p>Esta é uma mensagem de teste</p>",
|
|
||||||
"identityProviderLinkSubject": "Vincular {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Alguém quer vincular sua conta "{1}" com a conta "{0}" do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.\n\n{3}\n\nEste link irá expirar em {5}.\n\nSe você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} atrávés de {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Alguém quer vincular sua conta <b>{1}</b> com a conta <b>{0}</b> do usuário {2} . Se foi você, clique no link abaixo para vincular as contas.</p><p><a href="{3}">{3}</a></p><p>Este link irá expirar em {5}.</p><p>Se você não quer vincular a conta, apenas ignore esta mensagem. Se você vincular as contas, você será capaz de logar em {1} atrávés de {0}.</p>',
|
|
||||||
"passwordResetSubject": "Redefinição de senha",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.\n\n{0}\n\nEste link e código expiram em {3}.\n\nSe você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Alguém solicitou uma alteração de senha da sua conta {2}. Se foi você, clique no link abaixo para redefini-la.</p><p><a href="{0}">Link para redefinir a senha</a></p><p>Este link irá expirar em {3}.</p><p>Se você não deseja redefinir sua senha, apenas ignore esta mensagem e nada será alterado.</p>',
|
|
||||||
"executeActionsSubject": "Atualização de conta",
|
|
||||||
"executeActionsBody":
|
|
||||||
"O administrador solicitou que você atualize sua conta {2} executando a(s) seguinte(s) ação(ões): {3}. Clique no link abaixo para iniciar o processo.\n\n{0}\n\nEste link irá expirar em {4}.\n\nSe você não tem conhecimento de que o administrador solicitou isso, basta ignorar esta mensagem e nada será alterado.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>O administrador solicitou que você atualize sua conta {2} executando a(s) seguinte(s) ação(ões): {3}. Clique no link abaixo para iniciar o processo.</p><p><a href="{0}">Link to account update</a></p><p>Este link irá expirar em {4}.</p><p>Se você não tem conhecimento de que o administrador solicitou isso, basta ignorar esta mensagem e nada será alterado.</p>',
|
|
||||||
"eventLoginErrorSubject": "Erro de login",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Uma tentativa de login mal sucedida para a sua conta foi detectada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Remover OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP foi removido da sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP foi removido da sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Atualização de senha",
|
|
||||||
"eventUpdatePasswordBody": "Sua senha foi alterada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Sua senha foi alterada em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Atualização OTP",
|
|
||||||
"eventUpdateTotpBody":
|
|
||||||
"OTP foi atualizado para a sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP foi atualizado para a sua conta em {0} de {1}. Se não foi você, por favor, entre em contato com um administrador.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Configurar OTP",
|
|
||||||
"requiredAction.terms_and_conditions": "Termos e Condições",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Atualizar Senha",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Atualizar Perfil",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Verificar E-mail",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "segundos",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "segundo",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minutos",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minuto",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "horas",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "hora",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "dias",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "dia",
|
|
||||||
"emailVerificationBodyCode": "Verifique seu endereço de e-mail digitando o seguinte código.\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>Verifique seu endereço de e-mail digitando o seguinte código.</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"ru": {
|
|
||||||
"emailVerificationSubject": "Подтверждение E-mail",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Кто-то создал учетную запись {2} с этим E-mail. Если это были Вы, нажмите на следующую ссылку для подтверждения вашего email\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли Вы не создавали учетную запись, просто проигнорируйте это письмо.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Кто-то создал учетную запись {2} с этим E-mail. Если это были Вы, нажмите по ссылке для подтверждения вашего E-mail</p><p><a href="{0}">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если Вы не создавали учетную запись, просто проигнорируйте это письмо.</p>',
|
|
||||||
"identityProviderLinkSubject": "Ссылка {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Кто-то хочет связать вашу учетную запись "{1}" с "{0}" учетной записью пользователя {2} . Если это были Вы, нажмите по следующей ссылке, чтобы связать учетные записи\n\n{3}\n\nЭта ссылка устареет через {4} минут.\n\nЕсли это не хотите объединять учетные записи, просто проигнориуйте это письмо. После объединения учетных записей Вы можете войти в {1} через {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Кто-то хочет связать вашу учетную запись <b>{1}</b> с <b>{0}</b> учетной записью пользователя {2} . Если это были Вы, нажмите по следующей ссылке, чтобы связать учетные записи</p><p><a href="{3}">{3}</a></p><p>Эта ссылка устареет через {4} минут.</p><p>Если это не хотите объединять учетные записи, просто проигнориуйте это письмо. После объединения учетных записей Вы можете войти в {1} через {0}.</p>',
|
|
||||||
"passwordResetSubject": "Сброс пароля",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Кто-то только что запросил изменение пароля от Вашей учетной записи {2}. Если это были Вы, нажмите на следующую ссылку, чтобы сбросить его.\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли Вы не хотите сбрасывать пароль, просто проигнорируйте это письмо.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Кто-то только что запросил изменение пароля от Вашей учетной записи {2}. Если это были Вы, нажмите на следующую ссылку, чтобы сбросить его.</p><p><a href="{0}">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если Вы не хотите сбрасывать пароль, просто проигнорируйте это письмо и ничего не изменится.</p>',
|
|
||||||
"executeActionsSubject": "Обновление Вашей учетной записи",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Администратор просит Вас обновить данные Вашей учетной записи {2}. Нажмите по следующей ссылке чтобы начать этот процесс.\n\n{0}\n\nЭта ссылка устареет через {1} минут.\n\nЕсли у вас есть подозрения, что администратор не мог сделать такой запрос, просто проигнорируйте это письмо.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Администратор просит Вас обновить данные Вашей учетной записи {2}. Нажмите по следующей ссылке чтобы начать этот процесс.</p><p><a href="{0}">{0}</a></p><p>Эта ссылка устареет через {1} минут.</p><p>Если у вас есть подозрения, что администратор не мог сделать такой запрос, просто проигнорируйте это письмо.</p>',
|
|
||||||
"eventLoginErrorSubject": "Ошибка входа",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Была зафиксирована неудачная попытка входа в Вашу учетную запись {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Была зафиксирована неудачная попытка входа в Вашу учетную запись {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Удалить OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP был удален из вашей учетной записи {0} c {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP был удален из вашей учетной записи {0} c {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Обновление пароля",
|
|
||||||
"eventUpdatePasswordBody": "Ваш пароль был изменен в {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Ваш пароль был изменен в {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Обновление OTP",
|
|
||||||
"eventUpdateTotpBody": "OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
|
|
||||||
},
|
|
||||||
"sk": {
|
|
||||||
"emailVerificationSubject": "Overenie e-mailu",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Niekto vytvoril účet {2} s touto e-mailovou adresou. Ak ste to vy, kliknite na nižšie uvedený odkaz a overte svoju e-mailovú adresu \n\n{0}\n\nTento odkaz uplynie do {1} minút.\n\nAk ste tento účet nevytvorili, ignorujte túto správu.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Niekto vytvoril účet {2} s touto e-mailovou adresou. Ak ste to vy, kliknite na nižšie uvedený odkaz na overenie svojej e-mailovej adresy.</p><p><a href="{0}"> Odkaz na overenie e-mailovej adresy </a></p><p>Platnosť odkazu vyprší za {1} minút.</p><p> Ak ste tento účet nevytvorili, ignorujte túto správu.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - Testovacia správa SMTP",
|
|
||||||
"emailTestBody": "Toto je skúšobná správa",
|
|
||||||
"emailTestBodyHtml": "<p>Toto je skúšobná správa</p>",
|
|
||||||
"identityProviderLinkSubject": "Odkaz {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Niekto chce prepojiť váš účet "{1}" s účtom {0}"používateľa {2}. Ak ste to vy, kliknutím na odkaz nižšie prepojte účty. \n\n{3}\n\nTento odkaz uplynie do {4} minút.\n\nAk nechcete prepojiť účet, jednoducho ignorujte túto správu , Ak prepájate účty, budete sa môcť prihlásiť do {1} až {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Niekto chce prepojiť váš účet <b>{1}</b> s účtom <b>{0}</b> používateľa {2}. Ak ste to vy, kliknutím na odkaz nižšie prepojte účty</p><p><a href="{3}">Odkaz na potvrdenie prepojenia účtu </a></p><p> Platnosť tohto odkazu vyprší v rámci {4} minút.</p><p>Ak nechcete prepojiť účet, ignorujte túto správu. Ak prepojujete účty, budete sa môcť prihlásiť do {1} až {0}.</p>',
|
|
||||||
"passwordResetSubject": "Obnovenie hesla",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Niekto požiadal, aby ste zmenili svoje poverenia účtu {2}. Ak ste to vy, kliknite na odkaz uvedený nižšie, aby ste ich vynulovali.\n\n{0}\n\nTento odkaz a kód uplynie do {1} minút.\n\nAk nechcete obnoviť svoje poverenia , ignorujte túto správu a nič sa nezmení.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Niekto požiadal, aby ste zmenili svoje poverenia účtu {2}. Ak ste to vy, kliknutím na odkaz nižšie ich resetujte.</p><p><a href="{0}">Odkaz na obnovenie poverení </a></p><p>Platnosť tohto odkazu vyprší v priebehu {1} minút.</p><p>Ak nechcete obnoviť svoje poverenia, ignorujte túto správu a nič sa nezmení.</p>',
|
|
||||||
"executeActionsSubject": "Aktualizujte svoj účet",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Váš administrátor práve požiadal o aktualizáciu vášho účtu {2} vykonaním nasledujúcich akcií: {3}. Kliknutím na odkaz uvedený nižšie spustíte tento proces.\n\n{0}\n\nTento odkaz vyprší za {1} minúty.\n\nAk si nie ste vedomý, že váš adminstrátor o toto požiadal, ignorujte túto správu a nič bude zmenené.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Váš správca práve požiadal o aktualizáciu vášho účtu {2} vykonaním nasledujúcich akcií: {3}. Kliknutím na odkaz uvedený nižšie spustíte tento proces.</p><p><a href="{0}"> Odkaz na aktualizáciu účtu </a></p><p> Platnosť tohto odkazu uplynie do {1} minúty.</p><p> Ak si nie ste vedomí, že váš adminstrátor o toto požiadal, ignorujte túto správu a nič sa nezmení.</p>',
|
|
||||||
"eventLoginErrorSubject": "Chyba prihlásenia",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Bol zistený neúspešný pokus o prihlásenie do vášho účtu v {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Bol zistený neúspešný pokus o prihlásenie vášho účtu na {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Odstrániť TOTP",
|
|
||||||
"eventRemoveTotpBody": "OTP bol odstránený z vášho účtu dňa {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
|
|
||||||
"eventRemoveTotpBodyHtml": "<p>OTP bol odstránený z vášho účtu dňa {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Aktualizovať heslo",
|
|
||||||
"eventUpdatePasswordBody": "Vaše heslo bolo zmenené na {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>Vaše heslo bolo zmenené na {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Aktualizácia TOTP",
|
|
||||||
"eventUpdateTotpBody": "TOTP bol aktualizovaný pre váš účet na {0} z {1}. Ak ste to neboli vy, obráťte sa na administrátora.",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>TOTP bol aktualizovaný pre váš účet dňa {0} z {1}. Ak ste to neboli vy, kontaktujte administrátora.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "Konfigurácia OTP",
|
|
||||||
"requiredAction.terms_and_conditions": "Zmluvné podmienky",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Aktualizovať heslo",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Aktualizovať profil",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "Overiť e-mail",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "sekundy",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "sekunda",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "minuty",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "minúta",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "hodiny",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "hodina",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "dni",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "deň ",
|
|
||||||
},
|
|
||||||
"sv": {
|
|
||||||
"emailVerificationSubject": "Verifiera e-post",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Någon har skapat ett {2} konto med den här e-postadressen. Om det var du, klicka då på länken nedan för att verifiera din e-postadress\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm det inte var du som skapade det här kontot, ignorera i så fall det här meddelandet.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Någon har skapat ett {2} konto med den här e-postadressen. Om det var du, klicka då på länken nedan för att verifiera din e-postadress</p><p><a href="{0}">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om det inte var du som skapade det här kontot, ignorera i så fall det här meddelandet.</p>',
|
|
||||||
"identityProviderLinkSubject": "Länk {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Någon vill länka ditt "{1}" konto med "{0}" kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona\n\n{3}\n\nDen här länken kommer att upphöra inom {4} minuter.\n\nOm du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Någon vill länka ditt <b>{1}</b> konto med <b>{0}</b> kontot tillhörande användaren {2} . Om det var du, klicka då på länken nedan för att länka kontona</p><p><a href="{3}">{3}</a></p><p>Den här länken kommer att upphöra inom {4} minuter.</p><p>Om du inte vill länka kontot, ignorera i så fall det här meddelandet. Om du länkar kontona, så kan du logga in till {1} genom {0}.</p>',
|
|
||||||
"passwordResetSubject": "Återställ lösenord",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.\n\n{0}\n\nDen här länken och koden kommer att upphöra inom {1} minuter.\n\nOm du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Någon har precis bett om att ändra användaruppgifter för ditt konto {2}. Om det var du, klicka då på länken nedan för att återställa dem.</p><p><a href="{0}">{0}</a></p><p>Den här länken och koden kommer att upphöra inom {1} minuter.</p><p>Om du inte vill återställa dina kontouppgifter, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>',
|
|
||||||
"executeActionsSubject": "Uppdatera ditt konto",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.\n\n{0}\n\nDen här länken kommer att upphöra inom {1} minuter.\n\nOm du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Din administratör har precis bett om att du skall uppdatera ditt {2} konto. Klicka på länken för att påbörja processen.</p><p><a href="{0}">{0}</a></p><p>Den här länken kommer att upphöra inom {1} minuter.</p><p>Om du är omedveten om att din administratör har bett om detta, ignorera i så fall det här meddelandet så kommer inget att ändras.</p>',
|
|
||||||
"eventLoginErrorSubject": "Inloggningsfel",
|
|
||||||
"eventLoginErrorBody":
|
|
||||||
"Ett misslyckat inloggningsförsök har upptäckts på ditt konto på {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>Ett misslyckat inloggningsförsök har upptäckts på ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
|
|
||||||
"eventRemoveTotpSubject": "Ta bort OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP togs bort från ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP togs bort från ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Uppdatera lösenord",
|
|
||||||
"eventUpdatePasswordBody": "Ditt lösenord ändrades den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Ditt lösenord ändrades den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
|
|
||||||
"eventUpdateTotpSubject": "Uppdatera OTP",
|
|
||||||
"eventUpdateTotpBody": "OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
|
|
||||||
},
|
|
||||||
"tr": {
|
|
||||||
"emailVerificationSubject": "E-postayı doğrula",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"Birisi bu e-posta adresiyle bir {2} hesap oluşturdu. Bu sizseniz, e-posta adresinizi doğrulamak için aşağıdaki bağlantıya tıklayın\n\n{0}\n\nBu bağlantı {3} içinde sona erecek.\n\nBu hesabı oluşturmadıysanız, sadece bu iletiyi yoksayınız.",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>Birisi bu e-posta adresiyle bir {2} hesap oluşturdu. Bu sizseniz, e-posta adresinizi doğrulamak için aşağıdaki bağlantıyı tıklayın.</p><p><a href="{0}">E-posta adresi doğrulama adresi</a></p><p>Bu bağlantının süresi {3} içerisinde sona erecek.</p><p>Bu hesabı siz oluşturmadıysanız, bu mesajı göz ardı edin.</p>',
|
|
||||||
"emailTestSubject": "[KEYCLOAK] - SMTP test mesajı",
|
|
||||||
"emailTestBody": "Bu bir test mesajı",
|
|
||||||
"emailTestBodyHtml": "<p>Bu bir test mesajı</p>",
|
|
||||||
"identityProviderLinkSubject": "Link {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'Birisi "{1}" hesabınızı "{0}" kullanıcı hesabı {2} ile bağlamak istiyor. Bu sizseniz, hesapları bağlamak için aşağıdaki bağlantıyı tıklayın:\n\n{3}\n\nBu bağlantı {5} içinde sona erecek.\n\nHesabınızı bağlamak istemiyorsanız bu mesajı göz ardı edin. Hesapları bağlarsanız, {1} ile {0} arasında oturum açabilirsiniz.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>Birisi <b> {1} </ b> hesabınızı {2} kullanıcısı <b> {0} </ b> hesabına bağlamak istiyor. Bu sizseniz, bağlantı vermek için aşağıdaki bağlantıyı tıklayın</p><p><a href="{3}">Hesap bağlantısını onaylamak için bağlantı</a></p><p>Bu bağlantının süresi {5} içerisinde sona erecek.</p><p>Hesabı bağlamak istemiyorsanız, bu mesajı göz ardı edin. Hesapları bağlarsanız, {1} ile {0} arasında oturum açabilirsiniz.</p>',
|
|
||||||
"passwordResetSubject": "Şifreyi sıfırla",
|
|
||||||
"passwordResetBody":
|
|
||||||
"Birisi, {2} hesabınızın kimlik bilgilerini değiştirmeyi istedi.Bu sizseniz, sıfırlamak için aşağıdaki bağlantıyı tıklayın.\n\n{0}\n\nBu bağlantı ve kod {3} içinde sona erecek.\n\nFakat bilgilerinizi sıfırlamak istemiyorsanız, Sadece bu mesajı görmezden gelin ve hiçbir şey değişmeyecek.",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>Birisi, {2} hesabınızın kimlik bilgilerini değiştirmeyi istedi. Sizseniz, sıfırlamak için aşağıdaki linke tıklayınız.</p><p><a href="{0}">Kimlik bilgilerini sıfırlamak için bağlantı</a></p><p>Bu bağlantının süresi {3} içerisinde sona erecek.</p><p>Kimlik bilgilerinizi sıfırlamak istemiyorsanız, bu mesajı göz ardı edin.</p>',
|
|
||||||
"executeActionsSubject": "Hesabınızı Güncelleyin",
|
|
||||||
"executeActionsBody":
|
|
||||||
"Yöneticiniz aşağıdaki işlemleri gerçekleştirerek {2} hesabınızı güncelledi: {3}. Bu işlemi başlatmak için aşağıdaki linke tıklayın.\n\n{0}\n\nBu bağlantının süresi {4} içerisinde sona erecek.\n\nYöneticinizin bunu istediğinden habersizseniz, bu mesajı göz ardı edin ve hiçbir şey değişmez.",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>Yöneticiniz aşağıdaki işlemleri gerçekleştirerek {2} hesabınızı güncelledi: {3}. Bu işlemi başlatmak için aşağıdaki linke tıklayın.</p><p><a href="{0}">Hesap güncelleme bağlantısı</a></p><p>Bu bağlantının süresi {4} içerisinde sona erecek.</p><p>Yöneticinizin bunu istediğinden habersizseniz, bu mesajı göz ardı edin ve hiçbir şey değişmez.</p>',
|
|
||||||
"eventLoginErrorSubject": "Giriş hatası",
|
|
||||||
"eventLoginErrorBody": "{1} 'den {0} tarihinde başarısız bir giriş denemesi yapıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
|
|
||||||
"eventLoginErrorBodyHtml":
|
|
||||||
"<p>{1} 'den {0} tarihinde başarısız bir giriş denemesi yapıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
|
|
||||||
"eventRemoveTotpSubject": "OTP'yi kaldır",
|
|
||||||
"eventRemoveTotpBody": "OTP, {0} tarihinden {1} tarihinde hesabınızdan kaldırıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
|
|
||||||
"eventRemoveTotpBodyHtml":
|
|
||||||
"<p>OTP, {0} tarihinden {1} tarihinde hesabınızdan kaldırıldı. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
|
|
||||||
"eventUpdatePasswordSubject": "Şifreyi güncelle",
|
|
||||||
"eventUpdatePasswordBody": "Şifreniz {0} tarihinde {0} tarihinde değiştirildi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
|
|
||||||
"eventUpdatePasswordBodyHtml":
|
|
||||||
"<p>Şifreniz {0} tarihinde {0} tarihinde değiştirildi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
|
|
||||||
"eventUpdateTotpSubject": "OTP'yi Güncelle",
|
|
||||||
"eventUpdateTotpBody": "OTP, {0} tarihinden {1} tarihinde hesabınız için güncellendi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.",
|
|
||||||
"eventUpdateTotpBodyHtml":
|
|
||||||
"<p>OTP, {0} tarihinden {1} tarihinde hesabınız için güncellendi. Bu siz değilseniz, lütfen yöneticiyle iletişime geçin.</p>",
|
|
||||||
"requiredAction.CONFIGURE_TOTP": "OTP'yi yapılandır",
|
|
||||||
"requiredAction.terms_and_conditions": "Şartlar ve Koşullar",
|
|
||||||
"requiredAction.UPDATE_PASSWORD": "Şifre Güncelleme",
|
|
||||||
"requiredAction.UPDATE_PROFILE": "Profilleri güncelle",
|
|
||||||
"requiredAction.VERIFY_EMAIL": "E-mail doğrula",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds": "saniye",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.seconds.1": "saniye",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes": "dakika",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.minutes.1": "dakika",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours": "saat",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.hours.1": "saat",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days": "gün",
|
|
||||||
"linkExpirationFormatter.timePeriodUnit.days.1": "gün",
|
|
||||||
"emailVerificationBodyCode": "Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.\n\n{0}\n\n.",
|
|
||||||
"emailVerificationBodyCodeHtml": "<p>Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.</p><p><b>{0}</b></p>",
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
|
||||||
"emailVerificationSubject": "验证电子邮件",
|
|
||||||
"emailVerificationBody":
|
|
||||||
"用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您没有注册用户,请忽略这条消息。",
|
|
||||||
"emailVerificationBodyHtml":
|
|
||||||
'<p>用户使用当前电子邮件注册 {2} 账户。如是本人操作,请点击以下链接完成邮箱验证</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期.</p><p>如果您没有注册用户,请忽略这条消息。</p>',
|
|
||||||
"identityProviderLinkSubject": "链接 {0}",
|
|
||||||
"identityProviderLinkBody":
|
|
||||||
'有用户想要将账户 "{1}" 与用户{2}的账户"{0}" 做链接 . 如果是本人操作,请点击以下链接完成链接请求\n\n{3}\n\n这个链接会在 {4} 分钟后过期.\n\n如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.',
|
|
||||||
"identityProviderLinkBodyHtml":
|
|
||||||
'<p>有用户想要将账户 <b>{1}</b> 与用户{2} 的账户<b>{0}</b> 做链接 . 如果是本人操作,请点击以下链接完成链接请求</p><p><a href="{3}">{3}</a></p><p>这个链接会在 {4} 分钟后过期。</p><p>如非本人操作,请忽略这条消息。如果您链接账户,您将可以通过{0}登录账户 {1}.</p>',
|
|
||||||
"passwordResetSubject": "重置密码",
|
|
||||||
"passwordResetBody":
|
|
||||||
"有用户要求修改账户 {2} 的密码.如是本人操作,请点击下面链接进行重置.\n\n{0}\n\n这个链接会在 {1} 分钟后过期.\n\n如果您不想重置您的密码,请忽略这条消息,密码不会改变。",
|
|
||||||
"passwordResetBodyHtml":
|
|
||||||
'<p>有用户要求修改账户 {2} 的密码如是本人操作,请点击下面链接进行重置.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后过期</p><p>如果您不想重置您的密码,请忽略这条消息,密码不会改变。</p>',
|
|
||||||
"executeActionsSubject": "更新您的账户",
|
|
||||||
"executeActionsBody":
|
|
||||||
"您的管理员要求您更新账户 {2}. 点击以下链接开始更新\n\n{0}\n\n这个链接会在 {1} 分钟后失效.\n\n如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。",
|
|
||||||
"executeActionsBodyHtml":
|
|
||||||
'<p>您的管理员要求您更新账户{2}. 点击以下链接开始更新.</p><p><a href="{0}">{0}</a></p><p>这个链接会在 {1} 分钟后失效.</p><p>如果您不知道管理员要求更新账户信息,请忽略这条消息。账户信息不会修改。</p>',
|
|
||||||
"eventLoginErrorSubject": "登录错误",
|
|
||||||
"eventLoginErrorBody": "在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.",
|
|
||||||
"eventLoginErrorBodyHtml": "<p>在{0} 由 {1}使用您的账户登录失败. 如果这不是您本人操作,请联系管理员.</p>",
|
|
||||||
"eventRemoveTotpSubject": "删除 OTP",
|
|
||||||
"eventRemoveTotpBody": "OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员",
|
|
||||||
"eventRemoveTotpBodyHtml": "<p>OTP在 {0} 由{1} 从您的账户中删除.如果这不是您本人操作,请联系管理员。</p>",
|
|
||||||
"eventUpdatePasswordSubject": "更新密码",
|
|
||||||
"eventUpdatePasswordBody": "您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员",
|
|
||||||
"eventUpdatePasswordBodyHtml": "<p>您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员</p>",
|
|
||||||
"eventUpdateTotpSubject": "更新 OTP",
|
|
||||||
"eventUpdateTotpBody": "您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。",
|
|
||||||
"eventUpdateTotpBodyHtml": "<p>您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
/* spell-checker: enable */
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user