Compare commits
300 Commits
Author | SHA1 | Date | |
---|---|---|---|
f256b74929 | |||
4f1182a230 | |||
e7c20547f8 | |||
9ab4c510fe | |||
7d78c52064 | |||
6223d91291 | |||
840b5e1312 | |||
e69813f6e3 | |||
3c0c057e06 | |||
984d12b3f2 | |||
61dc54f115 | |||
34e47cccc1 | |||
c170345550 | |||
1e40706f72 | |||
ea1a747ebf | |||
a14e967020 | |||
0fff10d2c6 | |||
7c2123614d | |||
d149866703 | |||
18039140db | |||
4de9599018 | |||
bb85829d71 | |||
ff077943ec | |||
f057114bcc | |||
e7bfe7f80d | |||
18112a97ab | |||
8ee6fb58ac | |||
08831fc31d | |||
c5c25394fb | |||
2f649c9866 | |||
91c5dd40fa | |||
e95e688cf0 | |||
9845f1de08 | |||
07032d312d | |||
ccb5d32763 | |||
bf83e4b03b | |||
03b491763f | |||
3abc9edf0e | |||
f9accc51d3 | |||
c3bade81b4 | |||
6edd1f00dd | |||
02be899629 | |||
8e043f289a | |||
30fecf8578 | |||
1112da33e3 | |||
ffa65e871e | |||
f49c7b465b | |||
e6f75156ec | |||
ebafeb19ad | |||
5166c719c4 | |||
bf92ea8340 | |||
cf1e595ba2 | |||
2bf3296c0f | |||
11513f73b7 | |||
b6f60c6835 | |||
e9d276010f | |||
b08c4b0b29 | |||
d684807d96 | |||
9a60ef7c47 | |||
cc446059de | |||
d75b809c13 | |||
9fc3998cf7 | |||
238baa72cf | |||
089f0f7a87 | |||
aa9d3d1931 | |||
2fc6aed4f1 | |||
c2fdea7886 | |||
c8f71946d4 | |||
d1cc6ed88d | |||
f6e6cf3750 | |||
d1c7491704 | |||
fd49c2fd23 | |||
f7fb2efcdd | |||
ff0608c202 | |||
e63e20eade | |||
335292cf4c | |||
7cb927c8b8 | |||
802d6b3dad | |||
c16bf28369 | |||
0de76ae613 | |||
880396e3a6 | |||
879fc2812d | |||
6ac6209bd0 | |||
d7fd76c568 | |||
543e08276f | |||
2e647b9196 | |||
69cf556582 | |||
e168ee2ae6 | |||
274d758ba8 | |||
b52e35be7d | |||
7f1ba8f166 | |||
72a5b9bac5 | |||
34fb0c2753 | |||
e5f0885cb0 | |||
4f93190162 | |||
9d1dcd278a | |||
45d4bce0e7 | |||
680a7206d3 | |||
8a08e9fd64 | |||
0080dabe09 | |||
556ce60b27 | |||
12857e3027 | |||
10965b82a9 | |||
86884607ef | |||
1ff0449332 | |||
57b056b388 | |||
9058e9ac9d | |||
ad3de8bff5 | |||
476b100b04 | |||
b2c7c86609 | |||
f8a8ec2e4d | |||
393a5ba125 | |||
466c2d3eb4 | |||
b325b3537f | |||
e429127313 | |||
2d05521789 | |||
564ffc2be9 | |||
feaf34c124 | |||
c1e0563eba | |||
1c66f35337 | |||
4a7dd64982 | |||
a84f984a07 | |||
1f31a228d7 | |||
309308db15 | |||
a02c38ac45 | |||
49e495dbbe | |||
f6c2ccb0d6 | |||
dcd30f2cad | |||
e5d540ebd2 | |||
1073a610d6 | |||
034f6f8b0e | |||
3edb23be97 | |||
d308c04465 | |||
c5899eba94 | |||
97cb20b731 | |||
f58a5ad524 | |||
e00692956c | |||
b2c1b41981 | |||
ffa8440d1b | |||
32f66b3eaa | |||
42b196bd0b | |||
68dab45931 | |||
af2dbb0389 | |||
5abbc7f9a7 | |||
dcfefad17f | |||
4ece6457fd | |||
53e38336fb | |||
0b16df7731 | |||
900125d92e | |||
6aaaf5a9d3 | |||
bd2f6d8fee | |||
baae22657e | |||
46264c85f4 | |||
2811eb6024 | |||
218c1a5a50 | |||
ab5287a3d4 | |||
d55c62c073 | |||
4833c34800 | |||
fc70e657f0 | |||
ee23f629f6 | |||
44402c9571 | |||
ffefb38161 | |||
6d667f653e | |||
1c75fed727 | |||
e7837aea88 | |||
9c133be779 | |||
71eb953fd3 | |||
f49ef21fed | |||
6a6fa04ba0 | |||
83b0838c94 | |||
4ebc1e671f | |||
08c7e38587 | |||
b863d9feb3 | |||
e527f043b0 | |||
58bb403787 | |||
e4725c23eb | |||
b0db8caf65 | |||
3bcc6bdf93 | |||
eafb75a958 | |||
31ca0939aa | |||
7784fdcd6a | |||
8247eef735 | |||
cb6629f301 | |||
3a6fe1b374 | |||
0ba2f37004 | |||
e052dee753 | |||
9c2ec32d12 | |||
1669c38bc9 | |||
c6ce6d1b49 | |||
bc242b0aa7 | |||
41b67f6af4 | |||
bef21e1cb9 | |||
8c73630f5a | |||
724953d5b7 | |||
a22b231982 | |||
910bfe2318 | |||
70a524da46 | |||
bf6c846fac | |||
b83e4bef3f | |||
9f7fe0d8f7 | |||
741dee57e4 | |||
fff4dba708 | |||
f4f7ab3e49 | |||
88fe99b1b8 | |||
92c1486f6a | |||
caea64cef3 | |||
90783d8ee8 | |||
be57801e21 | |||
ff84786b4e | |||
1e863672cb | |||
fb98a9c383 | |||
05163f22cb | |||
160f12d7d3 | |||
49e4e36184 | |||
c4f8879cda | |||
8f54166653 | |||
b9f020c447 | |||
c357f3eb4d | |||
7ebbb0417a | |||
6e4b4173b5 | |||
87ebad7efb | |||
3294aaed3b | |||
0e21f3eab6 | |||
9fcf692cb8 | |||
da577ea3cc | |||
6ae1d8938a | |||
3e18a7390c | |||
5f43f1afc6 | |||
2fc9c03430 | |||
d951a9ba02 | |||
93385af675 | |||
dd75d0ece7 | |||
dcd37ed916 | |||
2e4d722d7f | |||
09543400ca | |||
8b101e5043 | |||
b31fff9c2b | |||
0c5b100dd9 | |||
253825a35e | |||
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 | |||
9ae8822e00 | |||
babffd1fe6 | |||
5615d62032 | |||
4b89d15c1e | |||
815f510d5f | |||
199ba193be | |||
4ae9bd3f9a | |||
1c9cf639ea | |||
0040464ca1 | |||
79997efbb6 | |||
0e42009798 |
151
.all-contributorsrc
Normal file
@ -0,0 +1,151 @@
|
||||
{
|
||||
"files": [
|
||||
"README.md"
|
||||
],
|
||||
"imageSize": 100,
|
||||
"commit": false,
|
||||
"commitConvention": "angular",
|
||||
"contributors": [
|
||||
{
|
||||
"login": "lordvlad",
|
||||
"name": "Waldemar Reusch",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1217769?v=4",
|
||||
"profile": "https://github.com/lordvlad",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "willwill96",
|
||||
"name": "William Will",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10997562?v=4",
|
||||
"profile": "https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Ann2827",
|
||||
"name": "Bystrova Ann",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32645809?v=4",
|
||||
"profile": "https://github.com/Ann2827",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mkreuzmayr",
|
||||
"name": "Michael Kreuzmayr",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/20108212?v=4",
|
||||
"profile": "https://github.com/mkreuzmayr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Mstrodl",
|
||||
"name": "Mary ",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6877780?v=4",
|
||||
"profile": "https://coolmathgames.tech",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tasyp",
|
||||
"name": "German Öö",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6623212?v=4",
|
||||
"profile": "https://tasyp.xyz/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "revolunet",
|
||||
"name": "Julien Bouquillon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/124937?v=4",
|
||||
"profile": "https://revolunet.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aidangilmore",
|
||||
"name": "Aidan Gilmore",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32880357?v=4",
|
||||
"profile": "https://github.com/aidangilmore",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "0x-Void",
|
||||
"name": "Void",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/32745739?v=4",
|
||||
"profile": "https://github.com/0x-Void",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "juffe",
|
||||
"name": "juffe",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5393231?v=4",
|
||||
"profile": "https://github.com/juffe",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lazToum",
|
||||
"name": "Lazaros Toumanidis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4764837?v=4",
|
||||
"profile": "https://github.com/lazToum",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "marcmrf",
|
||||
"name": "Marc",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9928519?v=4",
|
||||
"profile": "https://github.com/marcmrf",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "kasir-barati",
|
||||
"name": "Kasir Barati",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/73785723?v=4",
|
||||
"profile": "http://kasir-barati.github.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "asashay",
|
||||
"name": "Alex Oliynyk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10714670?v=4",
|
||||
"profile": "https://github.com/asashay",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "thosil",
|
||||
"name": "Thomas Silvestre",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1140574?v=4",
|
||||
"profile": "https://www.gravitysoftware.be",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
"skipCi": true,
|
||||
"repoType": "github",
|
||||
"repoHost": "https://github.com",
|
||||
"projectName": "keycloakify",
|
||||
"projectOwner": "keycloakify"
|
||||
}
|
70
.github/workflows/ci.yaml
vendored
@ -17,44 +17,48 @@ jobs:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- name: If this step fails run 'yarn format' then commit again.
|
||||
run: |
|
||||
PACKAGE_MANAGER=npm
|
||||
if [ -f "./yarn.lock" ]; then
|
||||
PACKAGE_MANAGER=yarn
|
||||
fi
|
||||
$PACKAGE_MANAGER run format:check
|
||||
run: yarn format:check
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs: test_lint
|
||||
strategy:
|
||||
matrix:
|
||||
node: [ '14','16' ]
|
||||
os: [ windows-latest, ubuntu-latest ]
|
||||
node: [ '18' ]
|
||||
os: [ ubuntu-latest ]
|
||||
name: Test with Node v${{ matrix.node }} on ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Tell if project is using npm or yarn
|
||||
id: step1
|
||||
uses: garronej/ts-ci@v2.0.2
|
||||
with:
|
||||
action_name: tell_if_project_uses_npm_or_yarn
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- if: steps.step1.outputs.npm_or_yarn == 'yarn'
|
||||
run: |
|
||||
yarn build
|
||||
yarn test
|
||||
- if: steps.step1.outputs.npm_or_yarn == 'npm'
|
||||
run: |
|
||||
npm run build
|
||||
npm test
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- run: yarn test:keycloakify-starter
|
||||
|
||||
storybook:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'push'
|
||||
needs: test
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: yarn build-storybook -o ./build_storybook
|
||||
- run: git remote set-url origin https://git:${GITHUB_TOKEN}@github.com/${{github.repository}}.git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- run: npx -y -p gh-pages@3.1.0 gh-pages -d ./build_storybook -u "github-actions-bot <actions@github.com>"
|
||||
|
||||
check_if_version_upgraded:
|
||||
name: Check if version upgrade
|
||||
# We run this only if it's a push on the default branch or if it's a PR from a
|
||||
# branch (meaning not a PR from a fork). It would be more straightforward to test if secrets.NPM_TOKEN is
|
||||
# defined but GitHub Action don't allow it yet.
|
||||
# When someone forks the repo and opens a PR we want to enables the tests to be run (the previous jobs)
|
||||
# but obviously only us should be allowed to release.
|
||||
# In the following check we make sure that we own the branch this CI workflow is running on before continuing.
|
||||
# Without this check, trying to release would fail anyway because only us have the correct secret.NPM_TOKEN but
|
||||
# it's cleaner to stop the execution instead of letting the CI crash.
|
||||
if: |
|
||||
github.event_name == 'push' ||
|
||||
github.event.pull_request.head.repo.owner.login == github.event.pull_request.base.repo.owner.login
|
||||
@ -66,7 +70,7 @@ jobs:
|
||||
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
|
||||
is_pre_release: ${{steps.step1.outputs.is_pre_release }}
|
||||
steps:
|
||||
- uses: garronej/ts-ci@v2.0.2
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
id: step1
|
||||
with:
|
||||
action_name: is_package_json_version_upgraded
|
||||
@ -74,8 +78,8 @@ jobs:
|
||||
|
||||
create_github_release:
|
||||
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.
|
||||
# We create release only if the version in the package.json have been upgraded and this CI is running against the main branch.
|
||||
# We allow branches with a PR open on main to publish pre-release (x.y.z-rc.u) but not actual releases.
|
||||
if: |
|
||||
needs.check_if_version_upgraded.outputs.is_upgraded_version == 'true' &&
|
||||
(
|
||||
@ -109,15 +113,13 @@ jobs:
|
||||
with:
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- uses: bahmutov/npm-install@v1
|
||||
- run: |
|
||||
PACKAGE_MANAGER=npm
|
||||
if [ -f "./yarn.lock" ]; then
|
||||
PACKAGE_MANAGER=yarn
|
||||
fi
|
||||
$PACKAGE_MANAGER run build
|
||||
- run: npx -y -p denoify@1.2.2 enable_short_npm_import_path
|
||||
- run: yarn build
|
||||
- run: npx -y -p denoify@1.3.0 enable_short_npm_import_path
|
||||
env:
|
||||
DRY_RUN: "0"
|
||||
- uses: garronej/ts-ci@v2.1.0
|
||||
with:
|
||||
action_name: remove_dark_mode_specific_images_from_readme
|
||||
- name: Publishing on NPM
|
||||
run: |
|
||||
if [ "$(npm show . version)" = "$VERSION" ]; then
|
||||
|
17
.gitignore
vendored
@ -41,12 +41,21 @@ jspm_packages
|
||||
.DS_Store
|
||||
|
||||
/dist
|
||||
/dist_test
|
||||
|
||||
/keycloakify_starter_test/
|
||||
/sample_custom_react_project/
|
||||
/sample_react_project/
|
||||
/.yarn_home/
|
||||
|
||||
.idea
|
||||
|
||||
/keycloak_email
|
||||
/build_keycloak
|
||||
/src/login/i18n/baseMessages/
|
||||
/src/account/i18n/baseMessages/
|
||||
|
||||
# VS Code devcontainers
|
||||
.devcontainer
|
||||
/.yarn
|
||||
/.yarnrc.yml
|
||||
|
||||
/stories/assets/fonts/
|
||||
/build_storybook/
|
||||
/storybook-static/
|
||||
|
@ -1,10 +1,15 @@
|
||||
node_modules/
|
||||
/dist/
|
||||
/dist_test/
|
||||
/CHANGELOG.md
|
||||
/.yarn_home/
|
||||
/src/test/apps/
|
||||
/src/tools/types/
|
||||
/sample_react_project
|
||||
/build_keycloak/
|
||||
/src/lib/i18n/generated_messages/
|
||||
/.vscode/
|
||||
/src/login/i18n/baseMessages/
|
||||
/src/account/i18n/baseMessages/
|
||||
/dist_test
|
||||
/sample_react_project/
|
||||
/sample_custom_react_project/
|
||||
/keycloakify_starter_test/
|
||||
/.storybook/static/keycloak-resources/
|
76
.storybook/Containers.js
Normal file
@ -0,0 +1,76 @@
|
||||
|
||||
import React from "react";
|
||||
import { DocsContainer as BaseContainer } from "@storybook/addon-docs";
|
||||
import { useDarkMode } from "storybook-dark-mode";
|
||||
import { darkTheme, lightTheme } from "./customTheme";
|
||||
import "./static/fonts/WorkSans/font.css";
|
||||
|
||||
export function DocsContainer({ children, context }) {
|
||||
const isStorybookUiDark = useDarkMode();
|
||||
|
||||
const theme = isStorybookUiDark ? darkTheme : lightTheme;
|
||||
|
||||
const backgroundColor = theme.appBg;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
body {
|
||||
padding: 0 !important;
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
|
||||
.docs-story {
|
||||
background-color: ${backgroundColor};
|
||||
}
|
||||
[id^=story--] .container {
|
||||
border: 1px dashed #e8e8e8;
|
||||
}
|
||||
|
||||
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(3) {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
<BaseContainer
|
||||
context={{
|
||||
...context,
|
||||
"storyById": id => {
|
||||
const storyContext = context.storyById(id);
|
||||
return {
|
||||
...storyContext,
|
||||
"parameters": {
|
||||
...storyContext?.parameters,
|
||||
"docs": {
|
||||
...storyContext?.parameters?.docs,
|
||||
"theme": isStorybookUiDark ? darkTheme : lightTheme
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</BaseContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function CanvasContainer({ children }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
`}</style>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
35
.storybook/customTheme.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { create } from "@storybook/theming";
|
||||
|
||||
const brandImage = "logo.png";
|
||||
const brandTitle = "Keycloakify";
|
||||
const brandUrl = "https://github.com/keycloakify/keycloakify";
|
||||
const fontBase = '"Work Sans", sans-serif';
|
||||
const fontCode = "monospace";
|
||||
|
||||
export const darkTheme = create({
|
||||
"base": "dark",
|
||||
"appBg": "#1E1E1E",
|
||||
"appContentBg": "#161616",
|
||||
"barBg": "#161616",
|
||||
"colorSecondary": "#8585F6",
|
||||
"textColor": "#FFFFFF",
|
||||
brandImage,
|
||||
brandTitle,
|
||||
brandUrl,
|
||||
fontBase,
|
||||
fontCode
|
||||
});
|
||||
|
||||
export const lightTheme = create({
|
||||
"base": "light",
|
||||
"appBg": "#F6F6F6",
|
||||
"appContentBg": "#FFFFFF",
|
||||
"barBg": "#FFFFFF",
|
||||
"colorSecondary": "#000091",
|
||||
"textColor": "#212121",
|
||||
brandImage,
|
||||
brandTitle,
|
||||
brandUrl,
|
||||
fontBase,
|
||||
fontCode
|
||||
});
|
15
.storybook/main.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
"stories": [
|
||||
"../stories/**/*.stories.@(ts|tsx|mdx)"
|
||||
],
|
||||
"addons": [
|
||||
"@storybook/addon-links",
|
||||
"@storybook/addon-essentials",
|
||||
"storybook-dark-mode",
|
||||
"@storybook/addon-a11y"
|
||||
],
|
||||
"core": {
|
||||
"builder": "webpack5"
|
||||
},
|
||||
"staticDirs": ["./static"]
|
||||
};
|
32
.storybook/manager-head.html
Normal file
@ -0,0 +1,32 @@
|
||||
<!-- start favicon -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon_package/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon_package/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon_package/favicon-16x16.png">
|
||||
<link rel="manifest" href="/favicon_package/site.webmanifest">
|
||||
<link rel="mask-icon" href="/favicon_package/safari-pinned-tab.svg" color="#5bbad5">
|
||||
<!-- end favicon -->
|
||||
|
||||
<!-- Meta tags generated by metatags.io -->
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Keycloakify Storybook</title>
|
||||
<meta name="title" content="Keycloakify Storybook">
|
||||
<meta name="description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
|
||||
<!-- Facebook Meta Tags -->
|
||||
<meta property="og:url" content="https://www.keycloakify.dev">
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:title" content="Keycloakify Storybook">
|
||||
<meta property="og:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
<meta property="og:image" content="https://storybook.keycloakify.dev/preview.png">
|
||||
|
||||
<!-- Twitter Meta Tags -->
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="twitter:title" content="Keycloakify Storybook">
|
||||
<meta name="twitter:description" content="Storybook of default components to use as a reference when building a custom Keycloak theme">
|
||||
<meta name="twitter:image" content="https://storybook.keycloakify.dev/preview.png">
|
||||
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-bold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-medium-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-regular-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="preload" href="/fonts/WorkSans/worksans-semibold-webfont.woff2" as="font" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="/fonts/WorkSans/font.css">
|
6
.storybook/manager.js
Normal file
@ -0,0 +1,6 @@
|
||||
import { addons } from '@storybook/addons';
|
||||
|
||||
addons.setConfig({
|
||||
"selectedPanel": 'storybook/a11y/panel',
|
||||
"showPanel": false,
|
||||
});
|
137
.storybook/preview.js
Normal file
@ -0,0 +1,137 @@
|
||||
import { darkTheme, lightTheme } from "./customTheme";
|
||||
import { DocsContainer, CanvasContainer } from "./Containers";
|
||||
|
||||
export const parameters = {
|
||||
"actions": { "argTypesRegex": "^on[A-Z].*" },
|
||||
"controls": {
|
||||
"matchers": {
|
||||
"color": /(background|color)$/i,
|
||||
"date": /Date$/,
|
||||
},
|
||||
},
|
||||
"backgrounds": { "disable": true },
|
||||
"darkMode": {
|
||||
"light": lightTheme,
|
||||
"dark": darkTheme,
|
||||
},
|
||||
"docs": {
|
||||
"container": DocsContainer
|
||||
},
|
||||
"controls": {
|
||||
"disable": true,
|
||||
},
|
||||
"actions": {
|
||||
"disable": true
|
||||
},
|
||||
"viewport": {
|
||||
"viewports": {
|
||||
"1440p": {
|
||||
"name": "1440p",
|
||||
"styles": {
|
||||
"width": "2560px",
|
||||
"height": "1440px",
|
||||
},
|
||||
},
|
||||
"fullHD": {
|
||||
"name": "Full HD",
|
||||
"styles": {
|
||||
"width": "1920px",
|
||||
"height": "1080px",
|
||||
},
|
||||
},
|
||||
"macBookProBig": {
|
||||
"name": "MacBook Pro Big",
|
||||
"styles": {
|
||||
"width": "1024px",
|
||||
"height": "640px",
|
||||
},
|
||||
},
|
||||
"macBookProMedium": {
|
||||
"name": "MacBook Pro Medium",
|
||||
"styles": {
|
||||
"width": "1440px",
|
||||
"height": "900px",
|
||||
},
|
||||
},
|
||||
"macBookProSmall": {
|
||||
"name": "MacBook Pro Small",
|
||||
"styles": {
|
||||
"width": "1680px",
|
||||
"height": "1050px",
|
||||
},
|
||||
},
|
||||
"pcAgent": {
|
||||
"name": "PC Agent",
|
||||
"styles": {
|
||||
"width": "960px",
|
||||
"height": "540px",
|
||||
},
|
||||
},
|
||||
"iphone12Pro": {
|
||||
"name": "Iphone 12 pro",
|
||||
"styles": {
|
||||
"width": "390px",
|
||||
"height": "844px",
|
||||
},
|
||||
},
|
||||
"iphone5se": {
|
||||
"name": "Iphone 5/SE",
|
||||
"styles": {
|
||||
"width": "320px",
|
||||
"height": "568px",
|
||||
},
|
||||
},
|
||||
"ipadPro": {
|
||||
"name": "Ipad pro",
|
||||
"styles": {
|
||||
"width": "1240px",
|
||||
"height": "1366px",
|
||||
},
|
||||
},
|
||||
"Galaxy s9+": {
|
||||
"name": "Galaxy S9+",
|
||||
"styles": {
|
||||
"width": "320px",
|
||||
"height": "658px",
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"storySort": (a, b) =>
|
||||
getHardCodedWeight(b[1].kind) - getHardCodedWeight(a[1].kind),
|
||||
},
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
(Story) => (
|
||||
<CanvasContainer>
|
||||
<Story />
|
||||
</CanvasContainer>
|
||||
),
|
||||
];
|
||||
|
||||
const { getHardCodedWeight } = (() => {
|
||||
|
||||
const orderedPagesPrefix = [
|
||||
"Introduction",
|
||||
"login/login.ftl",
|
||||
"login/register-user-profile.ftl",
|
||||
"login/register.ftl",
|
||||
"login/terms.ftl",
|
||||
"login/error.ftl",
|
||||
];
|
||||
|
||||
function getHardCodedWeight(kind) {
|
||||
|
||||
for (let i = 0; i < orderedPagesPrefix.length; i++) {
|
||||
if (kind.toLowerCase().startsWith(orderedPagesPrefix[i].toLowerCase())) {
|
||||
return orderedPagesPrefix.length - i;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
return { getHardCodedWeight };
|
||||
})();
|
1
.storybook/static/CNAME
Normal file
@ -0,0 +1 @@
|
||||
storybook.keycloakify.dev
|
BIN
.storybook/static/favicon_package/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
.storybook/static/favicon_package/android-chrome-384x384.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
.storybook/static/favicon_package/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 30 KiB |
9
.storybook/static/favicon_package/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square150x150logo src="/mstile-150x150.png"/>
|
||||
<TileColor>#da532c</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
</browserconfig>
|
BIN
.storybook/static/favicon_package/favicon-16x16.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
.storybook/static/favicon_package/favicon-32x32.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
.storybook/static/favicon_package/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
.storybook/static/favicon_package/mstile-150x150.png
Normal file
After Width: | Height: | Size: 19 KiB |
193
.storybook/static/favicon_package/safari-pinned-tab.svg
Normal file
@ -0,0 +1,193 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="447.000000pt" height="447.000000pt" viewBox="0 0 447.000000 447.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.14, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,447.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M2177 4413 c-3 -2 -17 -6 -33 -9 -85 -15 -204 -109 -286 -225 -95
|
||||
-133 -229 -437 -263 -597 -4 -18 -10 -30 -13 -28 -4 2 -7 -11 -8 -30 0 -19 -3
|
||||
-36 -6 -39 -3 -4 -23 1 -44 10 -51 22 -213 73 -289 92 -301 73 -516 74 -670 3
|
||||
-124 -57 -186 -153 -188 -295 -1 -67 5 -128 18 -180 3 -13 15 -45 26 -70 43
|
||||
-99 57 -135 53 -135 -3 0 4 -10 16 -22 11 -12 20 -26 20 -31 0 -5 9 -22 21
|
||||
-38 11 -16 18 -29 14 -29 -3 0 4 -10 15 -22 11 -12 18 -28 15 -36 -2 -7 -1
|
||||
-11 3 -8 4 2 19 -12 32 -32 13 -20 33 -47 44 -59 11 -13 17 -23 13 -23 -3 0
|
||||
12 -19 33 -42 22 -24 38 -49 35 -55 -2 -7 -1 -12 4 -10 4 1 32 -25 62 -58 30
|
||||
-33 88 -94 131 -135 l76 -75 -71 -70 c-112 -110 -174 -181 -262 -300 -106
|
||||
-144 -142 -202 -203 -325 -9 -19 -21 -38 -27 -42 -5 -4 -7 -8 -3 -8 4 0 -4
|
||||
-27 -18 -60 -25 -63 -58 -199 -50 -208 3 -2 1 -12 -4 -22 -5 -10 -6 -20 -3
|
||||
-24 4 -4 8 -23 10 -44 9 -107 77 -201 183 -251 33 -16 56 -32 52 -36 -4 -5 -2
|
||||
-5 4 -2 6 4 44 1 85 -6 41 -7 102 -12 136 -12 41 1 60 -2 55 -9 -3 -6 2 -5 11
|
||||
3 11 8 44 14 86 15 38 0 67 4 64 9 -4 6 10 8 79 11 14 0 24 3 22 5 -4 4 33 14
|
||||
145 37 13 3 33 10 43 15 11 6 26 8 34 5 10 -4 12 -2 8 6 -5 8 -2 9 9 5 10 -3
|
||||
17 -2 17 3 0 6 8 10 18 10 9 0 36 9 58 19 32 15 45 16 54 8 9 -9 11 -9 8 2 -2
|
||||
7 2 15 8 18 10 3 23 -34 38 -108 2 -8 6 -21 9 -29 15 -36 70 -206 70 -217 0
|
||||
-7 4 -13 8 -13 11 0 30 -61 22 -74 -3 -6 -3 -8 2 -4 8 7 86 -135 88 -159 0 -7
|
||||
4 -13 8 -13 13 0 39 -56 31 -66 -4 -5 -3 -6 2 -2 5 4 22 -12 39 -35 17 -23 49
|
||||
-59 71 -80 23 -21 40 -42 39 -47 -2 -6 0 -9 5 -8 15 3 63 -22 68 -37 3 -8 10
|
||||
-12 15 -10 5 3 22 -1 39 -10 17 -9 35 -13 40 -10 6 3 10 2 10 -2 0 -5 12 -8
|
||||
28 -8 15 1 33 -4 41 -9 11 -8 13 -8 8 0 -4 7 7 13 33 17 21 3 53 12 71 21 17
|
||||
9 36 13 42 9 5 -3 7 -1 3 5 -3 6 3 13 14 17 11 3 20 11 20 16 0 5 5 9 11 9 5
|
||||
0 7 -6 3 -13 -4 -7 -3 -9 2 -4 5 5 9 12 9 17 0 4 14 21 30 37 69 67 162 196
|
||||
179 251 4 12 12 20 17 16 5 -3 8 -2 7 3 -1 4 6 29 17 56 14 33 24 45 35 41 11
|
||||
-4 12 -2 4 6 -14 14 -6 52 9 43 5 -3 7 -2 4 4 -3 5 8 45 24 88 17 44 33 86 35
|
||||
94 7 34 56 206 59 209 1 1 15 -3 31 -9 16 -7 34 -13 39 -15 61 -18 144 -51
|
||||
139 -56 -3 -4 3 -4 14 -1 11 2 23 0 26 -6 4 -5 14 -7 23 -4 9 4 14 2 10 -3 -3
|
||||
-5 7 -9 21 -8 34 1 63 -6 58 -15 -2 -3 19 -7 47 -9 28 -2 55 -6 61 -10 13 -8
|
||||
18 -9 79 -10 27 -1 46 -5 44 -9 -3 -5 20 -6 50 -5 44 3 54 1 48 -10 -6 -10 -5
|
||||
-11 6 0 16 15 33 16 23 0 -5 -7 -3 -8 7 -3 7 5 29 10 49 12 74 5 116 11 135
|
||||
18 11 4 37 13 57 21 28 10 38 11 41 1 4 -8 6 -7 6 3 1 8 21 28 46 44 43 26 92
|
||||
86 109 131 28 73 27 217 -3 313 -5 18 -10 33 -9 35 2 23 -118 257 -184 356
|
||||
-44 67 -124 177 -138 191 -3 3 -34 39 -70 80 -36 41 -97 107 -137 146 l-72 71
|
||||
25 22 c14 12 30 19 36 15 6 -4 8 -3 4 4 -3 6 24 42 61 80 38 38 86 91 108 117
|
||||
22 27 46 54 53 61 6 7 12 17 12 23 0 6 3 11 8 11 4 0 22 22 41 50 19 27 37 47
|
||||
41 45 4 -3 7 3 6 13 0 9 5 16 12 14 8 -1 11 2 7 7 -5 9 36 83 55 101 5 5 101
|
||||
200 121 245 7 18 26 84 36 125 2 8 4 27 5 42 0 16 5 25 11 21 6 -4 7 -1 2 7
|
||||
-11 18 -11 62 0 80 5 8 4 11 -2 7 -6 -4 -12 10 -16 36 -6 51 -10 70 -18 77 -3
|
||||
3 -15 21 -27 41 -42 70 -184 145 -292 155 -205 20 -451 -18 -709 -108 -30 -10
|
||||
-60 -17 -68 -14 -8 3 -12 2 -9 -3 5 -7 -42 -31 -60 -31 -1 0 -5 15 -9 33 -18
|
||||
86 -108 342 -156 444 -17 35 -29 66 -29 69 1 4 -2 10 -7 13 -5 3 -24 32 -42
|
||||
64 -108 190 -245 296 -403 311 -20 2 -39 2 -41 -1z m53 -124 c0 -5 5 -7 10 -4
|
||||
14 9 52 -4 45 -16 -3 -5 0 -6 8 -4 17 7 69 -33 61 -47 -5 -7 -2 -8 5 -4 13 8
|
||||
43 -23 35 -36 -3 -4 1 -6 8 -3 7 2 24 -11 38 -31 14 -19 30 -41 35 -47 50 -56
|
||||
148 -233 139 -249 -4 -6 -3 -9 2 -5 12 7 97 -192 90 -210 -3 -8 -2 -12 3 -9
|
||||
11 7 45 -120 35 -135 -4 -8 -3 -9 4 -5 7 4 12 3 12 -3 0 -5 3 -16 6 -25 4 -11
|
||||
-23 -30 -113 -75 -138 -71 -276 -145 -350 -189 -29 -17 -53 -29 -53 -26 0 3
|
||||
-7 -1 -15 -10 -14 -14 -19 -13 -53 6 -20 11 -62 34 -92 51 -30 16 -59 33 -65
|
||||
37 -5 4 -86 47 -180 95 -93 48 -173 91 -177 94 -11 10 3 52 15 44 6 -3 7 -1 3
|
||||
6 -9 14 12 113 22 107 4 -2 8 7 8 20 2 24 42 134 53 144 3 3 7 12 9 20 2 8 17
|
||||
44 33 80 24 50 32 61 40 50 8 -11 9 -10 4 7 -5 17 10 47 60 123 36 55 71 98
|
||||
76 94 5 -3 9 -2 8 3 -4 23 2 35 17 29 8 -3 12 -2 9 3 -7 12 60 78 100 99 25
|
||||
13 70 27 98 31 4 1 7 -4 7 -10z m-1411 -783 c9 -6 12 -5 8 1 -4 6 10 10 34 11
|
||||
29 1 38 -1 33 -11 -5 -9 -4 -9 7 1 7 6 19 12 27 12 8 0 10 -5 6 -12 -6 -10 -5
|
||||
-10 7 -1 9 7 26 10 40 8 13 -3 47 -8 74 -11 63 -7 155 -23 175 -31 15 -6 35
|
||||
-10 71 -12 12 -1 17 -5 13 -13 -4 -7 -3 -8 4 -4 6 4 31 1 54 -5 24 -7 49 -13
|
||||
55 -15 25 -5 73 -29 73 -37 0 -5 4 -6 9 -3 12 8 41 -4 41 -16 0 -5 -4 -6 -10
|
||||
-3 -6 3 -7 -1 -4 -9 3 -9 1 -45 -5 -81 -6 -36 -14 -91 -17 -122 -4 -32 -11
|
||||
-61 -18 -65 -8 -6 -7 -8 2 -8 7 0 11 -4 8 -8 -3 -4 -8 -42 -11 -83 -4 -40 -9
|
||||
-81 -12 -89 -3 -8 -6 -44 -8 -80 -1 -36 -3 -65 -4 -65 -1 0 -3 -27 -4 -61 -3
|
||||
-66 0 -62 -92 -139 -33 -27 -62 -53 -63 -58 -2 -4 -8 -5 -13 -1 -5 3 -9 1 -9
|
||||
-3 0 -5 -41 -44 -91 -88 -50 -43 -98 -85 -106 -93 -13 -14 -23 -6 -91 64 -86
|
||||
90 -172 188 -186 213 -5 9 -12 18 -16 21 -12 9 -106 154 -139 215 -18 33 -37
|
||||
66 -43 72 -6 7 -8 20 -4 28 3 9 2 14 -3 11 -19 -12 -102 225 -105 296 0 27 -4
|
||||
48 -7 48 -16 0 9 108 32 142 22 31 125 81 151 74 10 -2 18 0 18 5 0 5 14 7 30
|
||||
5 17 -2 30 0 30 4 0 9 42 7 59 -4z m2823 1 c6 -9 8 -9 8 1 0 7 4 10 10 7 5 -3
|
||||
29 -8 52 -11 113 -13 201 -66 197 -118 0 -5 4 -12 10 -15 9 -6 11 -27 11 -124
|
||||
0 -16 -4 -26 -9 -23 -5 3 -7 -2 -4 -13 11 -41 -89 -307 -110 -294 -6 3 -7 1
|
||||
-3 -6 13 -20 -142 -281 -166 -281 -6 0 -8 -3 -5 -7 11 -10 -51 -84 -197 -238
|
||||
l-86 -89 -47 44 c-26 25 -52 50 -58 55 -7 6 -39 33 -71 61 -33 29 -86 73 -119
|
||||
100 -86 69 -87 71 -90 112 -5 69 -16 207 -21 242 -4 33 -10 92 -18 170 -4 37
|
||||
-14 114 -21 165 -2 17 -9 52 -14 80 -5 27 -9 50 -8 51 50 22 109 44 129 48 15
|
||||
2 36 10 48 16 12 6 28 9 35 6 8 -3 15 -1 17 5 2 5 19 11 39 13 20 2 40 6 45 9
|
||||
5 3 29 8 54 12 25 4 48 9 52 11 5 3 8 -1 8 -8 0 -9 2 -10 8 -2 9 16 43 21 61
|
||||
10 10 -7 12 -6 6 4 -6 10 -3 12 12 8 12 -3 25 -3 31 1 20 12 206 11 214 -2z
|
||||
m-1950 -189 c23 -13 46 -27 49 -31 3 -5 9 -8 13 -7 15 3 64 -25 59 -34 -3 -5
|
||||
-1 -6 5 -2 14 9 63 -14 55 -27 -3 -6 -2 -7 4 -4 5 3 56 -21 113 -53 57 -33
|
||||
107 -60 111 -60 4 0 11 -5 15 -12 5 -8 2 -9 -9 -5 -9 3 -16 2 -14 -2 1 -5 -48
|
||||
-42 -110 -83 -61 -40 -144 -95 -183 -123 -40 -27 -80 -54 -89 -60 -9 -5 -18
|
||||
-12 -21 -15 -17 -17 -80 -60 -80 -54 0 3 -9 -4 -20 -17 l-20 -24 6 105 c3 58
|
||||
7 121 9 140 2 19 5 52 6 72 1 20 5 36 8 34 3 -2 7 19 7 47 2 79 15 149 31 176
|
||||
8 13 10 20 4 17 -6 -4 -11 -2 -11 3 0 6 4 11 10 11 5 0 7 7 4 15 -8 20 -4 19
|
||||
48 -7z m1114 -66 c3 -26 8 -56 10 -67 10 -50 19 -145 19 -192 0 -28 3 -49 7
|
||||
-47 4 3 6 -16 5 -41 -1 -25 0 -45 3 -45 3 0 6 -32 7 -70 2 -39 -1 -70 -5 -70
|
||||
-5 0 -18 9 -30 20 -12 11 -26 20 -31 20 -5 0 -17 10 -25 22 -9 12 -16 19 -16
|
||||
15 0 -6 -92 56 -135 90 -25 20 -253 169 -278 182 -15 8 -26 15 -25 16 9 7 158
|
||||
95 184 108 17 9 34 13 38 10 3 -4 6 -1 6 5 0 11 175 102 197 102 7 0 13 4 13
|
||||
9 0 5 8 11 18 13 20 4 29 -14 38 -80z m-531 -268 c30 -20 55 -41 55 -45 0 -5
|
||||
6 -8 13 -6 6 1 11 -4 9 -11 -1 -8 2 -11 7 -7 14 8 82 -38 76 -52 -2 -7 2 -10
|
||||
11 -6 8 3 25 -3 37 -14 12 -11 35 -27 50 -37 16 -9 25 -22 21 -28 -4 -7 -3 -8
|
||||
4 -4 10 6 242 -148 250 -166 2 -5 9 -8 16 -8 7 0 21 -9 31 -20 17 -19 14 -24
|
||||
-10 -21 -5 1 -3 -4 5 -11 12 -10 15 -40 16 -138 0 -77 -4 -128 -10 -132 -7 -5
|
||||
-7 -8 -1 -8 12 0 14 -259 2 -277 -5 -7 -4 -13 1 -13 12 0 7 -79 -5 -90 -7 -7
|
||||
-244 -174 -287 -203 -10 -6 -24 -16 -30 -22 -12 -10 -34 -26 -211 -146 -96
|
||||
-65 -108 -71 -131 -60 -14 6 -22 16 -19 21 3 5 0 7 -7 5 -8 -3 -23 4 -35 15
|
||||
-12 11 -24 20 -27 20 -5 0 -246 166 -271 187 -5 4 -30 22 -55 38 -25 17 -49
|
||||
33 -55 38 -5 4 -44 31 -85 61 l-75 54 0 337 c1 317 2 338 19 351 11 7 23 11
|
||||
29 7 6 -3 7 -1 3 5 -4 7 3 17 18 24 14 6 26 15 26 20 0 4 7 8 15 8 8 0 15 5
|
||||
15 11 0 6 7 8 16 5 8 -3 13 -2 9 3 -3 5 12 20 32 32 21 13 41 27 44 32 4 5 12
|
||||
6 19 2 8 -5 11 -4 7 2 -9 15 38 44 60 37 12 -4 14 -3 6 3 -9 6 1 19 37 46 27
|
||||
20 54 37 59 37 5 0 11 3 13 8 5 12 131 95 144 95 7 0 12 4 11 8 -2 8 51 47 66
|
||||
48 4 1 32 -15 62 -35z m-822 -752 c2 -270 4 -262 -48 -215 -12 10 -41 34 -65
|
||||
53 -43 33 -83 67 -157 136 l-34 31 57 49 c177 153 219 185 228 176 4 -4 7 -2
|
||||
6 3 -4 17 0 29 6 23 3 -4 6 -119 7 -256z m1539 245 c7 -7 26 -20 41 -30 15
|
||||
-10 25 -23 21 -29 -4 -7 -3 -8 4 -4 7 4 12 2 12 -3 0 -6 6 -10 13 -8 6 1 11
|
||||
-4 9 -11 -1 -8 2 -11 7 -8 5 4 14 -2 20 -11 5 -10 13 -18 18 -18 4 0 7 -3 6
|
||||
-7 -2 -5 1 -7 5 -5 5 1 37 -22 71 -52 l62 -54 -53 -49 c-29 -27 -66 -59 -81
|
||||
-71 -16 -12 -34 -27 -41 -33 -43 -40 -129 -105 -133 -101 -2 3 0 14 6 25 7 13
|
||||
7 23 -2 32 -9 10 -9 14 1 17 6 3 9 9 6 14 -5 9 -8 289 -4 314 1 6 2 14 1 19
|
||||
-4 22 -8 86 -5 86 1 0 9 -6 16 -13z m-1862 -353 c48 -45 72 -65 109 -93 17
|
||||
-13 31 -30 31 -37 0 -8 3 -13 8 -13 21 4 32 -3 32 -18 0 -9 3 -14 6 -10 4 3
|
||||
13 -1 21 -9 8 -8 30 -26 49 -40 18 -15 31 -31 27 -37 -3 -5 -2 -7 4 -4 23 14
|
||||
43 -19 48 -78 10 -127 16 -186 40 -410 4 -33 10 -79 14 -102 5 -27 4 -46 -3
|
||||
-54 -8 -9 -8 -10 0 -6 7 4 14 -2 17 -13 3 -11 0 -20 -5 -20 -6 0 -5 -6 2 -15
|
||||
7 -8 10 -26 8 -40 -3 -13 -1 -22 3 -19 5 3 9 1 9 -5 0 -8 -35 -25 -50 -23 -3
|
||||
0 -14 -5 -25 -11 -11 -6 -22 -12 -25 -12 -3 -1 -36 -11 -75 -23 -61 -18 -120
|
||||
-33 -190 -48 -11 -2 -40 -7 -65 -10 -25 -3 -49 -8 -53 -11 -5 -3 -70 -6 -145
|
||||
-8 -122 -3 -168 0 -263 18 -72 14 -145 78 -155 136 -3 20 -8 44 -11 54 -2 9
|
||||
-1 17 4 17 4 0 8 17 7 37 -1 50 6 74 20 66 8 -4 8 -3 0 6 -12 14 33 164 47
|
||||
155 5 -3 6 2 3 10 -3 9 6 36 20 62 14 26 26 53 26 60 0 8 5 14 10 14 6 0 10 5
|
||||
10 11 0 22 93 168 103 162 6 -3 7 -2 4 4 -10 17 64 118 77 105 4 -3 5 0 2 8
|
||||
-4 9 12 35 39 65 25 28 73 80 106 117 34 38 65 65 70 62 5 -3 8 -2 7 3 -5 13
|
||||
26 42 37 35 6 -3 26 -21 45 -38z m2364 -103 c-1 -3 7 -12 18 -19 10 -7 15 -18
|
||||
12 -24 -4 -6 -2 -8 3 -5 6 4 21 -8 34 -25 13 -17 29 -36 34 -42 30 -33 63 -84
|
||||
58 -90 -4 -3 -1 -6 5 -6 17 0 85 -108 76 -121 -4 -7 -3 -9 3 -6 13 8 106 -169
|
||||
97 -185 -4 -6 -3 -8 3 -5 12 7 36 -50 28 -63 -3 -4 1 -10 9 -13 7 -3 17 -23
|
||||
21 -44 5 -21 10 -45 12 -53 2 -8 4 -24 6 -35 1 -11 6 -23 10 -26 5 -3 8 -36 8
|
||||
-73 0 -178 -108 -238 -417 -231 -78 2 -150 5 -161 8 -10 3 -34 8 -53 11 -19 3
|
||||
-48 8 -65 11 -114 22 -337 94 -329 106 3 5 -2 6 -12 2 -12 -5 -15 -2 -11 13 6
|
||||
20 16 78 22 129 2 17 6 46 9 65 3 19 8 60 11 90 3 30 8 69 11 87 2 17 7 62 10
|
||||
100 3 37 10 73 15 80 7 7 6 14 -1 18 -6 3 -7 12 -3 18 4 6 6 26 5 45 -2 25 1
|
||||
32 10 26 10 -6 10 -5 2 7 -16 21 -6 71 18 88 11 8 30 25 41 38 11 12 24 23 29
|
||||
23 4 0 25 16 45 36 63 60 110 94 118 86 5 -4 5 -2 2 4 -4 7 9 24 29 39 20 15
|
||||
49 40 66 57 l30 29 71 -72 c40 -39 71 -74 71 -78z m-1901 -294 c-3 -5 -2 -7 4
|
||||
-4 12 8 83 -39 83 -55 0 -6 3 -8 6 -5 7 7 136 -80 142 -95 2 -4 10 -8 18 -8 8
|
||||
0 14 -3 14 -8 0 -4 20 -18 45 -32 25 -14 45 -28 45 -32 0 -5 5 -8 10 -8 12 0
|
||||
124 -72 128 -82 2 -5 11 -8 20 -8 10 0 -36 -32 -101 -70 -65 -39 -124 -67
|
||||
-130 -64 -7 4 -8 3 -4 -2 5 -5 -41 -33 -110 -66 -80 -38 -120 -52 -125 -45 -3
|
||||
7 -9 32 -12 57 -3 25 -8 58 -11 74 -3 15 -8 49 -11 75 -3 25 -7 62 -9 81 -2
|
||||
19 -7 62 -10 94 -3 33 -8 64 -11 68 -3 4 0 8 6 8 6 0 8 5 4 11 -3 6 -8 43 -11
|
||||
82 -5 65 -4 70 11 58 9 -7 13 -18 9 -24z m1261 -64 c-4 -94 -9 -162 -18 -228
|
||||
-2 -16 -7 -55 -10 -85 -4 -30 -8 -62 -10 -70 -1 -8 -7 -37 -11 -65 -5 -27 -10
|
||||
-53 -11 -57 -1 -5 -2 -13 -3 -20 -1 -9 -4 -9 -13 0 -7 7 -20 12 -30 12 -10 0
|
||||
-18 5 -18 12 0 6 -3 9 -6 5 -4 -3 -54 19 -113 50 -58 31 -123 64 -143 75 -21
|
||||
10 -38 23 -38 29 0 6 -4 8 -9 5 -5 -3 -19 2 -32 12 -13 10 -37 24 -52 32 -25
|
||||
12 -26 15 -10 24 10 6 22 11 26 11 4 0 11 5 15 12 4 6 13 13 21 15 8 2 24 12
|
||||
36 23 13 12 36 27 52 35 15 8 35 21 43 28 8 7 18 12 23 12 4 0 16 8 26 18 42
|
||||
37 52 43 64 36 6 -4 9 -3 4 1 -9 11 43 46 56 38 6 -3 7 -2 4 4 -3 5 27 33 68
|
||||
62 41 28 77 56 80 61 12 20 13 8 9 -87z m-524 -403 c8 -5 30 -17 48 -26 17 -9
|
||||
32 -21 32 -26 0 -5 4 -7 9 -3 5 3 36 -11 68 -30 32 -19 63 -35 69 -35 5 0 21
|
||||
-10 34 -22 14 -13 25 -21 25 -18 1 8 154 -64 155 -73 0 -5 -4 -5 -10 -2 -6 4
|
||||
-7 -1 -3 -10 3 -10 0 -21 -8 -26 -11 -6 -11 -9 -1 -9 9 0 10 -5 4 -17 -5 -10
|
||||
-15 -40 -21 -68 -7 -27 -18 -63 -26 -80 -7 -16 -24 -58 -36 -92 -13 -34 -28
|
||||
-59 -33 -56 -5 3 -6 1 -3 -4 9 -14 -22 -75 -34 -68 -5 4 -6 -1 -3 -10 4 -9 -5
|
||||
-33 -20 -55 -14 -22 -26 -42 -26 -46 0 -3 -19 -33 -42 -67 -185 -267 -312
|
||||
-305 -471 -142 -56 56 -164 205 -150 205 4 0 3 4 -3 8 -14 8 -97 164 -109 202
|
||||
-4 14 -16 43 -26 65 -25 55 -76 216 -81 251 -2 23 1 29 14 28 10 -1 16 1 14 5
|
||||
-3 4 19 18 47 31 29 13 55 29 59 35 4 5 8 7 8 3 0 -4 23 6 50 22 28 16 50 26
|
||||
50 22 0 -4 4 -2 8 3 4 6 25 19 47 30 22 11 47 25 55 31 38 28 169 92 176 86 4
|
||||
-4 4 -2 1 5 -19 32 42 11 133 -47z"/>
|
||||
<path d="M2556 3192 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
|
||||
-9 -8z"/>
|
||||
<path d="M2455 2831 c-3 -5 -2 -12 3 -15 5 -3 9 1 9 9 0 17 -3 19 -12 6z"/>
|
||||
<path d="M2500 2790 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/>
|
||||
<path d="M2144 2592 c-70 -35 -108 -103 -100 -179 3 -38 32 -93 62 -118 9 -7
|
||||
1 -43 -31 -140 -65 -196 -66 -176 5 -173 33 2 60 -1 60 -5 0 -5 4 -6 8 -3 13
|
||||
8 227 10 239 3 18 -11 23 7 13 39 -6 16 -28 83 -49 149 l-39 120 29 27 c16 16
|
||||
29 30 29 33 0 22 1 26 8 22 4 -3 8 20 8 51 3 88 -33 145 -108 176 -44 19 -96
|
||||
18 -134 -2z"/>
|
||||
<path d="M1113 2105 c0 -8 4 -12 9 -9 5 3 6 10 3 15 -9 13 -12 11 -12 -6z"/>
|
||||
<path d="M859 1903 c-13 -16 -12 -17 4 -4 9 7 17 15 17 17 0 8 -8 3 -21 -13z"/>
|
||||
<path d="M1436 1803 c-6 -14 -5 -15 5 -6 7 7 10 15 7 18 -3 3 -9 -2 -12 -12z"/>
|
||||
<path d="M3760 1596 c0 -2 8 -10 18 -17 15 -13 16 -12 3 4 -13 16 -21 21 -21
|
||||
13z"/>
|
||||
<path d="M1616 1691 c-3 -5 2 -15 12 -22 15 -12 16 -12 5 2 -7 9 -10 19 -6 22
|
||||
3 4 4 7 0 7 -3 0 -8 -4 -11 -9z"/>
|
||||
<path d="M2710 1590 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0
|
||||
-4 -4 -4 -10z"/>
|
||||
<path d="M1090 831 c0 -6 4 -13 10 -16 6 -3 7 1 4 9 -7 18 -14 21 -14 7z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 13 KiB |
19
.storybook/static/favicon_package/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
37
.storybook/static/fonts/WorkSans/font.css
Normal file
@ -0,0 +1,37 @@
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
/*400*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-regular-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url("./worksans-medium-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url("./worksans-semibold-webfont.woff2") format("woff2");
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: 'Work Sans';
|
||||
font-style: normal;
|
||||
font-weight: bold;
|
||||
/*700*/
|
||||
font-display: swap;
|
||||
src: url("./worksans-bold-webfont.woff2") format("woff2");
|
||||
}
|
BIN
.storybook/static/fonts/WorkSans/worksans-bold-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-medium-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-regular-webfont.woff2
Normal file
BIN
.storybook/static/fonts/WorkSans/worksans-semibold-webfont.woff2
Normal file
BIN
.storybook/static/logo.png
Normal file
After Width: | Height: | Size: 102 KiB |
BIN
.storybook/static/preview.png
Normal file
After Width: | Height: | Size: 104 KiB |
121
README.md
@ -14,17 +14,22 @@
|
||||
<a href="https://github.com/garronej/keycloakify/blob/main/LICENSE">
|
||||
<img src="https://img.shields.io/npm/l/keycloakify">
|
||||
</a>
|
||||
<a href="https://github.com/InseeFrLab/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||
<a href="https://github.com/keycloakify/keycloakify/blob/729503fe31a155a823f46dd66ad4ff34ca274e0a/tsconfig.json#L14">
|
||||
<img src="https://camo.githubusercontent.com/0f9fcc0ac1b8617ad4989364f60f78b2d6b32985ad6a508f215f14d8f897b8d3/68747470733a2f2f62616467656e2e6e65742f62616467652f547970655363726970742f7374726963742532302546302539462539322541412f626c7565">
|
||||
</a>
|
||||
<a href="https://github.com/thomasdarimont/awesome-keycloak">
|
||||
<img src="https://awesome.re/mentioned-badge.svg"/>
|
||||
</a>
|
||||
<a href="https://discord.gg/rBzsYtUn">
|
||||
<img src="https://img.shields.io/discord/1097708346976505977"/>
|
||||
</a>
|
||||
<p align="center">
|
||||
<a href="https://www.keycloakify.dev">Home</a>
|
||||
-
|
||||
<a href="https://docs.keycloakify.dev">Documentation</a>
|
||||
-
|
||||
<a href="https://storybook.keycloakify.dev">Storybook</a>
|
||||
-
|
||||
<a href="https://github.com/codegouvfr/keycloakify-starter">Starter project</a>
|
||||
</p>
|
||||
</p>
|
||||
@ -34,15 +39,93 @@
|
||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||
</p>
|
||||
|
||||
> 🗣 V6 have been released 🎉
|
||||
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
|
||||
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
|
||||
## Sponsor 👼
|
||||
|
||||
We are exclusively sponsored by [Cloud IAM](https://www.cloud-iam.com), a French company offering Keycloak as a service.
|
||||
Their dedicated support helps us continue the development and maintenance of this project.
|
||||
|
||||
[Cloud IAM](https://www.cloud-iam.com/) provides the following services:
|
||||
|
||||
- Perfectly configured and optimized Keycloak IAM, ready in seconds.
|
||||
- Custom theme building for your brand using Keycloakify.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.cloud-iam.com/">
|
||||
<img src="https://user-images.githubusercontent.com/6702424/232165752-17134e68-4a55-4d6e-8672-e9132ecac5d5.svg" alt="Cloud IAM Logo" width="200"/>
|
||||
</a>
|
||||
<br/>
|
||||
<i>Use promo code <code>keycloakify5</code> </i>
|
||||
<br/>
|
||||
<i>5% of your annual subscription will be donated to us, and you'll get 5% off too.</i>
|
||||
</p>
|
||||
|
||||
Thank you, [Cloud IAM](https://www.cloud-iam.com/), for your support!
|
||||
|
||||
## Contributors ✨
|
||||
|
||||
Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->
|
||||
<!-- prettier-ignore-start -->
|
||||
<!-- markdownlint-disable -->
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lordvlad"><img src="https://avatars.githubusercontent.com/u/1217769?v=4?s=100" width="100px;" alt="Waldemar Reusch"/><br /><sub><b>Waldemar Reusch</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lordvlad" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://willwill96.github.io/the-ui-dawg-static-site/en/introduction/"><img src="https://avatars.githubusercontent.com/u/10997562?v=4?s=100" width="100px;" alt="William Will"/><br /><sub><b>William Will</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=willwill96" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Ann2827"><img src="https://avatars.githubusercontent.com/u/32645809?v=4?s=100" width="100px;" alt="Bystrova Ann"/><br /><sub><b>Bystrova Ann</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Ann2827" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkreuzmayr"><img src="https://avatars.githubusercontent.com/u/20108212?v=4?s=100" width="100px;" alt="Michael Kreuzmayr"/><br /><sub><b>Michael Kreuzmayr</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=mkreuzmayr" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://coolmathgames.tech"><img src="https://avatars.githubusercontent.com/u/6877780?v=4?s=100" width="100px;" alt="Mary "/><br /><sub><b>Mary </b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Mstrodl" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://tasyp.xyz/"><img src="https://avatars.githubusercontent.com/u/6623212?v=4?s=100" width="100px;" alt="German Öö"/><br /><sub><b>German Öö</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=Tasyp" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://revolunet.com"><img src="https://avatars.githubusercontent.com/u/124937?v=4?s=100" width="100px;" alt="Julien Bouquillon"/><br /><sub><b>Julien Bouquillon</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=revolunet" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aidangilmore"><img src="https://avatars.githubusercontent.com/u/32880357?v=4?s=100" width="100px;" alt="Aidan Gilmore"/><br /><sub><b>Aidan Gilmore</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=aidangilmore" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/0x-Void"><img src="https://avatars.githubusercontent.com/u/32745739?v=4?s=100" width="100px;" alt="Void"/><br /><sub><b>Void</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=0x-Void" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/juffe"><img src="https://avatars.githubusercontent.com/u/5393231?v=4?s=100" width="100px;" alt="juffe"/><br /><sub><b>juffe</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=juffe" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/lazToum"><img src="https://avatars.githubusercontent.com/u/4764837?v=4?s=100" width="100px;" alt="Lazaros Toumanidis"/><br /><sub><b>Lazaros Toumanidis</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=lazToum" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marcmrf"><img src="https://avatars.githubusercontent.com/u/9928519?v=4?s=100" width="100px;" alt="Marc"/><br /><sub><b>Marc</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=marcmrf" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://kasir-barati.github.io"><img src="https://avatars.githubusercontent.com/u/73785723?v=4?s=100" width="100px;" alt="Kasir Barati"/><br /><sub><b>Kasir Barati</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=kasir-barati" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/asashay"><img src="https://avatars.githubusercontent.com/u/10714670?v=4?s=100" width="100px;" alt="Alex Oliynyk"/><br /><sub><b>Alex Oliynyk</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=asashay" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.gravitysoftware.be"><img src="https://avatars.githubusercontent.com/u/1140574?v=4?s=100" width="100px;" alt="Thomas Silvestre"/><br /><sub><b>Thomas Silvestre</b></sub></a><br /><a href="https://github.com/keycloakify/keycloakify/commits?author=thosil" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- markdownlint-restore -->
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-LIST:END -->
|
||||
|
||||
# Changelog highlights
|
||||
|
||||
## 7.9
|
||||
|
||||
- Separate script for copying the default theme static assets to the public directory.
|
||||
Theses assets are only needed for testing your theme locally in Storybook or with a `mockPageId`.
|
||||
You are now expected to have a `"prepare": "copy-keycloak-resources-to-public",` in your package.json scripts.
|
||||
This script will create `public/keycloak-assets` when you run `yarn install` (If you are using another package manager
|
||||
like `pnpm` makes sure that `"prepare"` is actually ran.)
|
||||
[See the updated starter](https://github.com/keycloakify/keycloakify-starter/blob/94532fcf10bf8b19e0873be8575fd28a8958a806/package.json#L11).
|
||||
`public/keycloak-assets` shouldn't be tracked by GIT and is automatically ignored.
|
||||
|
||||
## 7.7
|
||||
|
||||
- Better storybook support, see [the starter project](https://github.com/keycloakify/keycloakify-starter).
|
||||
|
||||
## 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.
|
||||
- There is [a Storybook](https://storybook.keycloakify.dev)
|
||||
- [Remember me is fixed](https://github.com/keycloakify/keycloakify/pull/272)
|
||||
|
||||
## 6.13
|
||||
|
||||
- Build work behind corporate proxies, [see issue](https://github.com/InseeFrLab/keycloakify/issues/257).
|
||||
- Build work behind corporate proxies, [see issue](https://github.com/keycloakify/keycloakify/issues/257).
|
||||
|
||||
## 6.12
|
||||
|
||||
@ -55,13 +138,13 @@ Massive improvement in the developer experience:
|
||||
|
||||
## 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).
|
||||
- You no longer need to have Maven installed to build the theme. Thanks to @lordvlad, [see PR](https://github.com/keycloakify/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 🎉
|
||||
- Widows compat (thanks to @lordvlad, [see PR](https://github.com/keycloakify/keycloakify/pull/226)). WSL is no longer required 🎉
|
||||
|
||||
## 6.8.4
|
||||
|
||||
@ -71,19 +154,19 @@ Massive improvement in the developer experience:
|
||||
|
||||
- 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).
|
||||
example without having to switch to a full-component level customization. [See issue](https://github.com/keycloakify/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).
|
||||
- Add support for `webauthn-authenticate.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/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).
|
||||
- Add support for `login-password.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/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).
|
||||
- Add support for `login-username.ftl` thanks to [@mstrodl](https://github.com/Mstrodl)'s hacktoberfest [PR](https://github.com/keycloakify/keycloakify/pull/183).
|
||||
|
||||
## 6.4.0
|
||||
|
||||
@ -102,11 +185,11 @@ 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)
|
||||
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/keycloakify/keycloakify/issues/141)
|
||||
|
||||
## 5.7.0
|
||||
|
||||
- Feat `logout-confirm.ftl`. [PR](https://github.com/InseeFrLab/keycloakify/pull/120)
|
||||
- Feat `logout-confirm.ftl`. [PR](https://github.com/keycloakify/keycloakify/pull/120)
|
||||
|
||||
## 5.6.4
|
||||
|
||||
@ -114,7 +197,7 @@ Fix `login-verify-email.ftl` page. [Before](https://user-images.githubuserconten
|
||||
|
||||
## v5.6.0
|
||||
|
||||
Add support for `login-config-totp.ftl` page [#127](https://github.com/InseeFrLab/keycloakify/pull/127).
|
||||
Add support for `login-config-totp.ftl` page [#127](https://github.com/keycloakify/keycloakify/pull/127).
|
||||
|
||||
## v5.3.0
|
||||
|
||||
@ -129,7 +212,7 @@ Import of terms and services have changed. [See example](https://github.com/garr
|
||||
|
||||
## v4.10.0
|
||||
|
||||
Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycloakify/pull/92).
|
||||
Add `login-idp-link-email.ftl` page [See PR](https://github.com/keycloakify/keycloakify/pull/92).
|
||||
|
||||
## v4.8.0
|
||||
|
||||
@ -142,7 +225,7 @@ Add `login-idp-link-email.ftl` page [See PR](https://github.com/InseeFrLab/keycl
|
||||
## v4.7.2
|
||||
|
||||
> WARNING: This is broken.
|
||||
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
|
||||
> Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/keycloakify/keycloakify/issues/43#issuecomment-975699658).
|
||||
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||
|
||||
## v4.7.0
|
||||
@ -176,12 +259,12 @@ Change [this](https://github.com/garronej/keycloakify-demo-app/blob/df664c13c77c
|
||||
|
||||
No breaking changes except that `@emotion/react`, [`tss-react`](https://www.npmjs.com/package/tss-react) and [`powerhooks`](https://www.npmjs.com/package/powerhooks) are now `peerDependencies` instead of being just dependencies.
|
||||
It's important to avoid problem when using `keycloakify` alongside [`mui`](https://mui.com) and
|
||||
[when passing params from the app to the login page](https://github.com/InseeFrLab/keycloakify#implement-context-persistence-optional).
|
||||
[when passing params from the app to the login page](https://github.com/keycloakify/keycloakify#implement-context-persistence-optional).
|
||||
|
||||
## v2.5
|
||||
|
||||
- Feature [Use advanced message](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||
and [`messagesPerFields`](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/InseeFrLab/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
||||
- Feature [Use advanced message](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/i18n/useKcMessage.tsx#L53-L66)
|
||||
and [`messagesPerFields`](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/lib/getKcContext/KcContextBase.ts#L70-L75) (implementation [here](https://github.com/keycloakify/keycloakify/blob/59f106bf9e210b63b190826da2bf5f75fc8b7644/src/bin/build-keycloak-theme/generateFtl/common.ftl#L130-L189))
|
||||
- Test container now uses Keycloak version `15.0.2`.
|
||||
|
||||
## v2
|
||||
|
91
keycloakify-json-schema.json
Normal file
@ -0,0 +1,91 @@
|
||||
{
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://json.schemastore.org/package.json"
|
||||
},
|
||||
{
|
||||
"$ref": "keycloakifyPackageJsonSchema"
|
||||
}
|
||||
],
|
||||
"$ref": "#/definitions/keycloakifyPackageJsonSchema",
|
||||
"definitions": {
|
||||
"keycloakifyPackageJsonSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string"
|
||||
},
|
||||
"keycloakify": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"extraPages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extraLoginPages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extraAccountPages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"extraThemeProperties": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"areAppAndKeycloakServerSharingSameDomain": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"artifactId": {
|
||||
"type": "string"
|
||||
},
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"bundler": {
|
||||
"type": "string",
|
||||
"enum": ["mvn", "keycloakify", "none"]
|
||||
},
|
||||
"keycloakVersionDefaultAssets": {
|
||||
"type": "string"
|
||||
},
|
||||
"reactAppBuildDirPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"keycloakifyBuildDirPath": {
|
||||
"type": "string"
|
||||
},
|
||||
"customUserAttributes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"themeName": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"required": ["name", "version"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
73
package.json
@ -1,32 +1,39 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "6.13.2",
|
||||
"version": "7.10.0",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/garronej/keycloakify.git"
|
||||
"url": "git://github.com/keycloakify/keycloakify.git"
|
||||
},
|
||||
"main": "dist/lib/index.js",
|
||||
"types": "dist/lib/index.d.ts",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src/lib && yarn grant-exec-perms && yarn copy-files dist/",
|
||||
"build:test": "rimraf dist_test/ && tsc -p src/test && yarn copy-files dist_test/",
|
||||
"prepare": "yarn generate-i18n-messages",
|
||||
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/",
|
||||
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
|
||||
"generate:json-schema": "ts-node scripts/generate-json-schema.ts",
|
||||
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
|
||||
"copy-files": "copyfiles -u 1 src/**/*.ftl",
|
||||
"pretest": "yarn build:test",
|
||||
"test": "node dist_test/test/bin && node dist_test/test/lib",
|
||||
"test": "yarn test:types && vitest run",
|
||||
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
|
||||
"test:types": "tsc -p test/tsconfig.json --noEmit",
|
||||
"_format": "prettier '**/*.{ts,tsx,json,md}'",
|
||||
"format": "yarn _format --write",
|
||||
"format:check": "yarn _format --list-different",
|
||||
"generate-messages": "ts-node --skipProject src/scripts/generate-i18n-messages.ts",
|
||||
"link-in-app": "ts-node --skipProject src/scripts/link-in-app.ts",
|
||||
"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 "
|
||||
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
|
||||
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
|
||||
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"
|
||||
},
|
||||
"bin": {
|
||||
"keycloakify": "dist/bin/keycloakify/index.js",
|
||||
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
|
||||
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
|
||||
"copy-keycloak-resources-to-public": "dist/bin/copy-keycloak-resources-to-public.js",
|
||||
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js",
|
||||
"eject-keycloak-page": "dist/bin/eject-keycloak-page.js",
|
||||
"initialize-email-theme": "dist/bin/initialize-email-theme.js",
|
||||
"keycloakify": "dist/bin/keycloakify/index.js"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,json,md}": [
|
||||
@ -42,9 +49,9 @@
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"src/",
|
||||
"!src/scripts",
|
||||
"dist/",
|
||||
"!dist/tsconfig.tsbuildinfo"
|
||||
"!dist/tsconfig.tsbuildinfo",
|
||||
"!dist/bin/tsconfig.tsbuildinfo"
|
||||
],
|
||||
"keywords": [
|
||||
"bluehats",
|
||||
@ -56,29 +63,51 @@
|
||||
"login",
|
||||
"register"
|
||||
],
|
||||
"homepage": "https://github.com/garronej/keycloakify",
|
||||
"homepage": "https://www.keycloakify.dev",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@storybook/addon-a11y": "^6.5.16",
|
||||
"@storybook/addon-actions": "^6.5.13",
|
||||
"@storybook/addon-essentials": "^6.5.13",
|
||||
"@storybook/addon-interactions": "^6.5.13",
|
||||
"@storybook/addon-links": "^6.5.13",
|
||||
"@storybook/builder-webpack5": "^6.5.13",
|
||||
"@storybook/manager-webpack5": "^6.5.13",
|
||||
"@storybook/react": "^6.5.13",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@types/make-fetch-happen": "^10.0.1",
|
||||
"@types/minimist": "^1.2.2",
|
||||
"@types/node": "^18.14.1",
|
||||
"@types/react": "18.0.9",
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/react": "^18.0.35",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/yauzl": "^2.10.0",
|
||||
"concurrently": "^8.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint-plugin-storybook": "^0.6.7",
|
||||
"husky": "^4.3.8",
|
||||
"lint-staged": "^11.0.0",
|
||||
"powerhooks": "^0.26.7",
|
||||
"prettier": "^2.3.0",
|
||||
"properties-parser": "^0.3.1",
|
||||
"react": "18.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"scripting-tools": "^0.19.13",
|
||||
"storybook-dark-mode": "^1.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.5"
|
||||
"tsc-alias": "^1.8.3",
|
||||
"tss-react": "^4.8.2",
|
||||
"typescript": "^4.9.1-beta",
|
||||
"vitest": "^0.29.8",
|
||||
"zod-to-json-schema": "^3.20.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/rest": "^18.12.0",
|
||||
"@types/yazl": "^2.4.2",
|
||||
"cheerio": "^1.0.0-rc.5",
|
||||
"cli-select": "^1.1.2",
|
||||
"evt": "^2.4.18",
|
||||
@ -89,6 +118,8 @@
|
||||
"react-markdown": "^5.0.3",
|
||||
"rfc4648": "^1.5.2",
|
||||
"tsafe": "^1.6.0",
|
||||
"yauzl": "^2.10.0",
|
||||
"yazl": "^2.5.1",
|
||||
"zod": "^3.17.10"
|
||||
}
|
||||
}
|
||||
|
122
scripts/generate-i18n-messages.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative, dirname as pathDirname, sep as pathSep } 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 { 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 = true;
|
||||
|
||||
const logger = getLogger({ isSilent });
|
||||
|
||||
async function main() {
|
||||
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");
|
||||
const re = new RegExp(`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`);
|
||||
|
||||
crawl(baseThemeDirPath).forEach(filePath => {
|
||||
const match = filePath.match(re);
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
14
scripts/generate-json-schema.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import zodToJsonSchema from "zod-to-json-schema";
|
||||
import { zParsedPackageJson } from "../src/bin/keycloakify/parsedPackageJson";
|
||||
|
||||
const jsonSchemaName = "keycloakifyPackageJsonSchema";
|
||||
const jsonSchema = zodToJsonSchema(zParsedPackageJson, jsonSchemaName);
|
||||
|
||||
const baseProperties = {
|
||||
// merges package.json schema with keycloakify properties
|
||||
"allOf": [{ "$ref": "https://json.schemastore.org/package.json" }, { "$ref": jsonSchemaName }]
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(process.cwd(), "keycloakify-json-schema.json"), JSON.stringify({ ...baseProperties, ...jsonSchema }, null, 2));
|
@ -1,10 +1,11 @@
|
||||
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 = pathJoin(__dirname, "..", "..");
|
||||
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(
|
||||
@ -36,6 +37,8 @@ fs.writeFileSync(
|
||||
)
|
||||
);
|
||||
|
||||
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 ...)
|
29
scripts/test-keycloakify-starter.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { execSync } from "child_process";
|
||||
import { existsSync, readFileSync, rmSync, writeFileSync } from "fs";
|
||||
import path from "path";
|
||||
|
||||
const testDir = "keycloakify_starter_test";
|
||||
|
||||
if (existsSync(path.join(process.cwd(), testDir))) {
|
||||
rmSync(path.join(process.cwd(), testDir), { recursive: true });
|
||||
}
|
||||
// Build and link package
|
||||
execSync("yarn build");
|
||||
const pkgJSON = JSON.parse(readFileSync(path.join(process.cwd(), "package.json")).toString("utf8"));
|
||||
pkgJSON.main = "./index.js";
|
||||
pkgJSON.types = "./index.d.ts";
|
||||
pkgJSON.scripts.prepare = undefined;
|
||||
writeFileSync(path.join(process.cwd(), "dist", "package.json"), JSON.stringify(pkgJSON));
|
||||
// Wrapped in a try/catch because unlink errors if the package isn't linked
|
||||
try {
|
||||
execSync("yarn unlink");
|
||||
} catch {}
|
||||
execSync("yarn link", { "cwd": path.join(process.cwd(), "dist") });
|
||||
|
||||
// Clone latest keycloakify-starter and link to keycloakify output
|
||||
execSync(`git clone https://github.com/keycloakify/keycloakify-starter.git ${testDir}`);
|
||||
execSync("yarn install", { "cwd": path.join(process.cwd(), testDir) });
|
||||
execSync("yarn link keycloakify", { "cwd": path.join(process.cwd(), testDir) });
|
||||
|
||||
//Ensure keycloak theme can be built
|
||||
execSync("yarn build-keycloak-theme", { "cwd": path.join(process.cwd(), testDir) });
|
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>
|
||||
);
|
||||
}
|
131
src/account/Template.tsx
Normal file
@ -0,0 +1,131 @@
|
||||
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">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<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 && (
|
||||
<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
@ -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";
|
@ -1,10 +1,11 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
//NOTE for later: https://github.com/remarkjs/react-markdown/blob/236182ecf30bd89c1e5a7652acaf8d0bf81e6170/src/renderers.js#L7-L35
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import type baseMessages from "./generated_messages/18.0.1/login/en";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import fallbackMessages from "./baseMessages/en";
|
||||
import { getMessages } from "./baseMessages";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { KcContextBase } from "../getKcContext/KcContextBase";
|
||||
import { Markdown } from "../tools/Markdown";
|
||||
import type { KcContext } from "../kcContext/KcContext";
|
||||
import { Markdown } from "keycloakify/tools/Markdown";
|
||||
|
||||
export const fallbackLanguageTag = "en";
|
||||
|
||||
@ -15,11 +16,11 @@ export type KcContextLike = {
|
||||
};
|
||||
};
|
||||
|
||||
assert<KcContextBase extends KcContextLike ? true : false>();
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export type MessageKeyBase = keyof typeof baseMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
|
||||
|
||||
export type I18n<MessageKey extends string> = {
|
||||
export type GenericI18n<MessageKey extends string> = {
|
||||
/**
|
||||
* e.g: "en", "fr", "zh-CN"
|
||||
*
|
||||
@ -67,136 +68,72 @@ export type I18n<MessageKey extends string> = {
|
||||
advancedMsgStr: (key: string, ...args: (string | undefined)[]) => string;
|
||||
};
|
||||
|
||||
export type I18nBase = I18n<MessageKeyBase>;
|
||||
export type I18n = GenericI18n<MessageKey>;
|
||||
|
||||
export function __unsafe_useI18n<ExtraMessageKey extends string = never>(params: {
|
||||
kcContext: KcContextLike;
|
||||
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
||||
doSkip: boolean;
|
||||
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
||||
const { kcContext, extraMessages, doSkip } = params;
|
||||
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<I18n<ExtraMessageKey | MessageKeyBase> | undefined>(undefined);
|
||||
const [i18n, setI18n] = useState<GenericI18n<ExtraMessageKey | MessageKey> | undefined>(undefined);
|
||||
|
||||
const refHasStartedFetching = useRef(false);
|
||||
const refHasStartedFetching = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (doSkip || refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (refHasStartedFetching.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
refHasStartedFetching.current = true;
|
||||
refHasStartedFetching.current = true;
|
||||
|
||||
(async () => {
|
||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||
(async () => {
|
||||
const { currentLanguageTag = fallbackLanguageTag } = kcContext.locale ?? {};
|
||||
|
||||
const [fallbackMessages, messages] = await Promise.all([
|
||||
import("./generated_messages/18.0.1/login/en"),
|
||||
(() => {
|
||||
switch (currentLanguageTag) {
|
||||
case "ca":
|
||||
return import("./generated_messages/18.0.1/login/ca");
|
||||
case "cs":
|
||||
return import("./generated_messages/18.0.1/login/cs");
|
||||
case "da":
|
||||
return import("./generated_messages/18.0.1/login/da");
|
||||
case "de":
|
||||
return import("./generated_messages/18.0.1/login/de");
|
||||
case "en":
|
||||
return import("./generated_messages/18.0.1/login/en");
|
||||
case "es":
|
||||
return import("./generated_messages/18.0.1/login/es");
|
||||
case "fi":
|
||||
return import("./generated_messages/18.0.1/login/fi");
|
||||
case "fr":
|
||||
return import("./generated_messages/18.0.1/login/fr");
|
||||
case "hu":
|
||||
return import("./generated_messages/18.0.1/login/hu");
|
||||
case "it":
|
||||
return import("./generated_messages/18.0.1/login/it");
|
||||
case "ja":
|
||||
return import("./generated_messages/18.0.1/login/ja");
|
||||
case "lt":
|
||||
return import("./generated_messages/18.0.1/login/lt");
|
||||
case "lv":
|
||||
return import("./generated_messages/18.0.1/login/lv");
|
||||
case "nl":
|
||||
return import("./generated_messages/18.0.1/login/nl");
|
||||
case "no":
|
||||
return import("./generated_messages/18.0.1/login/no");
|
||||
case "pl":
|
||||
return import("./generated_messages/18.0.1/login/pl");
|
||||
case "pt-BR":
|
||||
return import("./generated_messages/18.0.1/login/pt-BR");
|
||||
case "ru":
|
||||
return import("./generated_messages/18.0.1/login/ru");
|
||||
case "sk":
|
||||
return import("./generated_messages/18.0.1/login/sk");
|
||||
case "sv":
|
||||
return import("./generated_messages/18.0.1/login/sv");
|
||||
case "tr":
|
||||
return import("./generated_messages/18.0.1/login/tr");
|
||||
case "zh-CN":
|
||||
return import("./generated_messages/18.0.1/login/zh-CN");
|
||||
default:
|
||||
return { "default": {} };
|
||||
}
|
||||
})()
|
||||
]).then(modules => modules.map(module => module.default));
|
||||
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;
|
||||
|
||||
setI18n({
|
||||
...createI18nTranslationFunctions({
|
||||
"fallbackMessages": {
|
||||
...fallbackMessages,
|
||||
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
|
||||
...(extraMessages[fallbackLanguageTag] ?? {})
|
||||
} as any,
|
||||
"messages": {
|
||||
...messages,
|
||||
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
|
||||
...(extraMessages[currentLanguageTag] ?? {})
|
||||
} as any
|
||||
}),
|
||||
currentLanguageTag,
|
||||
"changeLocale": newLanguageTag => {
|
||||
const { locale } = kcContext;
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
|
||||
assert(locale !== undefined, "Internationalization not enabled");
|
||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||
|
||||
const targetSupportedLocale = locale.supported.find(({ languageTag }) => languageTag === newLanguageTag);
|
||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||
|
||||
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
|
||||
window.location.href = targetSupportedLocale.url;
|
||||
|
||||
window.location.href = targetSupportedLocale.url;
|
||||
assert(false, "never");
|
||||
},
|
||||
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||
)
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
|
||||
assert(false, "never");
|
||||
},
|
||||
"labelBySupportedLanguageTag": Object.fromEntries(
|
||||
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])
|
||||
)
|
||||
});
|
||||
})();
|
||||
}, []);
|
||||
return i18n ?? null;
|
||||
}
|
||||
|
||||
return i18n ?? null;
|
||||
}
|
||||
|
||||
const useI18n_private = __unsafe_useI18n;
|
||||
|
||||
export function useI18n<ExtraMessageKey extends string = never>(params: {
|
||||
kcContext: KcContextLike;
|
||||
extraMessages: { [languageTag: string]: { [key in ExtraMessageKey]: string } };
|
||||
}): I18n<MessageKeyBase | ExtraMessageKey> | null {
|
||||
return useI18n_private({
|
||||
...params,
|
||||
"doSkip": false
|
||||
});
|
||||
return { useI18n };
|
||||
}
|
||||
|
||||
function createI18nTranslationFunctions<MessageKey extends string>(params: {
|
||||
fallbackMessages: Record<MessageKey, string>;
|
||||
messages: Record<MessageKey, string>;
|
||||
}): Pick<I18n<MessageKey>, "msg" | "msgStr" | "advancedMsg" | "advancedMsgStr"> {
|
||||
}): 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 {
|
1
src/account/i18n/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export type { I18n } from "./i18n";
|
9
src/account/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import Fallback from "keycloakify/account/Fallback";
|
||||
|
||||
export default Fallback;
|
||||
|
||||
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
|
||||
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
|
||||
export { createUseI18n } from "keycloakify/account/i18n/i18n";
|
||||
|
||||
export type { PageProps } from "keycloakify/account/pages/PageProps";
|
87
src/account/kcContext/KcContext.ts
Normal file
@ -0,0 +1,87 @@
|
||||
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 = {
|
||||
keycloakifyVersion: string;
|
||||
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;
|
||||
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
|
||||
referrerURI?: string;
|
||||
getLogoutUrl: () => string;
|
||||
};
|
||||
features: {
|
||||
passwordUpdateSupported: boolean;
|
||||
identityFederation: boolean;
|
||||
log: boolean;
|
||||
authorization: boolean;
|
||||
};
|
||||
realm: {
|
||||
internationalizationEnabled: boolean;
|
||||
userManagedAccessAllowed: boolean;
|
||||
};
|
||||
// Present only if redirected to account page with ?referrer=xxx&referrer_uri=http...
|
||||
message?: {
|
||||
type: "success" | "warning" | "error" | "info";
|
||||
summary: string;
|
||||
};
|
||||
referrer?: {
|
||||
url: string; // The url of the App
|
||||
name: string; // Client id
|
||||
};
|
||||
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;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
|
||||
export type Account = Common & {
|
||||
pageId: "account.ftl";
|
||||
url: {
|
||||
accountUrl: string;
|
||||
};
|
||||
realm: {
|
||||
registrationEmailAsUsername: boolean;
|
||||
editUsernameAllowed: boolean;
|
||||
};
|
||||
stateChecker: string;
|
||||
};
|
||||
}
|
||||
|
||||
assert<Equals<KcContext["pageId"], AccountThemePageId>>();
|
104
src/account/kcContext/createGetKcContext.ts
Normal file
@ -0,0 +1,104 @@
|
||||
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 { resourcesCommonDirPathRelativeToPublicDir } from "keycloakify/bin/mockTestingResourcesPath";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
import { kcContextMocks, kcContextCommonMock } from "keycloakify/account/kcContext/kcContextMocks";
|
||||
import { id } from "tsafe/id";
|
||||
import { accountThemePageIds } from "keycloakify/bin/keycloakify/generateFtl/pageId";
|
||||
|
||||
export function createGetKcContext<KcContextExtension extends { pageId: string } = never>(params?: {
|
||||
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
|
||||
}) {
|
||||
const { mockData } = params ?? {};
|
||||
|
||||
function getKcContext<PageId extends ExtendKcContext<KcContextExtension>["pageId"] | undefined = undefined>(params?: {
|
||||
mockPageId?: PageId;
|
||||
storyPartialKcContext?: DeepPartial<Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>>;
|
||||
}): {
|
||||
kcContext: PageId extends undefined
|
||||
? ExtendKcContext<KcContextExtension> | undefined
|
||||
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
|
||||
} {
|
||||
const { mockPageId, storyPartialKcContext } = 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}.`, "background: red; color: yellow; font-size: medium");
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
const partialKcContextCustomMock = (() => {
|
||||
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
|
||||
|
||||
const mockDataPick = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
if (mockDataPick !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": mockDataPick
|
||||
});
|
||||
}
|
||||
|
||||
if (storyPartialKcContext !== undefined) {
|
||||
deepAssign({
|
||||
"target": out,
|
||||
"source": storyPartialKcContext
|
||||
});
|
||||
}
|
||||
|
||||
return Object.keys(out).length === 0 ? undefined : out;
|
||||
})();
|
||||
|
||||
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 as any };
|
||||
}
|
||||
|
||||
if (id<readonly string[]>(accountThemePageIds).indexOf(realKcContext.pageId) < 0 && !("account" in realKcContext)) {
|
||||
return { "kcContext": undefined as any };
|
||||
}
|
||||
|
||||
{
|
||||
const { url } = realKcContext;
|
||||
|
||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(resourcesCommonDirPathRelativeToPublicDir));
|
||||
}
|
||||
|
||||
return { "kcContext": realKcContext as any };
|
||||
}
|
||||
|
||||
return { getKcContext };
|
||||
}
|
21
src/account/kcContext/getKcContext.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
|
||||
import type { ExtendKcContext } from "./getKcContextFromWindow";
|
||||
import { createGetKcContext } from "./createGetKcContext";
|
||||
|
||||
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
|
||||
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
|
||||
*/
|
||||
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 { getKcContext } = createGetKcContext({
|
||||
mockData
|
||||
});
|
||||
|
||||
const { kcContext } = getKcContext({ mockPageId });
|
||||
|
||||
return { kcContext };
|
||||
}
|
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
@ -0,0 +1 @@
|
||||
export type { KcContext } from "./KcContext";
|
176
src/account/kcContext/kcContextMocks.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import "minimal-polyfills/Object.fromEntries";
|
||||
import { resourcesCommonDirPathRelativeToPublicDir, resourcesDirPathRelativeToPublicDir } 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 = {
|
||||
"keycloakifyVersion": "0.0.0",
|
||||
"url": {
|
||||
"resourcesPath": pathJoin(PUBLIC_URL, resourcesDirPathRelativeToPublicDir),
|
||||
"resourcesCommonPath": pathJoin(PUBLIC_URL, resourcesCommonDirPathRelativeToPublicDir),
|
||||
"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
|
||||
},
|
||||
"stateChecker": "state checker"
|
||||
}),
|
||||
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
@ -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"
|
||||
}
|
||||
});
|
133
src/account/pages/Account.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
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 Account(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, referrer } = 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>
|
||||
{referrer !== undefined && <a href={referrer?.url}>{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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
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
@ -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 Password(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, stateChecker } = 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>
|
||||
);
|
||||
}
|
48
src/bin/copy-keycloak-resources-to-public.ts
Normal file
@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { downloadKeycloakStaticResources } from "./keycloakify/generateTheme/downloadKeycloakStaticResources";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { basenameOfKeycloakDirInPublicDir } from "./mockTestingResourcesPath";
|
||||
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||
import { themeTypes } from "./keycloakify/generateFtl";
|
||||
import * as fs from "fs";
|
||||
|
||||
(async () => {
|
||||
const projectDirPath = process.cwd();
|
||||
|
||||
const buildOptions = readBuildOptions({
|
||||
"processArgv": process.argv.slice(2),
|
||||
"projectDirPath": process.cwd()
|
||||
});
|
||||
|
||||
const keycloakDirInPublicDir = pathJoin(process.env["PUBLIC_DIR_PATH"] || pathJoin(projectDirPath, "public"), basenameOfKeycloakDirInPublicDir);
|
||||
|
||||
if (fs.existsSync(keycloakDirInPublicDir)) {
|
||||
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const themeType of themeTypes) {
|
||||
await downloadKeycloakStaticResources({
|
||||
"isSilent": false,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
"themeType": themeType,
|
||||
"themeDirPath": keycloakDirInPublicDir
|
||||
});
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakDirInPublicDir, "README.txt"),
|
||||
Buffer.from(
|
||||
// prettier-ignore
|
||||
[
|
||||
"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(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
|
||||
console.log(`${pathRelative(projectDirPath, keycloakDirInPublicDir)} directory created.`);
|
||||
})();
|
@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||
import { keycloakThemeEmailDirPath } from "./keycloakify";
|
||||
import { join as pathJoin, basename as pathBasename } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||
import * as fs from "fs";
|
||||
import { getCliOptions } from "./tools/cliOptions";
|
||||
import { getLogger } from "./tools/logger";
|
||||
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||
const logger = getLogger({ isSilent });
|
||||
if (fs.existsSync(keycloakThemeEmailDirPath)) {
|
||||
logger.warn(`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,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||
"destDirPath": keycloakThemeEmailDirPath
|
||||
});
|
||||
|
||||
logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
|
||||
|
||||
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||
})();
|
||||
}
|
@ -1,40 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { keycloakThemeBuildingDirPath } from "./keycloakify";
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadAndUnzip } from "./tools/downloadAndUnzip";
|
||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||
import { getCliOptions } from "./tools/cliOptions";
|
||||
import { getLogger } from "./tools/logger";
|
||||
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||
|
||||
export async function downloadBuiltinKeycloakTheme(params: { keycloakVersion: string; destDirPath: string; isSilent: boolean }) {
|
||||
const { keycloakVersion, destDirPath, isSilent } = params;
|
||||
const { keycloakVersion, destDirPath } = params;
|
||||
|
||||
for (const ext of ["", "-community"]) {
|
||||
await downloadAndUnzip({
|
||||
"destDirPath": destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`,
|
||||
"cacheDirPath": pathJoin(keycloakThemeBuildingDirPath, ".cache"),
|
||||
isSilent
|
||||
});
|
||||
}
|
||||
await Promise.all(
|
||||
["", "-community"].map(ext =>
|
||||
downloadAndUnzip({
|
||||
"destDirPath": destDirPath,
|
||||
"url": `https://github.com/keycloak/keycloak/archive/refs/tags/${keycloakVersion}.zip`,
|
||||
"pathOfDirToExtractInArchive": `keycloak-${keycloakVersion}/themes/src/main/resources${ext}/theme`
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const buildOptions = readBuildOptions({
|
||||
"projectDirPath": process.cwd(),
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
|
||||
const logger = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
const { keycloakVersion } = await promptKeycloakVersion();
|
||||
|
||||
const destDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources", "theme");
|
||||
|
||||
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
destDirPath,
|
||||
"isSilent": buildOptions.isSilent
|
||||
});
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
(async () => {
|
||||
const { isSilent } = getCliOptions(process.argv.slice(2));
|
||||
const logger = getLogger({ isSilent });
|
||||
const { keycloakVersion } = await promptKeycloakVersion();
|
||||
|
||||
const destDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme");
|
||||
|
||||
logger.log(`Downloading builtins theme of Keycloak ${keycloakVersion} here ${destDirPath}`);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
destDirPath,
|
||||
isSilent
|
||||
});
|
||||
})();
|
||||
main();
|
||||
}
|
||||
|
69
src/bin/eject-keycloak-page.ts
Normal file
@ -0,0 +1,69 @@
|
||||
#!/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";
|
||||
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";
|
||||
import { getThemeSrcDirPath } from "./getSrcDirPath";
|
||||
|
||||
(async () => {
|
||||
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 { themeSrcDirPath } = getThemeSrcDirPath({ "projectDirPath": process.cwd() });
|
||||
|
||||
if (themeSrcDirPath === undefined) {
|
||||
throw new Error("Couldn't locate your theme sources");
|
||||
}
|
||||
|
||||
const targetFilePath = pathJoin(themeSrcDirPath, themeType, "pages", pageBasename);
|
||||
|
||||
if (existsSync(targetFilePath)) {
|
||||
console.log(`${pageId} is already ejected, ${pathRelative(process.cwd(), targetFilePath)} already exists`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
await writeFile(targetFilePath, await readFile(pathJoin(getProjectRoot(), "src", themeType, "pages", pageBasename)));
|
||||
|
||||
console.log(`${pathRelative(process.cwd(), targetFilePath)} created`);
|
||||
})();
|
43
src/bin/getSrcDirPath.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import * as fs from "fs";
|
||||
import { exclude } from "tsafe";
|
||||
import { crawl } from "./tools/crawl";
|
||||
import { join as pathJoin } from "path";
|
||||
|
||||
const themeSrcDirBasename = "keycloak-theme";
|
||||
|
||||
export function getThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||
const { projectDirPath } = params;
|
||||
|
||||
const srcDirPath = pathJoin(projectDirPath, "src");
|
||||
|
||||
const themeSrcDirPath: string | undefined = crawl(srcDirPath)
|
||||
.map(fileRelativePath => {
|
||||
const split = fileRelativePath.split(themeSrcDirBasename);
|
||||
|
||||
if (split.length !== 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return pathJoin(srcDirPath, split[0] + themeSrcDirBasename);
|
||||
})
|
||||
.filter(exclude(undefined))[0];
|
||||
|
||||
if (themeSrcDirPath === undefined) {
|
||||
if (fs.existsSync(pathJoin(srcDirPath, "login")) || fs.existsSync(pathJoin(srcDirPath, "account"))) {
|
||||
return { "themeSrcDirPath": srcDirPath };
|
||||
}
|
||||
return { "themeSrcDirPath": undefined };
|
||||
}
|
||||
|
||||
return { themeSrcDirPath };
|
||||
}
|
||||
|
||||
export function getEmailThemeSrcDirPath(params: { projectDirPath: string }) {
|
||||
const { projectDirPath } = params;
|
||||
|
||||
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
|
||||
|
||||
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
|
||||
|
||||
return { emailThemeSrcDirPath };
|
||||
}
|
64
src/bin/initialize-email-theme.ts
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import { transformCodebase } from "./tools/transformCodebase";
|
||||
import { promptKeycloakVersion } from "./promptKeycloakVersion";
|
||||
import { readBuildOptions } from "./keycloakify/BuildOptions";
|
||||
import * as fs from "fs";
|
||||
import { getLogger } from "./tools/logger";
|
||||
import { getEmailThemeSrcDirPath } from "./getSrcDirPath";
|
||||
|
||||
export async function main() {
|
||||
const { isSilent } = readBuildOptions({
|
||||
"projectDirPath": process.cwd(),
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
|
||||
const logger = getLogger({ isSilent });
|
||||
|
||||
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({
|
||||
"projectDirPath": process.cwd()
|
||||
});
|
||||
|
||||
if (emailThemeSrcDirPath === undefined) {
|
||||
logger.warn("Couldn't locate your theme source directory");
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
if (fs.existsSync(emailThemeSrcDirPath)) {
|
||||
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);
|
||||
|
||||
process.exit(-1);
|
||||
}
|
||||
|
||||
const { keycloakVersion } = await promptKeycloakVersion();
|
||||
|
||||
const builtinKeycloakThemeTmpDirPath = pathJoin(emailThemeSrcDirPath, "..", "tmp_xIdP3_builtin_keycloak_theme");
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": builtinKeycloakThemeTmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
|
||||
"destDirPath": emailThemeSrcDirPath
|
||||
});
|
||||
|
||||
{
|
||||
const themePropertyFilePath = pathJoin(emailThemeSrcDirPath, "theme.properties");
|
||||
|
||||
fs.writeFileSync(themePropertyFilePath, Buffer.from(`parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, "utf8"));
|
||||
}
|
||||
|
||||
logger.log(`${pathRelative(process.cwd(), emailThemeSrcDirPath)} ready to be customized, feel free to remove every file you do not customize`);
|
||||
|
||||
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
@ -1,44 +1,12 @@
|
||||
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?: {
|
||||
extraPages?: 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(),
|
||||
"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>>();
|
||||
import { bundlers, getParsedPackageJson, type Bundler } from "./parsedPackageJson";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, sep as pathSep } from "path";
|
||||
import parseArgv from "minimist";
|
||||
|
||||
/** Consolidated build option gathered form CLI arguments and config in package.json */
|
||||
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
|
||||
@ -46,13 +14,20 @@ export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets
|
||||
export namespace BuildOptions {
|
||||
export type Common = {
|
||||
isSilent: boolean;
|
||||
version: string;
|
||||
themeVersion: string;
|
||||
themeName: string;
|
||||
extraPages?: string[];
|
||||
extraLoginPages: string[] | undefined;
|
||||
extraAccountPages: string[] | undefined;
|
||||
extraThemeProperties?: string[];
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
bundler: Bundler;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
/** Directory of your built react project. Defaults to {cwd}/build */
|
||||
reactAppBuildDirPath: string;
|
||||
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
|
||||
keycloakifyBuildDirPath: string;
|
||||
customUserAttributes: string[];
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
@ -79,15 +54,19 @@ export namespace BuildOptions {
|
||||
}
|
||||
}
|
||||
|
||||
export function readBuildOptions(params: {
|
||||
packageJson: string;
|
||||
CNAME: string | undefined;
|
||||
isExternalAssetsCliParamProvided: boolean;
|
||||
isSilent: boolean;
|
||||
}): BuildOptions {
|
||||
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
|
||||
export function readBuildOptions(params: { projectDirPath: string; processArgv: string[] }): BuildOptions {
|
||||
const { projectDirPath, processArgv } = params;
|
||||
|
||||
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
|
||||
const { isExternalAssetsCliParamProvided, isSilentCliParamProvided } = (() => {
|
||||
const argv = parseArgv(processArgv);
|
||||
|
||||
return {
|
||||
"isSilentCliParamProvided": typeof argv["silent"] === "boolean" ? argv["silent"] : false,
|
||||
"isExternalAssetsCliParamProvided": typeof argv["external-assets"] === "boolean" ? argv["external-assets"] : false
|
||||
};
|
||||
})();
|
||||
|
||||
const parsedPackageJson = getParsedPackageJson({ projectDirPath });
|
||||
|
||||
const url = (() => {
|
||||
const { homepage } = parsedPackageJson;
|
||||
@ -98,6 +77,16 @@ export function readBuildOptions(params: {
|
||||
url = new URL(homepage);
|
||||
}
|
||||
|
||||
const CNAME = (() => {
|
||||
const cnameFilePath = pathJoin(projectDirPath, "public", "CNAME");
|
||||
|
||||
if (!fs.existsSync(cnameFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return fs.readFileSync(cnameFilePath).toString("utf8");
|
||||
})();
|
||||
|
||||
if (CNAME !== undefined) {
|
||||
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
|
||||
}
|
||||
@ -119,12 +108,15 @@ export function readBuildOptions(params: {
|
||||
const common: BuildOptions.Common = (() => {
|
||||
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
|
||||
|
||||
const { extraPages, extraThemeProperties, groupId, artifactId, bundler } = keycloakify ?? {};
|
||||
const { extraPages, extraLoginPages, extraAccountPages, extraThemeProperties, groupId, artifactId, bundler, keycloakVersionDefaultAssets } =
|
||||
keycloakify ?? {};
|
||||
|
||||
const themeName = name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
const themeName =
|
||||
keycloakify.themeName ??
|
||||
name
|
||||
.replace(/^@(.*)/, "$1")
|
||||
.split("/")
|
||||
.join("-");
|
||||
|
||||
return {
|
||||
themeName,
|
||||
@ -157,10 +149,47 @@ export function readBuildOptions(params: {
|
||||
.join(".") ?? fallbackGroupId) + ".keycloak"
|
||||
);
|
||||
})(),
|
||||
"version": process.env.KEYCLOAKIFY_VERSION ?? version,
|
||||
extraPages,
|
||||
"themeVersion": process.env.KEYCLOAKIFY_THEME_VERSION ?? process.env.KEYCLOAKIFY_VERSION ?? version ?? "0.0.0",
|
||||
"extraLoginPages": [...(extraPages ?? []), ...(extraLoginPages ?? [])],
|
||||
extraAccountPages,
|
||||
extraThemeProperties,
|
||||
isSilent
|
||||
"isSilent": isSilentCliParamProvided,
|
||||
"keycloakVersionDefaultAssets": keycloakVersionDefaultAssets ?? "11.0.3",
|
||||
"reactAppBuildDirPath": (() => {
|
||||
let { reactAppBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (reactAppBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
reactAppBuildDirPath = reactAppBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (reactAppBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, reactAppBuildDirPath);
|
||||
}
|
||||
|
||||
return reactAppBuildDirPath;
|
||||
})(),
|
||||
"keycloakifyBuildDirPath": (() => {
|
||||
let { keycloakifyBuildDirPath = undefined } = parsedPackageJson.keycloakify ?? {};
|
||||
|
||||
if (keycloakifyBuildDirPath === undefined) {
|
||||
return pathJoin(projectDirPath, "build_keycloak");
|
||||
}
|
||||
|
||||
if (pathSep === "\\") {
|
||||
keycloakifyBuildDirPath = keycloakifyBuildDirPath.replace(/\//g, pathSep);
|
||||
}
|
||||
|
||||
if (keycloakifyBuildDirPath.startsWith(`.${pathSep}`)) {
|
||||
return pathJoin(projectDirPath, keycloakifyBuildDirPath);
|
||||
}
|
||||
|
||||
return keycloakifyBuildDirPath;
|
||||
})(),
|
||||
"customUserAttributes": keycloakify.customUserAttributes ?? []
|
||||
};
|
||||
})();
|
||||
|
||||
|
@ -2,8 +2,7 @@
|
||||
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
|
||||
(()=>{
|
||||
|
||||
const out =
|
||||
${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
|
||||
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
|
||||
@ -14,7 +13,7 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
|
||||
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
|
||||
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
|
||||
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"
|
||||
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
|
||||
]>
|
||||
|
||||
<#attempt>
|
||||
@ -111,6 +110,17 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
}
|
||||
};
|
||||
|
||||
<#if account??>
|
||||
out["url"]["getLogoutUrl"] = function () {
|
||||
<#attempt>
|
||||
return "${url.getLogoutUrl()}";
|
||||
<#recover>
|
||||
</#attempt>
|
||||
};
|
||||
</#if>
|
||||
|
||||
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
|
||||
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
|
||||
out["pageId"] = "${pageId}";
|
||||
|
||||
return out;
|
||||
@ -156,9 +166,9 @@ ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
|
||||
key == "updateProfileCtx" &&
|
||||
are_same_path(path, [])
|
||||
) || (
|
||||
<#-- https://github.com/InseeFrLab/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||
<#-- https://github.com/InseeFrLab/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||
<#-- https://github.com/InseeFrLab/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/pull/65#issuecomment-991896344 (reports with saml-post-form.ftl) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/91#issue-1212319466 (reports with error.ftl and Kc18) -->
|
||||
<#-- https://github.com/keycloakify/keycloakify/issues/109#issuecomment-1134610163 -->
|
||||
key == "loginAction" &&
|
||||
are_same_path(path, ["url"]) &&
|
||||
["saml-post-form.ftl", "error.ftl", "info.ftl"]?seq_contains(pageId) &&
|
||||
|
@ -8,37 +8,20 @@ import { objectKeys } from "tsafe/objectKeys";
|
||||
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { Reflect } from "tsafe/Reflect";
|
||||
|
||||
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
|
||||
export const pageIds = [
|
||||
"login.ftl",
|
||||
"login-username.ftl",
|
||||
"login-password.ftl",
|
||||
"webauthn-authenticate.ftl",
|
||||
"register.ftl",
|
||||
"register-user-profile.ftl",
|
||||
"info.ftl",
|
||||
"error.ftl",
|
||||
"login-reset-password.ftl",
|
||||
"login-verify-email.ftl",
|
||||
"terms.ftl",
|
||||
"login-otp.ftl",
|
||||
"login-update-profile.ftl",
|
||||
"login-update-password.ftl",
|
||||
"login-idp-link-confirm.ftl",
|
||||
"login-idp-link-email.ftl",
|
||||
"login-page-expired.ftl",
|
||||
"login-config-totp.ftl",
|
||||
"logout-confirm.ftl",
|
||||
"update-user-profile.ftl",
|
||||
"idp-review-user-profile.ftl"
|
||||
] as const;
|
||||
export const themeTypes = ["login", "account"] as const;
|
||||
|
||||
export type ThemeType = (typeof themeTypes)[number];
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Standalone = {
|
||||
export type Common = {
|
||||
customUserAttributes: string[];
|
||||
themeVersion: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
@ -50,33 +33,30 @@ export namespace BuildOptionsLike {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
export type SameDomain = Common &
|
||||
CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
export type DifferentDomains = Common &
|
||||
CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export type PageId = (typeof pageIds)[number];
|
||||
assert<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;
|
||||
keycloakifyVersion: string;
|
||||
}) {
|
||||
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
|
||||
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion } = params;
|
||||
|
||||
const $ = cheerio.load(indexHtmlCode);
|
||||
|
||||
@ -146,7 +126,13 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
|
||||
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
|
||||
.toString("utf8")
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
|
||||
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
|
||||
.replace(
|
||||
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
|
||||
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
|
||||
)
|
||||
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
|
||||
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion),
|
||||
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
|
||||
"<#if scripts??>",
|
||||
" <#list scripts as script>",
|
||||
@ -179,7 +165,6 @@ export function generateFtlFilesCodeFactory(params: {
|
||||
|
||||
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)));
|
||||
|
||||
|
@ -1 +1,2 @@
|
||||
export * from "./generateFtl";
|
||||
export * from "./pageId";
|
||||
|
31
src/bin/keycloakify/generateFtl/pageId.ts
Normal file
@ -0,0 +1,31 @@
|
||||
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",
|
||||
"update-email.ftl",
|
||||
"select-authenticator.ftl",
|
||||
"saml-post-form.ftl"
|
||||
] as const;
|
||||
|
||||
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;
|
||||
|
||||
export type LoginThemePageId = (typeof loginThemePageIds)[number];
|
||||
export type AccountThemePageId = (typeof accountThemePageIds)[number];
|
@ -1,5 +1,6 @@
|
||||
import * as fs from "fs";
|
||||
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";
|
||||
@ -8,7 +9,7 @@ export type BuildOptionsLike = {
|
||||
themeName: string;
|
||||
groupId: string;
|
||||
artifactId?: string;
|
||||
version: string;
|
||||
themeVersion: string;
|
||||
};
|
||||
|
||||
{
|
||||
@ -25,7 +26,7 @@ export function generateJavaStackFiles(params: {
|
||||
jarFilePath: string;
|
||||
} {
|
||||
const {
|
||||
buildOptions: { groupId, themeName, version, artifactId },
|
||||
buildOptions: { groupId, themeName, themeVersion, artifactId },
|
||||
keycloakThemeBuildingDirPath,
|
||||
doBundlesEmailTemplate
|
||||
} = params;
|
||||
@ -42,7 +43,7 @@ export function generateJavaStackFiles(params: {
|
||||
` <modelVersion>4.0.0</modelVersion>`,
|
||||
` <groupId>${groupId}</groupId>`,
|
||||
` <artifactId>${artifactId}</artifactId>`,
|
||||
` <version>${version}</version>`,
|
||||
` <version>${themeVersion}</version>`,
|
||||
` <name>${artifactId}</name>`,
|
||||
` <description />`,
|
||||
`</project>`
|
||||
@ -69,7 +70,7 @@ export function generateJavaStackFiles(params: {
|
||||
"themes": [
|
||||
{
|
||||
"name": themeName,
|
||||
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])]
|
||||
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -82,6 +83,6 @@ export function generateJavaStackFiles(params: {
|
||||
}
|
||||
|
||||
return {
|
||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${version}.jar`)
|
||||
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${artifactId}-${themeVersion}.jar`)
|
||||
};
|
||||
}
|
||||
|
@ -1,201 +0,0 @@
|
||||
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, pageIds } 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;
|
||||
extraPages?: string[];
|
||||
extraThemeProperties?: string[];
|
||||
isSilent: boolean;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||
|
||||
export namespace ExternalAssets {
|
||||
export type CommonExternalAssets = Common & {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const buildOptions = Reflect<BuildOptions>();
|
||||
|
||||
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
|
||||
}
|
||||
|
||||
export 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 themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
|
||||
|
||||
let allCssGlobalsToDefine: Record<string, string> = {};
|
||||
|
||||
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")
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
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": pathJoin(themeDirPath, "..", "email")
|
||||
});
|
||||
}
|
||||
|
||||
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
|
||||
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
|
||||
"cssGlobalsToDefine": allCssGlobalsToDefine,
|
||||
"buildOptions": buildOptions
|
||||
});
|
||||
|
||||
[...pageIds, ...(buildOptions.extraPages ?? [])].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")
|
||||
);
|
||||
|
||||
return { doBundlesEmailTemplate };
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin, relative as pathRelative } from "path";
|
||||
import type { ThemeType } from "../generateFtl";
|
||||
import { downloadBuiltinKeycloakTheme } from "../../download-builtin-keycloak-theme";
|
||||
import {
|
||||
resourcesCommonDirPathRelativeToPublicDir,
|
||||
resourcesDirPathRelativeToPublicDir,
|
||||
basenameOfKeycloakDirInPublicDir
|
||||
} from "../../mockTestingResourcesPath";
|
||||
import * as crypto from "crypto";
|
||||
|
||||
export async function downloadKeycloakStaticResources(
|
||||
// prettier-ignore
|
||||
params: {
|
||||
themeType: ThemeType;
|
||||
themeDirPath: string;
|
||||
isSilent: boolean;
|
||||
keycloakVersion: string;
|
||||
}
|
||||
) {
|
||||
const { themeType, isSilent, themeDirPath, keycloakVersion } = params;
|
||||
|
||||
const tmpDirPath = pathJoin(
|
||||
themeDirPath,
|
||||
"..",
|
||||
`tmp_suLeKsxId_${crypto.createHash("sha256").update(`${themeType}-${keycloakVersion}`).digest("hex").slice(0, 8)}`
|
||||
);
|
||||
|
||||
await downloadBuiltinKeycloakTheme({
|
||||
keycloakVersion,
|
||||
"destDirPath": tmpDirPath,
|
||||
isSilent
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", themeType, "resources"),
|
||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesDirPathRelativeToPublicDir))
|
||||
});
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
|
||||
"destDirPath": pathJoin(themeDirPath, pathRelative(basenameOfKeycloakDirInPublicDir, resourcesCommonDirPathRelativeToPublicDir))
|
||||
});
|
||||
|
||||
fs.rmSync(tmpDirPath, { "recursive": true, "force": true });
|
||||
}
|
239
src/bin/keycloakify/generateTheme/generateTheme.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { transformCodebase } from "../../tools/transformCodebase";
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
|
||||
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
|
||||
import { generateFtlFilesCodeFactory, loginThemePageIds, accountThemePageIds, themeTypes, type ThemeType } from "../generateFtl";
|
||||
import { basenameOfKeycloakDirInPublicDir } from "../../mockTestingResourcesPath";
|
||||
import { isInside } from "../../tools/isInside";
|
||||
import type { BuildOptions } from "../BuildOptions";
|
||||
import { assert } from "tsafe/assert";
|
||||
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
|
||||
|
||||
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
|
||||
|
||||
export namespace BuildOptionsLike {
|
||||
export type Common = {
|
||||
themeName: string;
|
||||
extraLoginPages?: string[];
|
||||
extraAccountPages?: string[];
|
||||
extraThemeProperties?: string[];
|
||||
isSilent: boolean;
|
||||
customUserAttributes: string[];
|
||||
themeVersion: string;
|
||||
keycloakVersionDefaultAssets: string;
|
||||
};
|
||||
|
||||
export type Standalone = Common & {
|
||||
isStandalone: true;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
|
||||
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
|
||||
|
||||
export namespace ExternalAssets {
|
||||
export type CommonExternalAssets = Common & {
|
||||
isStandalone: false;
|
||||
};
|
||||
|
||||
export type SameDomain = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: true;
|
||||
};
|
||||
|
||||
export type DifferentDomains = CommonExternalAssets & {
|
||||
areAppAndKeycloakServerSharingSameDomain: false;
|
||||
urlOrigin: string;
|
||||
urlPathname: string | undefined;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
assert<BuildOptions extends BuildOptionsLike ? true : false>();
|
||||
|
||||
export async function generateTheme(params: {
|
||||
reactAppBuildDirPath: string;
|
||||
keycloakThemeBuildingDirPath: string;
|
||||
emailThemeSrcDirPath: string | undefined;
|
||||
buildOptions: BuildOptionsLike;
|
||||
keycloakifyVersion: string;
|
||||
}): Promise<{ doBundlesEmailTemplate: boolean }> {
|
||||
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, buildOptions, keycloakifyVersion } = params;
|
||||
|
||||
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, basenameOfKeycloakDirInPublicDir),
|
||||
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,
|
||||
keycloakifyVersion
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
//TODO: Remove this block we left it for now only for backward compatibility
|
||||
// we now have a separate script for this
|
||||
copy_keycloak_resources_to_public: {
|
||||
const keycloakDirInPublicDir = pathJoin(reactAppBuildDirPath, "..", "public", basenameOfKeycloakDirInPublicDir);
|
||||
|
||||
if (fs.existsSync(keycloakDirInPublicDir)) {
|
||||
break copy_keycloak_resources_to_public;
|
||||
}
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
"isSilent": buildOptions.isSilent,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
"themeDirPath": keycloakDirInPublicDir,
|
||||
themeType
|
||||
});
|
||||
|
||||
if (themeType !== themeTypes[0]) {
|
||||
break copy_keycloak_resources_to_public;
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(keycloakDirInPublicDir, "README.txt"),
|
||||
Buffer.from(
|
||||
// prettier-ignore
|
||||
[
|
||||
"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(keycloakDirInPublicDir, ".gitignore"), Buffer.from("*", "utf8"));
|
||||
}
|
||||
|
||||
await downloadKeycloakStaticResources({
|
||||
"isSilent": buildOptions.isSilent,
|
||||
"keycloakVersion": buildOptions.keycloakVersionDefaultAssets,
|
||||
themeDirPath,
|
||||
themeType
|
||||
});
|
||||
|
||||
fs.writeFileSync(
|
||||
pathJoin(themeDirPath, "theme.properties"),
|
||||
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
|
||||
);
|
||||
}
|
||||
|
||||
let doBundlesEmailTemplate: boolean;
|
||||
|
||||
email: {
|
||||
if (emailThemeSrcDirPath === undefined) {
|
||||
doBundlesEmailTemplate = false;
|
||||
break email;
|
||||
}
|
||||
|
||||
doBundlesEmailTemplate = true;
|
||||
|
||||
transformCodebase({
|
||||
"srcDirPath": emailThemeSrcDirPath,
|
||||
"destDirPath": getThemeDirPath("email")
|
||||
});
|
||||
}
|
||||
|
||||
return { doBundlesEmailTemplate };
|
||||
}
|
1
src/bin/keycloakify/generateTheme/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./generateTheme";
|
@ -4,5 +4,5 @@ export * from "./keycloakify";
|
||||
import { main } from "./keycloakify";
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(e => console.error(e));
|
||||
main();
|
||||
}
|
||||
|
@ -1,54 +1,52 @@
|
||||
import { generateKeycloakThemeResources } from "./generateKeycloakThemeResources";
|
||||
import { generateTheme } from "./generateTheme";
|
||||
import { generateJavaStackFiles } from "./generateJavaStackFiles";
|
||||
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
|
||||
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(keycloakThemeBuildingDirPath, "..", "keycloak_email");
|
||||
import { Equals } from "tsafe";
|
||||
import { getEmailThemeSrcDirPath } from "../getSrcDirPath";
|
||||
import { getProjectRoot } from "../tools/getProjectRoot";
|
||||
|
||||
export async function main() {
|
||||
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
|
||||
const logger = getLogger({ isSilent });
|
||||
logger.log("🔏 Building the keycloak theme...⌚");
|
||||
const projectDirPath = process.cwd();
|
||||
|
||||
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
|
||||
projectDirPath,
|
||||
"processArgv": process.argv.slice(2)
|
||||
});
|
||||
|
||||
const { doBundlesEmailTemplate } = await generateKeycloakThemeResources({
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakThemeEmailDirPath,
|
||||
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
|
||||
const logger = getLogger({ "isSilent": buildOptions.isSilent });
|
||||
logger.log("🔏 Building the keycloak theme...⌚");
|
||||
|
||||
const { doBundlesEmailTemplate } = await generateTheme({
|
||||
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
"emailThemeSrcDirPath": (() => {
|
||||
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath });
|
||||
|
||||
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return emailThemeSrcDirPath;
|
||||
})(),
|
||||
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
|
||||
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"
|
||||
"keycloakifyVersion": (() => {
|
||||
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"];
|
||||
|
||||
assert(typeof version === "string");
|
||||
|
||||
return version;
|
||||
})()
|
||||
});
|
||||
|
||||
const { jarFilePath } = generateJavaStackFiles({
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
doBundlesEmailTemplate,
|
||||
buildOptions
|
||||
});
|
||||
@ -60,8 +58,8 @@ export async function main() {
|
||||
case "keycloakify":
|
||||
logger.log("🫶 Let keycloakify do its thang");
|
||||
await jar({
|
||||
"rootPath": pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources"),
|
||||
"version": buildOptions.version,
|
||||
"rootPath": pathJoin(buildOptions.keycloakifyBuildDirPath, "src", "main", "resources"),
|
||||
"version": buildOptions.themeVersion,
|
||||
"groupId": buildOptions.groupId,
|
||||
"artifactId": buildOptions.artifactId,
|
||||
"targetPath": jarFilePath
|
||||
@ -69,7 +67,7 @@ export async function main() {
|
||||
break;
|
||||
case "mvn":
|
||||
logger.log("🫙 Run maven to deliver a jar");
|
||||
child_process.execSync("mvn package", { "cwd": keycloakThemeBuildingDirPath });
|
||||
child_process.execSync("mvn package", { "cwd": buildOptions.keycloakifyBuildDirPath });
|
||||
break;
|
||||
default:
|
||||
assert<Equals<typeof buildOptions.bundler, never>>(false);
|
||||
@ -79,7 +77,7 @@ export async function main() {
|
||||
const containerKeycloakVersion = "20.0.1";
|
||||
|
||||
generateStartKeycloakTestingContainer({
|
||||
keycloakThemeBuildingDirPath,
|
||||
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath,
|
||||
"keycloakVersion": containerKeycloakVersion,
|
||||
buildOptions
|
||||
});
|
||||
@ -87,7 +85,7 @@ export async function main() {
|
||||
logger.log(
|
||||
[
|
||||
"",
|
||||
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
|
||||
`✅ Your keycloak theme has been generated and bundled into .${pathSep}${pathRelative(projectDirPath, 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.
|
||||
@ -121,19 +119,33 @@ export async function main() {
|
||||
"",
|
||||
`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))} 👈`,
|
||||
`👉 $ .${pathSep}${pathRelative(
|
||||
projectDirPath,
|
||||
pathJoin(buildOptions.keycloakifyBuildDirPath, 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: ",
|
||||
`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: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
|
||||
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
|
||||
"",
|
||||
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
|
||||
""
|
||||
`- Create a realm: Master -> AddRealm -> Name: myrealm`,
|
||||
`- Enable registration: Realm settings -> Login tab -> User registration: on`,
|
||||
`- Enable the Account theme (optional): Realm settings -> Themes tab -> Account theme: ${buildOptions.themeName}`,
|
||||
` Clients -> account -> Login theme: ${buildOptions.themeName}`,
|
||||
`- Enable the email theme (optional): Realm settings -> Themes tab -> Email theme: ${buildOptions.themeName} (option will appear only if you have ran npx initialize-email-theme)`,
|
||||
`- Create a client Clients -> Create -> 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")
|
||||
);
|
||||
}
|
||||
|
64
src/bin/keycloakify/parsedPackageJson.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import * as fs from "fs";
|
||||
import { assert } from "tsafe";
|
||||
import type { Equals } from "tsafe";
|
||||
import { z } from "zod";
|
||||
import { pathJoin } from "../tools/pathJoin";
|
||||
|
||||
export const bundlers = ["mvn", "keycloakify", "none"] as const;
|
||||
export type Bundler = (typeof bundlers)[number];
|
||||
export 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;
|
||||
keycloakVersionDefaultAssets?: string;
|
||||
reactAppBuildDirPath?: string;
|
||||
keycloakifyBuildDirPath?: string;
|
||||
customUserAttributes?: string[];
|
||||
themeName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const zParsedPackageJson = z.object({
|
||||
"name": z.string(),
|
||||
"version": z.string().optional(),
|
||||
"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(),
|
||||
"keycloakVersionDefaultAssets": z.string().optional(),
|
||||
"reactAppBuildDirPath": z.string().optional(),
|
||||
"keycloakifyBuildDirPath": z.string().optional(),
|
||||
"customUserAttributes": z.array(z.string()).optional(),
|
||||
"themeName": z.string().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
|
||||
assert<Equals<ReturnType<(typeof zParsedPackageJson)["parse"]>, ParsedPackageJson>>();
|
||||
|
||||
let parsedPackageJson: undefined | ReturnType<(typeof zParsedPackageJson)["parse"]>;
|
||||
export function getParsedPackageJson(params: { projectDirPath: string }) {
|
||||
const { projectDirPath } = params;
|
||||
if (parsedPackageJson) {
|
||||
return parsedPackageJson;
|
||||
}
|
||||
parsedPackageJson = zParsedPackageJson.parse(JSON.parse(fs.readFileSync(pathJoin(projectDirPath, "package.json")).toString("utf8")));
|
||||
return parsedPackageJson;
|
||||
}
|
@ -1,5 +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");
|
||||
export const basenameOfKeycloakDirInPublicDir = "keycloak-resources";
|
||||
export const resourcesDirPathRelativeToPublicDir = pathJoin(basenameOfKeycloakDirInPublicDir, "resources");
|
||||
export const resourcesCommonDirPathRelativeToPublicDir = pathJoin(resourcesDirPathRelativeToPublicDir, "resources_common");
|
||||
|
@ -1,15 +0,0 @@
|
||||
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
|
||||
};
|
||||
};
|
@ -42,6 +42,7 @@ export function crc32(input: Readable | String | Buffer): Promise<number> {
|
||||
} 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) => {
|
||||
|
@ -1,15 +1,13 @@
|
||||
import { dirname as pathDirname, basename as pathBasename, join as pathJoin, join } from "path";
|
||||
import { createReadStream, createWriteStream } from "fs";
|
||||
import { stat, mkdir, unlink, writeFile } from "fs/promises";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
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 { createHash } from "crypto";
|
||||
import { mkdir, stat, writeFile } from "fs/promises";
|
||||
import fetch, { type FetchOptions } from "make-fetch-happen";
|
||||
import { dirname as pathDirname, join as pathJoin } from "path";
|
||||
import { assert } from "tsafe";
|
||||
import { promisify } from "util";
|
||||
import { getProjectRoot } from "./getProjectRoot";
|
||||
import { transformCodebase } from "./transformCodebase";
|
||||
import { unzip } from "./unzip";
|
||||
|
||||
const exec = promisify(execCallback);
|
||||
|
||||
@ -17,25 +15,27 @@ function hash(s: string) {
|
||||
return createHash("sha256").update(s).digest("hex");
|
||||
}
|
||||
|
||||
async function maybeStat(path: string) {
|
||||
async function exists(path: string) {
|
||||
try {
|
||||
return await stat(path);
|
||||
await stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as Error & { code: string }).code === "ENOENT") return undefined;
|
||||
if ((error as Error & { code: string }).code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an npm configuration value as string, undefined if not set.
|
||||
*
|
||||
* @param key
|
||||
* @returns string or undefined
|
||||
* Get npm configuration as map
|
||||
*/
|
||||
async function getNmpConfig(key: string): Promise<string | undefined> {
|
||||
const { stdout } = await exec(`npm config get ${key}`);
|
||||
const value = stdout.trim();
|
||||
return value && value !== "null" ? value : undefined;
|
||||
async function getNmpConfig(): Promise<Record<string, string>> {
|
||||
const { stdout } = await exec("npm config get", { encoding: "utf8" });
|
||||
return stdout
|
||||
.split("\n")
|
||||
.filter(line => !line.startsWith(";"))
|
||||
.map(line => line.trim())
|
||||
.map(line => line.split("=", 2))
|
||||
.reduce((cfg, [key, value]) => ({ ...cfg, [key]: value }), {});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -45,231 +45,43 @@ async function getNmpConfig(key: string): Promise<string | undefined> {
|
||||
* @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"));
|
||||
const cfg = await getNmpConfig();
|
||||
|
||||
const proxy = cfg["https-proxy"] ?? cfg["proxy"];
|
||||
const noProxy = cfg["noproxy"] ?? cfg["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;
|
||||
}
|
||||
export async function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
|
||||
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
|
||||
|
||||
/**
|
||||
* @typedef
|
||||
* @type MultiError = Error & { cause: Error[] }
|
||||
*/
|
||||
const downloadHash = hash(JSON.stringify({ url })).substring(0, 15);
|
||||
const projectRoot = getProjectRoot();
|
||||
const cacheRoot = process.env.XDG_CACHE_HOME ?? pathJoin(projectRoot, "node_modules", ".cache");
|
||||
const zipFilePath = pathJoin(cacheRoot, "keycloakify", "zip", `_${downloadHash}.zip`);
|
||||
const extractDirPath = pathJoin(cacheRoot, "keycloakify", "unzip", `_${downloadHash}`);
|
||||
|
||||
/**
|
||||
* Extract the archive `zipFile` into the directory `dir`. If `archiveDir` is given,
|
||||
* only that directory will be extracted, stripping the given path components.
|
||||
*
|
||||
* If dir does not exist, it will be created.
|
||||
*
|
||||
* If any archive file exists, it will be overwritten.
|
||||
*
|
||||
* Will unzip using all available nodejs worker threads.
|
||||
*
|
||||
* Will try to clean up extracted files on failure.
|
||||
*
|
||||
* If unpacking fails, will either throw an regular error, or
|
||||
* possibly an `MultiError`, which contains a `cause` field with
|
||||
* a number of root cause errors.
|
||||
*
|
||||
* Warning this method is not optimized for continuous reading of the zip
|
||||
* archive, but is a trade-off between simplicity and allowing extraction
|
||||
* of a single directory from the archive.
|
||||
*
|
||||
* @param zipFile the file to unzip
|
||||
* @param dir the target directory
|
||||
* @param archiveDir if given, unpack only files from this archive directory
|
||||
* @throws {MultiError} error
|
||||
* @returns Promise for a list of full file paths pointing to actually extracted files
|
||||
*/
|
||||
async function unzip(zipFile: string, dir: string, archiveDir?: string): Promise<string[]> {
|
||||
await mkdir(dir, { recursive: true });
|
||||
const promises: Promise<string>[] = [];
|
||||
|
||||
// Iterate over all files in the zip, skip files which are not in archiveDir,
|
||||
// if given.
|
||||
for await (const record of iterateZipArchive(zipFile)) {
|
||||
const { path: recordPath, createReadStream: createRecordReadStream } = record;
|
||||
const filePath = pathJoin(dir, recordPath);
|
||||
const parent = pathDirname(filePath);
|
||||
if (archiveDir && !recordPath.startsWith(archiveDir)) continue;
|
||||
promises.push(
|
||||
new Promise<string>(async (resolve, reject) => {
|
||||
await mkdir(parent, { recursive: true });
|
||||
// Pull the file out of the archive, write it to the target directory
|
||||
const input = createRecordReadStream();
|
||||
const output = createWriteStream(filePath);
|
||||
output.on("error", e => reject(Object.assign(e, { filePath })));
|
||||
output.on("finish", () => resolve(filePath));
|
||||
input.pipe(output);
|
||||
})
|
||||
);
|
||||
if (!(await exists(zipFilePath))) {
|
||||
const proxyOpts = await getNpmProxyConfig();
|
||||
const response = await fetch(url, proxyOpts);
|
||||
await mkdir(pathDirname(zipFilePath), { "recursive": true });
|
||||
/**
|
||||
* The correct way to fix this is to upgrade node-fetch beyond 3.2.5
|
||||
* (see https://github.com/node-fetch/node-fetch/issues/1295#issuecomment-1144061991.)
|
||||
* Unfortunately, octokit (a dependency of keycloakify) also uses node-fetch, and
|
||||
* does not support node-fetch 3.x. So we stick around with this band-aid until
|
||||
* octokit upgrades.
|
||||
*/
|
||||
response.body?.setMaxListeners(Number.MAX_VALUE);
|
||||
assert(typeof response.body !== "undefined" && response.body != null);
|
||||
await writeFile(zipFilePath, response.body);
|
||||
}
|
||||
|
||||
// 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);
|
||||
await unzip(zipFilePath, extractDirPath, pathOfDirToExtractInArchive);
|
||||
|
||||
// If any extraction failed, try to clean up, then throw a MultiError,
|
||||
// which has a `cause` field, containing a list of root cause errors.
|
||||
if (failure.length) {
|
||||
await Promise.all(success.map(path => unlink(path)));
|
||||
await Promise.all(failure.map(e => e && e.path && unlink(e.path as string)));
|
||||
const e = new Error("Failed to extract: " + failure.map(e => e.message).join(";"));
|
||||
(e as any).cause = failure;
|
||||
throw e;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param file file to read
|
||||
* @param start first byte to read
|
||||
* @param end last byte to read
|
||||
* @returns Promise of a buffer of read bytes
|
||||
*/
|
||||
async function readFileChunk(file: string, start: number, end: number): Promise<Buffer> {
|
||||
const chunks: Buffer[] = [];
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = createReadStream(file, { start, end });
|
||||
stream.on("error", e => reject(e));
|
||||
stream.on("end", () => resolve(Buffer.concat(chunks)));
|
||||
stream.on("data", chunk => chunks.push(chunk as Buffer));
|
||||
transformCodebase({
|
||||
"srcDirPath": extractDirPath,
|
||||
"destDirPath": destDirPath
|
||||
});
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
@ -1,102 +1,87 @@
|
||||
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 { ZipFile } from "yazl";
|
||||
import { mkdir } from "fs/promises";
|
||||
import trimIndent from "./trimIndent";
|
||||
|
||||
/** Trim leading whitespace from every line */
|
||||
const trimIndent = (s: string) => s.replace(/(\n)\s+/g, "$1");
|
||||
export type ZipEntry = { zipPath: string } & ({ fsPath: string } | { buffer: Buffer });
|
||||
export type ZipEntryGenerator = AsyncGenerator<ZipEntry, void, unknown>;
|
||||
|
||||
type JarArgs = {
|
||||
rootPath: string;
|
||||
targetPath: string;
|
||||
type CommonJarArgs = {
|
||||
groupId: string;
|
||||
artifactId: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export type JarStreamArgs = CommonJarArgs & {
|
||||
asyncPathGeneratorFn(): ZipEntryGenerator;
|
||||
};
|
||||
|
||||
export type JarArgs = CommonJarArgs & {
|
||||
targetPath: string;
|
||||
rootPath: string;
|
||||
};
|
||||
|
||||
export async function jarStream({ groupId, artifactId, version, asyncPathGeneratorFn }: JarStreamArgs) {
|
||||
const manifestPath = "META-INF/MANIFEST.MF";
|
||||
const manifestData = Buffer.from(trimIndent`
|
||||
Manifest-Version: 1.0
|
||||
Archiver-Version: Plexus Archiver
|
||||
Created-By: Keycloakify
|
||||
Built-By: unknown
|
||||
Build-Jdk: 19.0.0
|
||||
`);
|
||||
|
||||
const pomPropsPath = `META-INF/maven/${groupId}/${artifactId}/pom.properties`;
|
||||
const pomPropsData = Buffer.from(trimIndent`
|
||||
# Generated by keycloakify
|
||||
# ${new Date()}
|
||||
artifactId=${artifactId}
|
||||
groupId=${groupId}
|
||||
version=${version}
|
||||
`);
|
||||
|
||||
const zipFile = new ZipFile();
|
||||
|
||||
for await (const entry of asyncPathGeneratorFn()) {
|
||||
if ("buffer" in entry) {
|
||||
zipFile.addBuffer(entry.buffer, entry.zipPath);
|
||||
} else if ("fsPath" in entry && !entry.fsPath.endsWith(sep)) {
|
||||
zipFile.addFile(entry.fsPath, entry.zipPath);
|
||||
}
|
||||
}
|
||||
|
||||
zipFile.addBuffer(manifestData, manifestPath);
|
||||
zipFile.addBuffer(pomPropsData, pomPropsPath);
|
||||
|
||||
zipFile.end();
|
||||
|
||||
return zipFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const asyncPathGeneratorFn = async function* (): ZipEntryGenerator {
|
||||
for await (const fsPath of walk(rootPath)) {
|
||||
const zipPath = relative(rootPath, fsPath).split(sep).join("/");
|
||||
yield { fsPath, zipPath };
|
||||
}
|
||||
};
|
||||
|
||||
const zipFile = await jarStream({ groupId, artifactId, version, asyncPathGeneratorFn });
|
||||
|
||||
await new Promise<void>(async (resolve, reject) => {
|
||||
zipFile.outputStream
|
||||
.pipe(createWriteStream(targetPath, { encoding: "binary" }))
|
||||
.on("finish", () => resolve())
|
||||
.on("close", () => 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
@ -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("");
|
||||
}
|
11
src/bin/tools/partitionPromiseSettledResults.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export type PromiseSettledAndPartitioned<T> = [T[], any[]];
|
||||
|
||||
export function partitionPromiseSettledResults<T>() {
|
||||
return [
|
||||
([successes, failures]: PromiseSettledAndPartitioned<T>, item: PromiseSettledResult<T>) =>
|
||||
item.status === "rejected"
|
||||
? ([successes, [item.reason, ...failures]] as PromiseSettledAndPartitioned<T>)
|
||||
: ([[item.value, ...successes], failures] as PromiseSettledAndPartitioned<T>),
|
||||
[[], []] as PromiseSettledAndPartitioned<T>
|
||||
] as const;
|
||||
}
|
@ -7,6 +7,8 @@ export default function tee(input: Readable) {
|
||||
let aFull = false;
|
||||
let bFull = false;
|
||||
|
||||
a.setMaxListeners(Infinity);
|
||||
|
||||
a.on("drain", () => {
|
||||
aFull = false;
|
||||
if (!aFull && !bFull) input.resume();
|
||||
|
46
src/bin/tools/trimIndent.ts
Normal file
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Concatenate the string fragments and interpolated values
|
||||
* to get a single string.
|
||||
*/
|
||||
function populateTemplate(strings: TemplateStringsArray, ...args: unknown[]) {
|
||||
const chunks: string[] = [];
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
let lastStringLineLength = 0;
|
||||
if (strings[i]) {
|
||||
chunks.push(strings[i]);
|
||||
// remember last indent of the string portion
|
||||
lastStringLineLength = strings[i].split("\n").at(-1)?.length ?? 0;
|
||||
}
|
||||
if (args[i]) {
|
||||
// if the interpolation value has newlines, indent the interpolation values
|
||||
// using the last known string indent
|
||||
const chunk = String(args[i]).replace(/([\r?\n])/g, "$1" + " ".repeat(lastStringLineLength));
|
||||
chunks.push(chunk);
|
||||
}
|
||||
}
|
||||
return chunks.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Shift all lines left by the *smallest* indentation level,
|
||||
* and remove initial newline and all trailing spaces.
|
||||
*/
|
||||
export default function trimIndent(strings: TemplateStringsArray, ...args: any[]) {
|
||||
// Remove initial and final newlines
|
||||
let string = populateTemplate(strings, ...args)
|
||||
.replace(/^[\r\n]/, "")
|
||||
.replace(/\r?\n *$/, "");
|
||||
const dents =
|
||||
string
|
||||
.match(/^([ \t])+/gm)
|
||||
?.filter(s => /^\s+$/.test(s))
|
||||
?.map(s => s.length) ?? [];
|
||||
// No dents? no change required
|
||||
if (!dents || dents.length == 0) return string;
|
||||
const minDent = Math.min(...dents);
|
||||
// The min indentation is 0, no change needed
|
||||
if (!minDent) return string;
|
||||
const re = new RegExp(`^${" ".repeat(minDent)}`, "gm");
|
||||
const dedented = string.replace(re, "");
|
||||
return dedented;
|
||||
}
|
92
src/bin/tools/unzip.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import fsp from "node:fs/promises";
|
||||
import fs from "fs";
|
||||
import path from "node:path";
|
||||
import yauzl from "yauzl";
|
||||
import stream from "node:stream";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const pipeline = promisify(stream.pipeline);
|
||||
|
||||
async function pathExists(path: string) {
|
||||
try {
|
||||
await fsp.stat(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as { code: string }).code === "ENOENT") {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unzip(file: string, targetFolder: string, unzipSubPath?: string) {
|
||||
// add trailing slash to unzipSubPath and targetFolder
|
||||
if (unzipSubPath && (!unzipSubPath.endsWith("/") || !unzipSubPath.endsWith("\\"))) {
|
||||
unzipSubPath += "/";
|
||||
}
|
||||
|
||||
if (!targetFolder.endsWith("/") || !targetFolder.endsWith("\\")) {
|
||||
targetFolder += "/";
|
||||
}
|
||||
if (!fs.existsSync(targetFolder)) {
|
||||
fs.mkdirSync(targetFolder, { recursive: true });
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
yauzl.open(file, { lazyEntries: true }, async (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.readEntry();
|
||||
|
||||
zipfile.on("entry", async entry => {
|
||||
if (unzipSubPath) {
|
||||
// Skip files outside of the unzipSubPath
|
||||
if (!entry.fileName.startsWith(unzipSubPath)) {
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the unzipSubPath from the file name
|
||||
entry.fileName = entry.fileName.substring(unzipSubPath.length);
|
||||
}
|
||||
|
||||
const target = path.join(targetFolder, entry.fileName);
|
||||
|
||||
// Directory file names end with '/'.
|
||||
// Note that entries for directories themselves are optional.
|
||||
// An entry's fileName implicitly requires its parent directories to exist.
|
||||
if (/[\/\\]$/.test(target)) {
|
||||
await fsp.mkdir(target, { recursive: true });
|
||||
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip existing files
|
||||
if (await pathExists(target)) {
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.openReadStream(entry, async (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
await pipeline(readStream, fs.createWriteStream(target));
|
||||
|
||||
zipfile.readEntry();
|
||||
});
|
||||
});
|
||||
|
||||
zipfile.once("end", function () {
|
||||
zipfile.close();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
import { readdir } from "fs/promises";
|
||||
import { resolve } from "path";
|
||||
import { resolve, sep } from "path";
|
||||
|
||||
/**
|
||||
* Asynchronously and recursively walk a directory tree, yielding every file and directory
|
||||
* found
|
||||
* found. Directory paths will _always_ end with a path separator.
|
||||
*
|
||||
* @param root the starting directory
|
||||
* @returns AsyncGenerator
|
||||
*/
|
||||
export default async function* walk(root: string): AsyncGenerator<string, void, void> {
|
||||
export default async function* walk(root: string): AsyncGenerator<string, void, unknown> {
|
||||
for (const entry of await readdir(root, { withFileTypes: true })) {
|
||||
const absolutePath = resolve(root, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
yield absolutePath;
|
||||
yield absolutePath.endsWith(sep) ? absolutePath : absolutePath + sep;
|
||||
yield* walk(absolutePath);
|
||||
} else yield absolutePath;
|
||||
} else yield absolutePath.endsWith(sep) ? absolutePath.substring(0, absolutePath.length - 1) : absolutePath;
|
||||
}
|
||||
}
|
||||
|
@ -1,246 +0,0 @@
|
||||
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;
|
||||
}
|
1
src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { createKeycloakAdapter } from "keycloakify/lib/keycloakJsAdapter";
|
@ -1,100 +0,0 @@
|
||||
import React, { lazy, Suspense } from "react";
|
||||
import { __unsafe_useI18n as useI18n } from "./i18n";
|
||||
import DefaultTemplate from "./Template";
|
||||
import type { KcContextBase } from "./getKcContext/KcContextBase";
|
||||
import type { PageProps } from "./KcProps";
|
||||
import type { I18nBase } from "./i18n";
|
||||
import type { SetOptional } from "./tools/SetOptional";
|
||||
|
||||
const Login = lazy(() => import("./pages/Login"));
|
||||
const Register = lazy(() => import("./pages/Register"));
|
||||
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
|
||||
const Info = lazy(() => import("./pages/Info"));
|
||||
const Error = lazy(() => import("./pages/Error"));
|
||||
const LoginResetPassword = lazy(() => import("./pages/LoginResetPassword"));
|
||||
const LoginVerifyEmail = lazy(() => import("./pages/LoginVerifyEmail"));
|
||||
const Terms = lazy(() => import("./pages/Terms"));
|
||||
const LoginOtp = lazy(() => import("./pages/LoginOtp"));
|
||||
const LoginPassword = lazy(() => import("./pages/LoginPassword"));
|
||||
const LoginUsername = lazy(() => import("./pages/LoginUsername"));
|
||||
const WebauthnAuthenticate = lazy(() => import("./pages/WebauthnAuthenticate"));
|
||||
const LoginUpdatePassword = lazy(() => import("./pages/LoginUpdatePassword"));
|
||||
const LoginUpdateProfile = lazy(() => import("./pages/LoginUpdateProfile"));
|
||||
const LoginIdpLinkConfirm = lazy(() => import("./pages/LoginIdpLinkConfirm"));
|
||||
const LoginPageExpired = lazy(() => import("./pages/LoginPageExpired"));
|
||||
const LoginIdpLinkEmail = lazy(() => import("./pages/LoginIdpLinkEmail"));
|
||||
const LoginConfigTotp = lazy(() => import("./pages/LoginConfigTotp"));
|
||||
const LogoutConfirm = lazy(() => import("./pages/LogoutConfirm"));
|
||||
const UpdateUserProfile = lazy(() => import("./pages/UpdateUserProfile"));
|
||||
const IdpReviewUserProfile = lazy(() => import("./pages/IdpReviewUserProfile"));
|
||||
|
||||
export default function KcApp(props_: SetOptional<PageProps<KcContextBase, I18nBase>, "Template">) {
|
||||
const { kcContext, i18n: userProvidedI18n, Template = DefaultTemplate, ...kcProps } = props_;
|
||||
|
||||
const i18n = (function useClosure() {
|
||||
const i18n = useI18n({
|
||||
kcContext,
|
||||
"extraMessages": {},
|
||||
"doSkip": userProvidedI18n !== undefined
|
||||
});
|
||||
|
||||
return userProvidedI18n ?? i18n;
|
||||
})();
|
||||
|
||||
if (i18n === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const commonProps = { i18n, Template, ...kcProps };
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{(() => {
|
||||
switch (kcContext.pageId) {
|
||||
case "login.ftl":
|
||||
return <Login {...{ kcContext, ...commonProps }} />;
|
||||
case "register.ftl":
|
||||
return <Register {...{ kcContext, ...commonProps }} />;
|
||||
case "register-user-profile.ftl":
|
||||
return <RegisterUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "info.ftl":
|
||||
return <Info {...{ kcContext, ...commonProps }} />;
|
||||
case "error.ftl":
|
||||
return <Error {...{ kcContext, ...commonProps }} />;
|
||||
case "login-reset-password.ftl":
|
||||
return <LoginResetPassword {...{ kcContext, ...commonProps }} />;
|
||||
case "login-verify-email.ftl":
|
||||
return <LoginVerifyEmail {...{ kcContext, ...commonProps }} />;
|
||||
case "terms.ftl":
|
||||
return <Terms {...{ kcContext, ...commonProps }} />;
|
||||
case "login-otp.ftl":
|
||||
return <LoginOtp {...{ kcContext, ...commonProps }} />;
|
||||
case "login-username.ftl":
|
||||
return <LoginUsername {...{ kcContext, ...commonProps }} />;
|
||||
case "login-password.ftl":
|
||||
return <LoginPassword {...{ kcContext, ...commonProps }} />;
|
||||
case "webauthn-authenticate.ftl":
|
||||
return <WebauthnAuthenticate {...{ kcContext, ...commonProps }} />;
|
||||
case "login-update-password.ftl":
|
||||
return <LoginUpdatePassword {...{ kcContext, ...commonProps }} />;
|
||||
case "login-update-profile.ftl":
|
||||
return <LoginUpdateProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "login-idp-link-confirm.ftl":
|
||||
return <LoginIdpLinkConfirm {...{ kcContext, ...commonProps }} />;
|
||||
case "login-idp-link-email.ftl":
|
||||
return <LoginIdpLinkEmail {...{ kcContext, ...commonProps }} />;
|
||||
case "login-page-expired.ftl":
|
||||
return <LoginPageExpired {...{ kcContext, ...commonProps }} />;
|
||||
case "login-config-totp.ftl":
|
||||
return <LoginConfigTotp {...{ kcContext, ...commonProps }} />;
|
||||
case "logout-confirm.ftl":
|
||||
return <LogoutConfirm {...{ kcContext, ...commonProps }} />;
|
||||
case "update-user-profile.ftl":
|
||||
return <UpdateUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
case "idp-review-user-profile.ftl":
|
||||
return <IdpReviewUserProfile {...{ kcContext, ...commonProps }} />;
|
||||
}
|
||||
})()}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
@ -1,239 +0,0 @@
|
||||
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
|
||||
import { assert } from "tsafe/assert";
|
||||
import type { KcContextBase } from "./getKcContext";
|
||||
import type { ReactNode } from "react";
|
||||
import { I18nBase } from "./i18n";
|
||||
|
||||
/** 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"
|
||||
| "kcWebAuthnDefaultIcon"
|
||||
| "kcFormClass"
|
||||
| "kcFormGroupErrorClass"
|
||||
| "kcLabelClass"
|
||||
| "kcInputClass"
|
||||
| "kcInputErrorMessageClass"
|
||||
| "kcInputWrapperClass"
|
||||
| "kcFormOptionsClass"
|
||||
| "kcFormButtonsClass"
|
||||
| "kcFormSettingClass"
|
||||
| "kcTextareaClass"
|
||||
| "kcInfoAreaClass"
|
||||
| "kcFormGroupHeader"
|
||||
| "kcButtonClass"
|
||||
| "kcButtonPrimaryClass"
|
||||
| "kcButtonDefaultClass"
|
||||
| "kcButtonLargeClass"
|
||||
| "kcButtonBlockClass"
|
||||
| "kcInputLargeClass"
|
||||
| "kcSrOnlyClass"
|
||||
| "kcSelectAuthListClass"
|
||||
| "kcSelectAuthListItemClass"
|
||||
| "kcSelectAuthListItemFillClass"
|
||||
| "kcSelectAuthListItemInfoClass"
|
||||
| "kcSelectAuthListItemLeftClass"
|
||||
| "kcSelectAuthListItemBodyClass"
|
||||
| "kcSelectAuthListItemDescriptionClass"
|
||||
| "kcSelectAuthListItemHeadingClass"
|
||||
| "kcSelectAuthListItemHelpTextClass"
|
||||
| "kcSelectAuthListItemIconPropertyClass"
|
||||
| "kcSelectAuthListItemIconClass"
|
||||
| "kcSelectAuthListItemTitle"
|
||||
| "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"],
|
||||
"kcWebAuthnDefaultIcon": ["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"],
|
||||
"kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
|
||||
"kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
|
||||
"kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
|
||||
"kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
|
||||
"kcSelectAuthListItemInfoClass": ["list-view-pf-main-info"],
|
||||
"kcSelectAuthListItemLeftClass": ["list-view-pf-left"],
|
||||
"kcSelectAuthListItemBodyClass": ["list-view-pf-body"],
|
||||
"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;
|
||||
|
||||
export type TemplateProps<KcContext extends KcContextBase.Common, I18n extends I18nBase> = {
|
||||
kcContext: KcContext;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources: boolean;
|
||||
} & {
|
||||
displayInfo?: boolean;
|
||||
displayMessage?: boolean;
|
||||
displayRequiredFields?: boolean;
|
||||
displayWide?: boolean;
|
||||
showAnotherWayIfPresent?: boolean;
|
||||
headerNode: ReactNode;
|
||||
showUsernameNode?: ReactNode;
|
||||
formNode: ReactNode;
|
||||
infoNode?: ReactNode;
|
||||
} & KcTemplateProps;
|
||||
|
||||
export type PageProps<KcContext, I18n extends I18nBase> = {
|
||||
kcContext: KcContext;
|
||||
i18n: I18n;
|
||||
doFetchDefaultThemeResources?: boolean;
|
||||
Template: (props: TemplateProps<any, any>) => JSX.Element | null;
|
||||
} & KcProps;
|
||||
|
||||
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,131 +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 "../../bin/tools/pathJoin";
|
||||
import { pathBasename } from "../tools/pathBasename";
|
||||
import { mockTestingResourcesCommonPath } from "../../bin/mockTestingResourcesPath";
|
||||
import { symToStr } from "tsafe/symToStr";
|
||||
|
||||
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
|
||||
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
|
||||
mockData?: readonly DeepPartial<ExtendsKcContextBase<KcContextExtended>>[];
|
||||
}): { kcContext: ExtendsKcContextBase<KcContextExtended> | undefined } {
|
||||
const { mockPageId, mockData } = params ?? {};
|
||||
|
||||
const realKcContext = getKcContextFromWindow<KcContextExtended>();
|
||||
|
||||
if (mockPageId !== undefined && realKcContext === undefined) {
|
||||
//TODO maybe trow if no mock fo custom page
|
||||
|
||||
console.log(
|
||||
[
|
||||
`%cKeycloakify: ${symToStr({ mockPageId })} set to ${mockPageId}.`,
|
||||
`If assets are missing make sure you have built your Keycloak theme at least once.`
|
||||
].join(" "),
|
||||
"background: red; color: yellow; font-size: medium"
|
||||
);
|
||||
|
||||
const kcContextDefaultMock = kcContextMocks.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
const partialKcContextCustomMock = mockData?.find(({ pageId }) => pageId === mockPageId);
|
||||
|
||||
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" ||
|
||||
partialKcContextCustomMock.pageId === "update-user-profile.ftl" ||
|
||||
partialKcContextCustomMock.pageId === "idp-review-user-profile.ftl"
|
||||
) {
|
||||
assert(
|
||||
kcContextDefaultMock?.pageId === "register-user-profile.ftl" ||
|
||||
kcContextDefaultMock?.pageId === "update-user-profile.ftl" ||
|
||||
kcContextDefaultMock?.pageId === "idp-review-user-profile.ftl"
|
||||
);
|
||||
|
||||
const { attributes } = kcContextDefaultMock.profile;
|
||||
|
||||
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
|
||||
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
|
||||
.forEach(partialAttribute => {
|
||||
const { name } = partialAttribute;
|
||||
|
||||
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
|
||||
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
|
||||
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { kcContext };
|
||||
}
|
||||
|
||||
if (realKcContext === undefined) {
|
||||
return { "kcContext": undefined };
|
||||
}
|
||||
|
||||
{
|
||||
const { url } = realKcContext;
|
||||
|
||||
url.resourcesCommonPath = pathJoin(url.resourcesPath, pathBasename(mockTestingResourcesCommonPath));
|
||||
}
|
||||
|
||||
return { "kcContext": realKcContext };
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import type { KcContextBase } from "./KcContextBase";
|
||||
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
|
||||
import { ftlValuesGlobalName } from "../../bin/keycloakify/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,137 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Desa",
|
||||
"doCancel": "Cancel·la",
|
||||
"doLogOutAllSessions": "Desconnecta de totes les sessions",
|
||||
"doRemove": "Elimina",
|
||||
"doAdd": "Afegeix",
|
||||
"doSignOut": "Desconnectar",
|
||||
"editAccountHtmlTitle": "Edita compte",
|
||||
"federatedIdentitiesHtmlTitle": "Identitats federades",
|
||||
"accountLogHtmlTitle": "Registre del compte",
|
||||
"changePasswordHtmlTitle": "Canvia contrasenya",
|
||||
"sessionsHtmlTitle": "Sessions",
|
||||
"accountManagementTitle": "Gestió de Compte Keycloak",
|
||||
"authenticatorTitle": "Autenticador",
|
||||
"applicationsHtmlTitle": "Aplicacions",
|
||||
"authenticatorCode": "Codi d'un sol ús",
|
||||
"email": "Email",
|
||||
"firstName": "Nom",
|
||||
"givenName": "Nom de pila",
|
||||
"fullName": "Nom complet",
|
||||
"lastName": "Cognoms",
|
||||
"familyName": "Cognom",
|
||||
"password": "Contrasenya",
|
||||
"passwordConfirm": "Confirma la contrasenya",
|
||||
"passwordNew": "Nova contrasenya",
|
||||
"username": "Usuari",
|
||||
"address": "Adreça",
|
||||
"street": "Carrer",
|
||||
"locality": "Ciutat o Municipi",
|
||||
"region": "Estat, Província, o Regió",
|
||||
"postal_code": "Postal code",
|
||||
"country": "País",
|
||||
"emailVerified": "Email verificat",
|
||||
"gssDelegationCredential": "GSS Delegation Credential",
|
||||
"role_admin": "Administrador",
|
||||
"role_realm-admin": "Administrador del domini",
|
||||
"role_create-realm": "Crear domini",
|
||||
"role_view-realm": "Veure domini",
|
||||
"role_view-users": "Veure usuaris",
|
||||
"role_view-applications": "Veure aplicacions",
|
||||
"role_view-clients": "Veure clients",
|
||||
"role_view-events": "Veure events",
|
||||
"role_view-identity-providers": "Veure proveïdors d'identitat",
|
||||
"role_manage-realm": "Gestionar domini",
|
||||
"role_manage-users": "Gestinar usuaris",
|
||||
"role_manage-applications": "Gestionar aplicacions",
|
||||
"role_manage-identity-providers": "Gestionar proveïdors d'identitat",
|
||||
"role_manage-clients": "Gestionar clients",
|
||||
"role_manage-events": "Gestionar events",
|
||||
"role_view-profile": "Veure perfil",
|
||||
"role_manage-account": "Gestionar compte",
|
||||
"role_read-token": "Llegir token",
|
||||
"role_offline-access": "Accés sense connexió",
|
||||
"client_account": "Compte",
|
||||
"client_security-admin-console": "Consola d'Administració de Seguretat",
|
||||
"client_realm-management": "Gestió de domini",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Camps obligatoris",
|
||||
"allFieldsRequired": "Tots els camps obligatoris",
|
||||
"backToApplication": "« Torna a l'aplicació",
|
||||
"backTo": "Torna a {0}",
|
||||
"date": "Data",
|
||||
"event": "Event",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"details": "Detalls",
|
||||
"started": "Iniciat",
|
||||
"lastAccess": "Últim accés",
|
||||
"expires": "Expira",
|
||||
"applications": "Aplicacions",
|
||||
"account": "Compte",
|
||||
"federatedIdentity": "Identitat federada",
|
||||
"authenticator": "Autenticador",
|
||||
"sessions": "Sessions",
|
||||
"log": "Registre",
|
||||
"application": "Aplicació",
|
||||
"availablePermissions": "Permisos disponibles",
|
||||
"grantedPermissions": "Permisos concedits",
|
||||
"grantedPersonalInfo": "Informació personal concedida",
|
||||
"additionalGrants": "Permisos addicionals",
|
||||
"action": "Acció",
|
||||
"inResource": "a",
|
||||
"fullAccess": "Accés total",
|
||||
"offlineToken": "Codi d'autorització offline",
|
||||
"revoke": "Revocar permís",
|
||||
"configureAuthenticators": "Autenticadors configurats",
|
||||
"mobile": "Mòbil",
|
||||
"totpStep1":
|
||||
'Instal·la <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> o Google Authenticator al teu telèfon mòbil. Les dues aplicacions estan disponibles a <a href="https://play.google.com">Google Play</a> i en l\'App Store d\'Apple.',
|
||||
"totpStep2": "Obre l'aplicació i escaneja el codi o introdueix la clau.",
|
||||
"totpStep3": "Introdueix el codi únic que et mostra l'aplicació d'autenticació i fes clic a Envia per finalitzar la configuració",
|
||||
"missingUsernameMessage": "Si us plau indica el teu usuari.",
|
||||
"missingFirstNameMessage": "Si us plau indica el nom.",
|
||||
"invalidEmailMessage": "Email no vàlid",
|
||||
"missingLastNameMessage": "Si us plau indica els teus cognoms.",
|
||||
"missingEmailMessage": "Si us plau indica l'email.",
|
||||
"missingPasswordMessage": "Si us plau indica la contrasenya.",
|
||||
"notMatchPasswordMessage": "Les contrasenyes no coincideixen.",
|
||||
"missingTotpMessage": "Si us plau indica el teu codi d'autenticació",
|
||||
"invalidPasswordExistingMessage": "La contrasenya actual no és correcta.",
|
||||
"invalidPasswordConfirmMessage": "La confirmació de contrasenya no coincideix.",
|
||||
"invalidTotpMessage": "El código de autenticación no es válido.",
|
||||
"usernameExistsMessage": "L'usuari ja existeix",
|
||||
"emailExistsMessage": "L'email ja existeix",
|
||||
"readOnlyUserMessage": "No pots actualitzar el teu usuari perquè el teu compte és de només lectura.",
|
||||
"readOnlyPasswordMessage": "No pots actualitzar la contrasenya perquè el teu compte és de només lectura.",
|
||||
"successTotpMessage": "Aplicació d'autenticació mòbil configurada.",
|
||||
"successTotpRemovedMessage": "Aplicació d'autenticació mòbil eliminada.",
|
||||
"successGrantRevokedMessage": "Permís revocat correctament",
|
||||
"accountUpdatedMessage": "El teu compte s'ha actualitzat.",
|
||||
"accountPasswordUpdatedMessage": "La contrasenya s'ha actualitzat.",
|
||||
"missingIdentityProviderMessage": "Proveïdor d'identitat no indicat.",
|
||||
"invalidFederatedIdentityActionMessage": "Acció no vàlida o no indicada.",
|
||||
"identityProviderNotFoundMessage": "No s'ha trobat un proveïdor d'identitat.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Aquesta identitat ja no està activa",
|
||||
"federatedIdentityRemovingLastProviderMessage": "No pots eliminar l'última identitat federada perquè no tens fixada una contrasenya.",
|
||||
"identityProviderRedirectErrorMessage": "Error en la redirecció al proveïdor d'identitat",
|
||||
"identityProviderRemovedMessage": "Proveïdor d'identitat esborrat correctament.",
|
||||
"accountDisabledMessage": "El compte està desactivada, contacteu amb l'administrador.",
|
||||
"accountTemporarilyDisabledMessage": "El compte està temporalment desactivat, contacta amb l'administrador o intenta-ho de nou més tard.",
|
||||
"invalidPasswordMinLengthMessage": "Contrasenya incorrecta: longitud mínima {0}.",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres minúscules.",
|
||||
"invalidPasswordMinDigitsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres numéricos.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
|
||||
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
|
||||
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
|
||||
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
|
||||
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,156 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Uložit",
|
||||
"doCancel": "Zrušit",
|
||||
"doLogOutAllSessions": "Odhlásit všechny relace",
|
||||
"doRemove": "Odstranit",
|
||||
"doAdd": "Přidat",
|
||||
"doSignOut": "Odhlásit se",
|
||||
"editAccountHtmlTitle": "Upravit účet",
|
||||
"federatedIdentitiesHtmlTitle": "Propojené identity",
|
||||
"accountLogHtmlTitle": "Log účtu",
|
||||
"changePasswordHtmlTitle": "Změnit heslo",
|
||||
"sessionsHtmlTitle": "Relace",
|
||||
"accountManagementTitle": "Správa účtů Keycloak",
|
||||
"authenticatorTitle": "Autentizátor",
|
||||
"applicationsHtmlTitle": "Aplikace",
|
||||
"authenticatorCode": "Jednorázový kód",
|
||||
"email": "E-mail",
|
||||
"firstName": "První křestní jméno",
|
||||
"givenName": "Křestní jména",
|
||||
"fullName": "Celé jméno",
|
||||
"lastName": "Příjmení",
|
||||
"familyName": "Rodinné jméno",
|
||||
"password": "Heslo",
|
||||
"passwordConfirm": "Nové heslo (znovu)",
|
||||
"passwordNew": "Nové heslo",
|
||||
"username": "Uživatelské jméno",
|
||||
"address": "Adresa",
|
||||
"street": "Ulice",
|
||||
"locality": "Město nebo lokalita",
|
||||
"region": "Kraj",
|
||||
"postal_code": "PSČ",
|
||||
"country": "Stát",
|
||||
"emailVerified": "E-mail ověřen",
|
||||
"gssDelegationCredential": "GSS delegované oprávnění",
|
||||
"role_admin": "Správce",
|
||||
"role_realm-admin": "Správce realmu",
|
||||
"role_create-realm": "Vytvořit realm",
|
||||
"role_view-realm": "Zobrazit realm",
|
||||
"role_view-users": "Zobrazit uživatele",
|
||||
"role_view-applications": "Zobrazit aplikace",
|
||||
"role_view-clients": "Zobrazit klienty",
|
||||
"role_view-events": "Zobrazit události",
|
||||
"role_view-identity-providers": "Zobrazit poskytovatele identity",
|
||||
"role_manage-realm": "Spravovat realm",
|
||||
"role_manage-users": "Spravovat uživatele",
|
||||
"role_manage-applications": "Spravovat aplikace",
|
||||
"role_manage-identity-providers": "Spravovat poskytovatele identity",
|
||||
"role_manage-clients": "Spravovat klienty",
|
||||
"role_manage-events": "Spravovat události",
|
||||
"role_view-profile": "Zobrazit profil",
|
||||
"role_manage-account": "Spravovat účet",
|
||||
"role_manage-account-links": "Spravovat odkazy na účet",
|
||||
"role_read-token": "Číst token",
|
||||
"role_offline-access": "Přístup offline",
|
||||
"role_uma_authorization": "Získání oprávnění",
|
||||
"client_account": "Účet",
|
||||
"client_security-admin-console": "Administrátorská bezpečnostní konzole",
|
||||
"client_admin-cli": "Administrátorské CLI",
|
||||
"client_realm-management": "Správa realmů",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Požadovaná pole",
|
||||
"allFieldsRequired": "Všechna pole vyžadovaná",
|
||||
"backToApplication": "« Zpět na aplikaci",
|
||||
"backTo": "Zpět na {0}",
|
||||
"date": "Datum",
|
||||
"event": "Událost",
|
||||
"ip": "IP",
|
||||
"client": "Klient",
|
||||
"clients": "Klienti",
|
||||
"details": "Podrobnosti",
|
||||
"started": "Zahájeno",
|
||||
"lastAccess": "Poslední přístup",
|
||||
"expires": "Vyprší",
|
||||
"applications": "Aplikace",
|
||||
"account": "Účet",
|
||||
"federatedIdentity": "Propojená identita",
|
||||
"authenticator": "Autentizátor",
|
||||
"sessions": "Relace",
|
||||
"log": "Log",
|
||||
"application": "Aplikace",
|
||||
"availablePermissions": "Dostupná oprávnění",
|
||||
"grantedPermissions": "Udělené oprávnění",
|
||||
"grantedPersonalInfo": "Poskytnuté osobní informace",
|
||||
"additionalGrants": "Dodatečné oprávnění",
|
||||
"action": "Akce",
|
||||
"inResource": "v",
|
||||
"fullAccess": "Úplný přístup",
|
||||
"offlineToken": "Offline Token",
|
||||
"revoke": "Zrušit oprávnění",
|
||||
"configureAuthenticators": "Konfigurované autentizátory",
|
||||
"mobile": "Mobilní",
|
||||
"totpStep1": "Nainstalujte jednu z následujících aplikací",
|
||||
"totpStep2": "Otevřete aplikaci a naskenujte čárový kód",
|
||||
"totpStep3": "Zadejte jednorázový kód poskytnutý aplikací a klepnutím na tlačítko Uložit dokončete nastavení.",
|
||||
"totpManualStep2": "Otevřete aplikaci a zadejte klíč",
|
||||
"totpManualStep3": "Použijte následující hodnoty konfigurace, pokud aplikace umožňuje jejich nastavení",
|
||||
"totpUnableToScan": "Nelze skenovat?",
|
||||
"totpScanBarcode": "Skenovat čárový kód?",
|
||||
"totp.totp": "Založeno na čase",
|
||||
"totp.hotp": "Založeno na čítači",
|
||||
"totpType": "Typ",
|
||||
"totpAlgorithm": "Algoritmus",
|
||||
"totpDigits": "Číslice",
|
||||
"totpInterval": "Interval",
|
||||
"totpCounter": "Čítač",
|
||||
"missingUsernameMessage": "Zadejte uživatelské jméno.",
|
||||
"missingFirstNameMessage": "Zadejte prosím křestní jméno.",
|
||||
"invalidEmailMessage": "Neplatná e-mailová adresa.",
|
||||
"missingLastNameMessage": "Zadejte prosím příjmení.",
|
||||
"missingEmailMessage": "Zadejte prosím e-mail.",
|
||||
"missingPasswordMessage": "Zadejte prosím heslo.",
|
||||
"notMatchPasswordMessage": "Hesla se neshodují.",
|
||||
"missingTotpMessage": "Zadejte prosím kód autentizátoru.",
|
||||
"invalidPasswordExistingMessage": "Neplatné stávající heslo.",
|
||||
"invalidPasswordConfirmMessage": "Nová hesla se neshodují.",
|
||||
"invalidTotpMessage": "Neplatný kód autentizátoru.",
|
||||
"usernameExistsMessage": "Uživatelské jméno již existuje.",
|
||||
"emailExistsMessage": "E-mail již existuje.",
|
||||
"readOnlyUserMessage": "Nemůžete svůj účet aktualizovat, protože je pouze pro čtení.",
|
||||
"readOnlyUsernameMessage": "Nemůžete aktualizovat své uživatelské jméno, protože je pouze pro čtení.",
|
||||
"readOnlyPasswordMessage": "Nemůžete aktualizovat své heslo, protože váš účet je jen pro čtení.",
|
||||
"successTotpMessage": "Ověření pomocí OTP úspěšně konfigurováno.",
|
||||
"successTotpRemovedMessage": "Ověření pomocí OTP úspěšně odstraněno.",
|
||||
"successGrantRevokedMessage": "Oprávnění bylo úspěšně zrušeno.",
|
||||
"accountUpdatedMessage": "Váš účet byl aktualizován.",
|
||||
"accountPasswordUpdatedMessage": "Vaše heslo bylo aktualizováno.",
|
||||
"missingIdentityProviderMessage": "Chybějící poskytovatel identity.",
|
||||
"invalidFederatedIdentityActionMessage": "Neplatná nebo chybějící akce.",
|
||||
"identityProviderNotFoundMessage": "Poskytovatel identity nenalezen.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Tato identita již není aktivní.",
|
||||
"federatedIdentityRemovingLastProviderMessage": "Nemůžete odstranit poslední propojenou identitu, protože nemáte heslo.",
|
||||
"identityProviderRedirectErrorMessage": "Nepodařilo se přesměrovat na poskytovatele identity.",
|
||||
"identityProviderRemovedMessage": "Poskytovatel identity byl úspěšně odstraněn.",
|
||||
"identityProviderAlreadyLinkedMessage": "Propojená identita vrácená uživatelem {0} je již propojena s jiným uživatelem.",
|
||||
"staleCodeAccountMessage": "Platnost vypršela. Zkuste to ještě jednou.",
|
||||
"consentDenied": "Souhlas byl zamítnut.",
|
||||
"accountDisabledMessage": "Účet je zakázán, kontaktujte správce.",
|
||||
"accountTemporarilyDisabledMessage": "Účet je dočasně zakázán, kontaktujte správce nebo zkuste to později.",
|
||||
"invalidPasswordMinLengthMessage": "Neplatné heslo: musí obsahovat minimálně {0} malých znaků.",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "Neplatné heslo: musí obsahovat minimálně {0} malé znaky.",
|
||||
"invalidPasswordMinDigitsMessage": "Neplatné heslo: musí obsahovat nejméně {0} číslic.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Neplatné heslo: musí obsahovat nejméně {0} velkých písmenen.",
|
||||
"invalidPasswordMinSpecialCharsMessage": "Neplatné heslo: musí obsahovat nejméně {0} speciálních znaků.",
|
||||
"invalidPasswordNotUsernameMessage": "Neplatné heslo: nesmí být totožné s uživatelským jménem.",
|
||||
"invalidPasswordRegexPatternMessage": "Neplatné heslo: neshoduje se zadaným regulárním výrazem.",
|
||||
"invalidPasswordHistoryMessage": "Neplatné heslo: Nesmí se opakovat žádné z posledních {0} hesel.",
|
||||
"invalidPasswordBlacklistedMessage": "Neplatné heslo: heslo je na černé listině.",
|
||||
"invalidPasswordGenericMessage": "Neplatné heslo: nové heslo neodpovídá pravidlům hesla."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,156 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Speichern",
|
||||
"doCancel": "Abbrechen",
|
||||
"doLogOutAllSessions": "Alle Sitzungen abmelden",
|
||||
"doRemove": "Entfernen",
|
||||
"doAdd": "Hinzufügen",
|
||||
"doSignOut": "Abmelden",
|
||||
"editAccountHtmlTitle": "Benutzerkonto bearbeiten",
|
||||
"federatedIdentitiesHtmlTitle": "Föderierte Identitäten",
|
||||
"accountLogHtmlTitle": "Benutzerkonto Log",
|
||||
"changePasswordHtmlTitle": "Passwort Ändern",
|
||||
"sessionsHtmlTitle": "Sitzungen",
|
||||
"accountManagementTitle": "Keycloak Benutzerkontoverwaltung",
|
||||
"authenticatorTitle": "Mehrfachauthentifizierung",
|
||||
"applicationsHtmlTitle": "Applikationen",
|
||||
"authenticatorCode": "One-time Code",
|
||||
"email": "E-Mail",
|
||||
"firstName": "Vorname",
|
||||
"givenName": "Vorname",
|
||||
"fullName": "Voller Name",
|
||||
"lastName": "Nachname",
|
||||
"familyName": "Nachname",
|
||||
"password": "Passwort",
|
||||
"passwordConfirm": "Passwort bestätigen",
|
||||
"passwordNew": "Neues Passwort",
|
||||
"username": "Benutzername",
|
||||
"address": "Adresse",
|
||||
"street": "Straße",
|
||||
"region": "Staat, Provinz, Region",
|
||||
"postal_code": "PLZ",
|
||||
"locality": "Stadt oder Ortschaft",
|
||||
"country": "Land",
|
||||
"emailVerified": "E-Mail verifiziert",
|
||||
"gssDelegationCredential": "GSS delegierte Berechtigung",
|
||||
"role_admin": "Admin",
|
||||
"role_realm-admin": "Realm Admin",
|
||||
"role_create-realm": "Realm erstellen",
|
||||
"role_view-realm": "Realm ansehen",
|
||||
"role_view-users": "Benutzer ansehen",
|
||||
"role_view-applications": "Applikationen ansehen",
|
||||
"role_view-clients": "Clients ansehen",
|
||||
"role_view-events": "Events ansehen",
|
||||
"role_view-identity-providers": "Identity Provider ansehen",
|
||||
"role_manage-realm": "Realm verwalten",
|
||||
"role_manage-users": "Benutzer verwalten",
|
||||
"role_manage-applications": "Applikationen verwalten",
|
||||
"role_manage-identity-providers": "Identity Provider verwalten",
|
||||
"role_manage-clients": "Clients verwalten",
|
||||
"role_manage-events": "Events verwalten",
|
||||
"role_view-profile": "Profile ansehen",
|
||||
"role_manage-account": "Profile verwalten",
|
||||
"role_manage-account-links": "Profil-Links verwalten",
|
||||
"role_read-token": "Token lesen",
|
||||
"role_offline-access": "Offline-Zugriff",
|
||||
"role_uma_authorization": "Berechtigungen einholen",
|
||||
"client_account": "Clientkonto",
|
||||
"client_security-admin-console": "Security Adminkonsole",
|
||||
"client_realm-management": "Realm-Management",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Erforderliche Felder",
|
||||
"allFieldsRequired": "Alle Felder sind erforderlich",
|
||||
"backToApplication": "« Zurück zur Applikation",
|
||||
"backTo": "Zurück zu {0}",
|
||||
"date": "Datum",
|
||||
"event": "Ereignis",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"details": "Details",
|
||||
"started": "Startdatum",
|
||||
"lastAccess": "Letzter Zugriff",
|
||||
"expires": "Ablaufdatum",
|
||||
"applications": "Applikationen",
|
||||
"account": "Benutzerkonto",
|
||||
"federatedIdentity": "Föderierte Identität",
|
||||
"authenticator": "Mehrfachauthentifizierung",
|
||||
"sessions": "Sitzungen",
|
||||
"log": "Log",
|
||||
"application": "Applikation",
|
||||
"availablePermissions": "verfügbare Berechtigungen",
|
||||
"grantedPermissions": "gewährte Berechtigungen",
|
||||
"grantedPersonalInfo": "gewährte persönliche Informationen",
|
||||
"additionalGrants": "zusätzliche Berechtigungen",
|
||||
"action": "Aktion",
|
||||
"inResource": "in",
|
||||
"fullAccess": "Vollzugriff",
|
||||
"offlineToken": "Offline-Token",
|
||||
"revoke": "Berechtigung widerrufen",
|
||||
"configureAuthenticators": "Mehrfachauthentifizierung konfigurieren",
|
||||
"mobile": "Mobil",
|
||||
"totpStep1": "Installieren Sie eine der folgenden Applikationen auf Ihrem Smartphone:",
|
||||
"totpStep2": "Öffnen Sie die Applikation und scannen Sie den Barcode.",
|
||||
"totpStep3": "Geben Sie den von der Applikation generierten One-time Code ein und klicken Sie auf Speichern.",
|
||||
"totpManualStep2": "Öffnen Sie die Applikation und geben Sie den folgenden Schlüssel ein.",
|
||||
"totpManualStep3": "Verwenden Sie die folgenden Konfigurationswerte, falls Sie diese für die Applikation anpassen können:",
|
||||
"totpUnableToScan": "Sie können den Barcode nicht scannen?",
|
||||
"totpScanBarcode": "Barcode scannen?",
|
||||
"totp.totp": "zeitbasiert (time-based)",
|
||||
"totp.hotp": "zählerbasiert (counter-based)",
|
||||
"totpType": "Typ",
|
||||
"totpAlgorithm": "Algorithmus",
|
||||
"totpDigits": "Ziffern",
|
||||
"totpInterval": "Intervall",
|
||||
"totpCounter": "Zähler",
|
||||
"missingUsernameMessage": "Bitte geben Sie einen Benutzernamen ein.",
|
||||
"missingFirstNameMessage": "Bitte geben Sie einen Vornamen ein.",
|
||||
"invalidEmailMessage": "Ungültige E-Mail Adresse.",
|
||||
"missingLastNameMessage": "Bitte geben Sie einen Nachnamen ein.",
|
||||
"missingEmailMessage": "Bitte geben Sie eine E-Mail Adresse ein.",
|
||||
"missingPasswordMessage": "Bitte geben Sie ein Passwort ein.",
|
||||
"notMatchPasswordMessage": "Die Passwörter sind nicht identisch.",
|
||||
"missingTotpMessage": "Bitte geben Sie den One-time Code ein.",
|
||||
"invalidPasswordExistingMessage": "Das aktuelle Passwort ist ungültig.",
|
||||
"invalidPasswordConfirmMessage": "Die Passwortbestätigung ist nicht identisch.",
|
||||
"invalidTotpMessage": "Ungültiger One-time Code.",
|
||||
"usernameExistsMessage": "Der Benutzername existiert bereits.",
|
||||
"emailExistsMessage": "Die E-Mail-Adresse existiert bereits.",
|
||||
"readOnlyUserMessage": "Sie können Ihr Benutzerkonto nicht ändern, da es schreibgeschützt ist.",
|
||||
"readOnlyUsernameMessage": "Sie können Ihren Benutzernamen nicht ändern, da er schreibgeschützt ist.",
|
||||
"readOnlyPasswordMessage": "Sie können Ihr Passwort nicht ändern, da es schreibgeschützt ist.",
|
||||
"successTotpMessage": "Mehrfachauthentifizierung erfolgreich konfiguriert.",
|
||||
"successTotpRemovedMessage": "Mehrfachauthentifizierung erfolgreich entfernt.",
|
||||
"successGrantRevokedMessage": "Berechtigung erfolgreich widerrufen.",
|
||||
"accountUpdatedMessage": "Ihr Benutzerkonto wurde aktualisiert.",
|
||||
"accountPasswordUpdatedMessage": "Ihr Passwort wurde aktualisiert.",
|
||||
"missingIdentityProviderMessage": "Identity Provider nicht angegeben.",
|
||||
"invalidFederatedIdentityActionMessage": "Ungültige oder fehlende Aktion.",
|
||||
"identityProviderNotFoundMessage": "Angegebener Identity Provider nicht gefunden.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Diese Identität ist nicht mehr aktiv.",
|
||||
"federatedIdentityRemovingLastProviderMessage": "Sie können den letzten Eintrag nicht entfernen, da Sie kein Passwort haben.",
|
||||
"identityProviderRedirectErrorMessage": "Fehler bei der Weiterleitung zum Identity Provider.",
|
||||
"identityProviderRemovedMessage": "Identity Provider erfolgreich entfernt.",
|
||||
"identityProviderAlreadyLinkedMessage": "Die föderierte Identität von {0} ist bereits einem anderen Benutzer zugewiesen.",
|
||||
"staleCodeAccountMessage": "Diese Seite ist nicht mehr gültig, bitte versuchen Sie es noch einmal.",
|
||||
"consentDenied": "Einverständnis verweigert.",
|
||||
"accountDisabledMessage": "Ihr Benutzerkonto ist gesperrt, bitte kontaktieren Sie den Admin.",
|
||||
"accountTemporarilyDisabledMessage":
|
||||
"Ihr Benutzerkonto ist temporär gesperrt, bitte kontaktieren Sie den Admin oder versuchen Sie es später noch einmal.",
|
||||
"invalidPasswordMinLengthMessage": "Ungültiges Passwort: Es muss mindestens {0} Zeichen lang sein.",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "Ungültiges Passwort: Es muss mindestens {0} Kleinbuchstaben beinhalten.",
|
||||
"invalidPasswordMinDigitsMessage": "Ungültiges Passwort: Es muss mindestens {0} Zahl(en) beinhalten.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Ungültiges Passwort: Es muss mindestens {0} Großbuchstaben beinhalten.",
|
||||
"invalidPasswordMinSpecialCharsMessage": "Ungültiges Passwort: Es muss mindestens {0} Sonderzeichen beinhalten.",
|
||||
"invalidPasswordNotUsernameMessage": "Ungültiges Passwort: Es darf nicht gleich sein wie der Benutzername.",
|
||||
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: Es entspricht nicht dem Regex-Muster.",
|
||||
"invalidPasswordHistoryMessage": "Ungültiges Passwort: Es darf nicht einem der letzten {0} Passwörter entsprechen.",
|
||||
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Das Passwort steht auf der Blocklist (schwarzen Liste).",
|
||||
"invalidPasswordGenericMessge": "Ungültiges Passwort: Das neue Passwort verletzt die Passwort-Richtlinien."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,325 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Save",
|
||||
"doCancel": "Cancel",
|
||||
"doLogOutAllSessions": "Log out all sessions",
|
||||
"doRemove": "Remove",
|
||||
"doAdd": "Add",
|
||||
"doSignOut": "Sign Out",
|
||||
"doLogIn": "Log In",
|
||||
"doLink": "Link",
|
||||
"editAccountHtmlTitle": "Edit Account",
|
||||
"personalInfoHtmlTitle": "Personal Info",
|
||||
"federatedIdentitiesHtmlTitle": "Federated Identities",
|
||||
"accountLogHtmlTitle": "Account Log",
|
||||
"changePasswordHtmlTitle": "Change Password",
|
||||
"deviceActivityHtmlTitle": "Device Activity",
|
||||
"sessionsHtmlTitle": "Sessions",
|
||||
"accountManagementTitle": "Keycloak Account Management",
|
||||
"authenticatorTitle": "Authenticator",
|
||||
"applicationsHtmlTitle": "Applications",
|
||||
"linkedAccountsHtmlTitle": "Linked Accounts",
|
||||
"accountManagementWelcomeMessage": "Welcome to Keycloak Account Management",
|
||||
"personalInfoIntroMessage": "Manage your basic information",
|
||||
"accountSecurityTitle": "Account Security",
|
||||
"accountSecurityIntroMessage": "Control your password and account access",
|
||||
"applicationsIntroMessage": "Track and manage your app permission to access your account",
|
||||
"resourceIntroMessage": "Share your resources among team members",
|
||||
"passwordLastUpdateMessage": "Your password was updated at",
|
||||
"updatePasswordTitle": "Update Password",
|
||||
"updatePasswordMessageTitle": "Make sure you choose a strong password",
|
||||
"updatePasswordMessage":
|
||||
"A strong password contains a mix of numbers, letters, and symbols. It is hard to guess, does not resemble a real word, and is only used for this account.",
|
||||
"personalSubTitle": "Your Personal Info",
|
||||
"personalSubMessage": "Manage this basic information: your first name, last name and email",
|
||||
"authenticatorCode": "One-time code",
|
||||
"email": "Email",
|
||||
"firstName": "First name",
|
||||
"givenName": "Given name",
|
||||
"fullName": "Full name",
|
||||
"lastName": "Last name",
|
||||
"familyName": "Family name",
|
||||
"password": "Password",
|
||||
"currentPassword": "Current Password",
|
||||
"passwordConfirm": "Confirmation",
|
||||
"passwordNew": "New Password",
|
||||
"username": "Username",
|
||||
"address": "Address",
|
||||
"street": "Street",
|
||||
"locality": "City or Locality",
|
||||
"region": "State, Province, or Region",
|
||||
"postal_code": "Zip or Postal code",
|
||||
"country": "Country",
|
||||
"emailVerified": "Email verified",
|
||||
"gssDelegationCredential": "GSS Delegation Credential",
|
||||
"profileScopeConsentText": "User profile",
|
||||
"emailScopeConsentText": "Email address",
|
||||
"addressScopeConsentText": "Address",
|
||||
"phoneScopeConsentText": "Phone number",
|
||||
"offlineAccessScopeConsentText": "Offline Access",
|
||||
"samlRoleListScopeConsentText": "My Roles",
|
||||
"rolesScopeConsentText": "User roles",
|
||||
"role_admin": "Admin",
|
||||
"role_realm-admin": "Realm Admin",
|
||||
"role_create-realm": "Create realm",
|
||||
"role_view-realm": "View realm",
|
||||
"role_view-users": "View users",
|
||||
"role_view-applications": "View applications",
|
||||
"role_view-clients": "View clients",
|
||||
"role_view-events": "View events",
|
||||
"role_view-identity-providers": "View identity providers",
|
||||
"role_view-consent": "View consents",
|
||||
"role_manage-realm": "Manage realm",
|
||||
"role_manage-users": "Manage users",
|
||||
"role_manage-applications": "Manage applications",
|
||||
"role_manage-identity-providers": "Manage identity providers",
|
||||
"role_manage-clients": "Manage clients",
|
||||
"role_manage-events": "Manage events",
|
||||
"role_view-profile": "View profile",
|
||||
"role_manage-account": "Manage account",
|
||||
"role_manage-account-links": "Manage account links",
|
||||
"role_manage-consent": "Manage consents",
|
||||
"role_read-token": "Read token",
|
||||
"role_offline-access": "Offline access",
|
||||
"role_uma_authorization": "Obtain permissions",
|
||||
"client_account": "Account",
|
||||
"client_account-console": "Account Console",
|
||||
"client_security-admin-console": "Security Admin Console",
|
||||
"client_admin-cli": "Admin CLI",
|
||||
"client_realm-management": "Realm Management",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Required fields",
|
||||
"allFieldsRequired": "All fields required",
|
||||
"backToApplication": "« Back to application",
|
||||
"backTo": "Back to {0}",
|
||||
"date": "Date",
|
||||
"event": "Event",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"details": "Details",
|
||||
"started": "Started",
|
||||
"lastAccess": "Last Access",
|
||||
"expires": "Expires",
|
||||
"applications": "Applications",
|
||||
"account": "Account",
|
||||
"federatedIdentity": "Federated Identity",
|
||||
"authenticator": "Authenticator",
|
||||
"device-activity": "Device Activity",
|
||||
"sessions": "Sessions",
|
||||
"log": "Log",
|
||||
"application": "Application",
|
||||
"availableRoles": "Available Roles",
|
||||
"grantedPermissions": "Granted Permissions",
|
||||
"grantedPersonalInfo": "Granted Personal Info",
|
||||
"additionalGrants": "Additional Grants",
|
||||
"action": "Action",
|
||||
"inResource": "in",
|
||||
"fullAccess": "Full Access",
|
||||
"offlineToken": "Offline Token",
|
||||
"revoke": "Revoke Grant",
|
||||
"configureAuthenticators": "Configured Authenticators",
|
||||
"mobile": "Mobile",
|
||||
"totpStep1": "Install one of the following applications on your mobile:",
|
||||
"totpStep2": "Open the application and scan the barcode:",
|
||||
"totpStep3": "Enter the one-time code provided by the application and click Save to finish the setup.",
|
||||
"totpStep3DeviceName": "Provide a Device Name to help you manage your OTP devices.",
|
||||
"totpManualStep2": "Open the application and enter the key:",
|
||||
"totpManualStep3": "Use the following configuration values if the application allows setting them:",
|
||||
"totpUnableToScan": "Unable to scan?",
|
||||
"totpScanBarcode": "Scan barcode?",
|
||||
"totp.totp": "Time-based",
|
||||
"totp.hotp": "Counter-based",
|
||||
"totpType": "Type",
|
||||
"totpAlgorithm": "Algorithm",
|
||||
"totpDigits": "Digits",
|
||||
"totpInterval": "Interval",
|
||||
"totpCounter": "Counter",
|
||||
"totpDeviceName": "Device Name",
|
||||
"missingUsernameMessage": "Please specify username.",
|
||||
"missingFirstNameMessage": "Please specify first name.",
|
||||
"invalidEmailMessage": "Invalid email address.",
|
||||
"missingLastNameMessage": "Please specify last name.",
|
||||
"missingEmailMessage": "Please specify email.",
|
||||
"missingPasswordMessage": "Please specify password.",
|
||||
"notMatchPasswordMessage": "Passwords don't match.",
|
||||
"invalidUserMessage": "Invalid user",
|
||||
"missingTotpMessage": "Please specify authenticator code.",
|
||||
"missingTotpDeviceNameMessage": "Please specify device name.",
|
||||
"invalidPasswordExistingMessage": "Invalid existing password.",
|
||||
"invalidPasswordConfirmMessage": "Password confirmation doesn't match.",
|
||||
"invalidTotpMessage": "Invalid authenticator code.",
|
||||
"usernameExistsMessage": "Username already exists.",
|
||||
"emailExistsMessage": "Email already exists.",
|
||||
"readOnlyUserMessage": "You can't update your account as it is read-only.",
|
||||
"readOnlyUsernameMessage": "You can't update your username as it is read-only.",
|
||||
"readOnlyPasswordMessage": "You can't update your password as your account is read-only.",
|
||||
"successTotpMessage": "Mobile authenticator configured.",
|
||||
"successTotpRemovedMessage": "Mobile authenticator removed.",
|
||||
"successGrantRevokedMessage": "Grant revoked successfully.",
|
||||
"accountUpdatedMessage": "Your account has been updated.",
|
||||
"accountPasswordUpdatedMessage": "Your password has been updated.",
|
||||
"missingIdentityProviderMessage": "Identity provider not specified.",
|
||||
"invalidFederatedIdentityActionMessage": "Invalid or missing action.",
|
||||
"identityProviderNotFoundMessage": "Specified identity provider not found.",
|
||||
"federatedIdentityLinkNotActiveMessage": "This identity is not active anymore.",
|
||||
"federatedIdentityRemovingLastProviderMessage": "You can't remove last federated identity as you don't have a password.",
|
||||
"identityProviderRedirectErrorMessage": "Failed to redirect to identity provider.",
|
||||
"identityProviderRemovedMessage": "Identity provider removed successfully.",
|
||||
"identityProviderAlreadyLinkedMessage": "Federated identity returned by {0} is already linked to another user.",
|
||||
"staleCodeAccountMessage": "The page expired. Please try one more time.",
|
||||
"consentDenied": "Consent denied.",
|
||||
"accountDisabledMessage": "Account is disabled, contact your administrator.",
|
||||
"accountTemporarilyDisabledMessage": "Account is temporarily disabled, contact your administrator or try again later.",
|
||||
"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 doesn't match password policies.",
|
||||
"myResources": "My Resources",
|
||||
"myResourcesSub": "My resources",
|
||||
"doDeny": "Deny",
|
||||
"doRevoke": "Revoke",
|
||||
"doApprove": "Approve",
|
||||
"doRemoveSharing": "Remove Sharing",
|
||||
"doRemoveRequest": "Remove Request",
|
||||
"peopleAccessResource": "People with access to this resource",
|
||||
"resourceManagedPolicies": "Permissions granting access to this resource",
|
||||
"resourceNoPermissionsGrantingAccess": "No permissions granting access to this resource",
|
||||
"anyAction": "Any action",
|
||||
"description": "Description",
|
||||
"name": "Name",
|
||||
"scopes": "Scopes",
|
||||
"resource": "Resource",
|
||||
"user": "User",
|
||||
"peopleSharingThisResource": "People sharing this resource",
|
||||
"shareWithOthers": "Share with others",
|
||||
"needMyApproval": "Need my approval",
|
||||
"requestsWaitingApproval": "Your requests waiting approval",
|
||||
"icon": "Icon",
|
||||
"requestor": "Requestor",
|
||||
"owner": "Owner",
|
||||
"resourcesSharedWithMe": "Resources shared with me",
|
||||
"permissionRequestion": "Permission Requestion",
|
||||
"permission": "Permission",
|
||||
"shares": "share(s)",
|
||||
"notBeingShared": "This resource is not being shared.",
|
||||
"notHaveAnyResource": "You don't have any resources",
|
||||
"noResourcesSharedWithYou": "There are no resources shared with you",
|
||||
"havePermissionRequestsWaitingForApproval": "You have {0} permission request(s) waiting for approval.",
|
||||
"clickHereForDetails": "Click here for details.",
|
||||
"resourceIsNotBeingShared": "The resource is not being shared",
|
||||
"locale_ca": "Català",
|
||||
"locale_cs": "Čeština",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "English",
|
||||
"locale_es": "Español",
|
||||
"locale_fr": "Français",
|
||||
"locale_it": "Italian",
|
||||
"locale_ja": "日本語",
|
||||
"locale_nl": "Nederlands",
|
||||
"locale_no": "Norsk",
|
||||
"locale_lt": "Lietuvių",
|
||||
"locale_pt-BR": "Português (Brasil)",
|
||||
"locale_ru": "Русский",
|
||||
"locale_sk": "Slovenčina",
|
||||
"locale_sv": "Svenska",
|
||||
"locale_tr": "Turkish",
|
||||
"locale_zh-CN": "中文简体",
|
||||
"applicaitonName": "Name",
|
||||
"applicationType": "Application Type",
|
||||
"applicationInUse": "In-use app only",
|
||||
"clearAllFilter": "Clear all filters",
|
||||
"activeFilters": "Active filters",
|
||||
"filterByName": "Filter By Name ...",
|
||||
"allApps": "All applications",
|
||||
"internalApps": "Internal applications",
|
||||
"thirdpartyApps": "Third-Party applications",
|
||||
"appResults": "Results",
|
||||
"clientNotFoundMessage": "Client not found.",
|
||||
"authorizedProvider": "Authorized Provider",
|
||||
"authorizedProviderMessage": "Authorized Providers linked with your account",
|
||||
"identityProvider": "Identity Provider",
|
||||
"identityProviderMessage": "To link your account with identity providers you have configured",
|
||||
"socialLogin": "Social Login",
|
||||
"userDefined": "User Defined",
|
||||
"removeAccess": "Remove Access",
|
||||
"removeAccessMessage": "You will need to grant access again, if you want to use this app account.",
|
||||
"authenticatorStatusMessage": "Two-factor authentication is currently",
|
||||
"authenticatorFinishSetUpTitle": "Your Two-Factor Authentication",
|
||||
"authenticatorFinishSetUpMessage":
|
||||
"Each time you sign in to your Keycloak account, you will be asked to provide a two-factor authentication code.",
|
||||
"authenticatorSubTitle": "Set Up Two-Factor Authentication",
|
||||
"authenticatorSubMessage": "To enhance the security of your account, enable at least one of the available two-factor authentication methods.",
|
||||
"authenticatorMobileTitle": "Mobile Authenticator",
|
||||
"authenticatorMobileMessage": "Use mobile Authenticator to get Verification codes as the two-factor authentication.",
|
||||
"authenticatorMobileFinishSetUpMessage": "The authenticator has been bound to your phone.",
|
||||
"authenticatorActionSetup": "Set up",
|
||||
"authenticatorSMSTitle": "SMS Code",
|
||||
"authenticatorSMSMessage": "Keycloak will send the Verification code to your phone as the two-factor authentication.",
|
||||
"authenticatorSMSFinishSetUpMessage": "Text messages are sent to",
|
||||
"authenticatorDefaultStatus": "Default",
|
||||
"authenticatorChangePhone": "Change Phone Number",
|
||||
"authenticatorBackupCodesTitle": "Backup Codes",
|
||||
"authenticatorBackupCodesMessage": "Get your 8-digit backup codes",
|
||||
"authenticatorBackupCodesFinishSetUpMessage": "12 backup codes were generated at this time. Each one can be used once.",
|
||||
"authenticatorMobileSetupTitle": "Mobile Authenticator Setup",
|
||||
"smscodeIntroMessage": "Enter your phone number and a verification code will be sent to your phone.",
|
||||
"mobileSetupStep1": "Install an authenticator application on your phone. The applications listed here are supported.",
|
||||
"mobileSetupStep2": "Open the application and scan the barcode:",
|
||||
"mobileSetupStep3": "Enter the one-time code provided by the application and click Save to finish the setup.",
|
||||
"scanBarCode": "Want to scan the barcode?",
|
||||
"enterBarCode": "Enter the one-time code",
|
||||
"doCopy": "Copy",
|
||||
"doFinish": "Finish",
|
||||
"authenticatorSMSCodeSetupTitle": "SMS Code Setup",
|
||||
"chooseYourCountry": "Choose your country",
|
||||
"enterYourPhoneNumber": "Enter your phone number",
|
||||
"sendVerficationCode": "Send Verification Code",
|
||||
"enterYourVerficationCode": "Enter your verification code",
|
||||
"authenticatorBackupCodesSetupTitle": "Backup Codes Setup",
|
||||
"backupcodesIntroMessage":
|
||||
"If you lose access to your phone, you can still log into your account through backup codes. Keep them somewhere safe and accessible.",
|
||||
"realmName": "Realm",
|
||||
"doDownload": "Download",
|
||||
"doPrint": "Print",
|
||||
"backupCodesTips-1": "Each backup code can be used once.",
|
||||
"backupCodesTips-2": "These codes were generated on",
|
||||
"generateNewBackupCodes": "Generate New Backup Codes",
|
||||
"backupCodesTips-3": "When you generate new backup codes, the current codes will not work anymore.",
|
||||
"backtoAuthenticatorPage": "Back to Authenticator Page",
|
||||
"resources": "Resources",
|
||||
"sharedwithMe": "Shared with Me",
|
||||
"share": "Share",
|
||||
"sharedwith": "Shared with",
|
||||
"accessPermissions": "Access Permissions",
|
||||
"permissionRequests": "Permission Requests",
|
||||
"approve": "Approve",
|
||||
"approveAll": "Approve all",
|
||||
"people": "people",
|
||||
"perPage": "per page",
|
||||
"currentPage": "Current Page",
|
||||
"sharetheResource": "Share the resource",
|
||||
"group": "Group",
|
||||
"selectPermission": "Select Permission",
|
||||
"addPeople": "Add people to share your resource with",
|
||||
"addTeam": "Add team to share your resource with",
|
||||
"myPermissions": "My Permissions",
|
||||
"waitingforApproval": "Waiting for approval",
|
||||
"anyPermission": "Any Permission",
|
||||
"openshift.scope.user_info": "User information",
|
||||
"openshift.scope.user_check-access": "User access information",
|
||||
"openshift.scope.user_full": "Full Access",
|
||||
"openshift.scope.list-projects": "List projects"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,137 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Guardar",
|
||||
"doCancel": "Cancelar",
|
||||
"doLogOutAllSessions": "Desconectar de todas las sesiones",
|
||||
"doRemove": "Eliminar",
|
||||
"doAdd": "Añadir",
|
||||
"doSignOut": "Desconectar",
|
||||
"editAccountHtmlTitle": "Editar cuenta",
|
||||
"federatedIdentitiesHtmlTitle": "Identidades federadas",
|
||||
"accountLogHtmlTitle": "Registro de la cuenta",
|
||||
"changePasswordHtmlTitle": "Cambiar contraseña",
|
||||
"sessionsHtmlTitle": "Sesiones",
|
||||
"accountManagementTitle": "Gestión de Cuenta Keycloak",
|
||||
"authenticatorTitle": "Autenticador",
|
||||
"applicationsHtmlTitle": "Aplicaciones",
|
||||
"authenticatorCode": "Código de un solo uso",
|
||||
"email": "Email",
|
||||
"firstName": "Nombre",
|
||||
"givenName": "Nombre de pila",
|
||||
"fullName": "Nombre completo",
|
||||
"lastName": "Apellidos",
|
||||
"familyName": "Apellido",
|
||||
"password": "Contraseña",
|
||||
"passwordConfirm": "Confirma la contraseña",
|
||||
"passwordNew": "Nueva contraseña",
|
||||
"username": "Usuario",
|
||||
"address": "Dirección",
|
||||
"street": "Calle",
|
||||
"locality": "Ciudad o Municipio",
|
||||
"region": "Estado, Provincia, o Región",
|
||||
"postal_code": "Código Postal",
|
||||
"country": "País",
|
||||
"emailVerified": "Email verificado",
|
||||
"gssDelegationCredential": "GSS Delegation Credential",
|
||||
"role_admin": "Administrador",
|
||||
"role_realm-admin": "Administrador del dominio",
|
||||
"role_create-realm": "Crear dominio",
|
||||
"role_view-realm": "Ver dominio",
|
||||
"role_view-users": "Ver usuarios",
|
||||
"role_view-applications": "Ver aplicaciones",
|
||||
"role_view-clients": "Ver clientes",
|
||||
"role_view-events": "Ver eventos",
|
||||
"role_view-identity-providers": "Ver proveedores de identidad",
|
||||
"role_manage-realm": "Gestionar dominio",
|
||||
"role_manage-users": "Gestionar usuarios",
|
||||
"role_manage-applications": "Gestionar aplicaciones",
|
||||
"role_manage-identity-providers": "Gestionar proveedores de identidad",
|
||||
"role_manage-clients": "Gestionar clientes",
|
||||
"role_manage-events": "Gestionar eventos",
|
||||
"role_view-profile": "Ver perfil",
|
||||
"role_manage-account": "Gestionar cuenta",
|
||||
"role_read-token": "Leer token",
|
||||
"role_offline-access": "Acceso sin conexión",
|
||||
"client_account": "Cuenta",
|
||||
"client_security-admin-console": "Consola de Administración de Seguridad",
|
||||
"client_realm-management": "Gestión de dominio",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Campos obligatorios",
|
||||
"allFieldsRequired": "Todos los campos obligatorios",
|
||||
"backToApplication": "« Volver a la aplicación",
|
||||
"backTo": "Volver a {0}",
|
||||
"date": "Fecha",
|
||||
"event": "Evento",
|
||||
"ip": "IP",
|
||||
"client": "Cliente",
|
||||
"clients": "Clientes",
|
||||
"details": "Detalles",
|
||||
"started": "Iniciado",
|
||||
"lastAccess": "Último acceso",
|
||||
"expires": "Expira",
|
||||
"applications": "Aplicaciones",
|
||||
"account": "Cuenta",
|
||||
"federatedIdentity": "Identidad federada",
|
||||
"authenticator": "Autenticador",
|
||||
"sessions": "Sesiones",
|
||||
"log": "Regisro",
|
||||
"application": "Aplicación",
|
||||
"availablePermissions": "Permisos disponibles",
|
||||
"grantedPermissions": "Permisos concedidos",
|
||||
"grantedPersonalInfo": "Información personal concedida",
|
||||
"additionalGrants": "Permisos adicionales",
|
||||
"action": "Acción",
|
||||
"inResource": "en",
|
||||
"fullAccess": "Acceso total",
|
||||
"offlineToken": "Código de autorización offline",
|
||||
"revoke": "Revocar permiso",
|
||||
"configureAuthenticators": "Autenticadores configurados",
|
||||
"mobile": "Móvil",
|
||||
"totpStep1":
|
||||
'Instala <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> o Google Authenticator en tu teléfono móvil. Ambas aplicaciones están disponibles en <a href="https://play.google.com">Google Play</a> y en la App Store de Apple.',
|
||||
"totpStep2": "Abre la aplicación y escanea el código o introduce la clave.",
|
||||
"totpStep3": "Introduce el código único que te muestra la aplicación de autenticación y haz clic en Enviar para finalizar la configuración",
|
||||
"missingUsernameMessage": "Por favor indica tu usuario.",
|
||||
"missingFirstNameMessage": "Por favor indica el nombre.",
|
||||
"invalidEmailMessage": "Email no válido",
|
||||
"missingLastNameMessage": "Por favor indica tus apellidos.",
|
||||
"missingEmailMessage": "Por favor indica el email.",
|
||||
"missingPasswordMessage": "Por favor indica tu contraseña.",
|
||||
"notMatchPasswordMessage": "Las contraseñas no coinciden.",
|
||||
"missingTotpMessage": "Por favor indica tu código de autenticación",
|
||||
"invalidPasswordExistingMessage": "La contraseña actual no es correcta.",
|
||||
"invalidPasswordConfirmMessage": "La confirmación de contraseña no coincide.",
|
||||
"invalidTotpMessage": "El código de autenticación no es válido.",
|
||||
"usernameExistsMessage": "El usuario ya existe",
|
||||
"emailExistsMessage": "El email ya existe",
|
||||
"readOnlyUserMessage": "No puedes actualizar tu usuario porque tu cuenta es de solo lectura.",
|
||||
"readOnlyPasswordMessage": "No puedes actualizar tu contraseña porque tu cuenta es de solo lectura.",
|
||||
"successTotpMessage": "Aplicación de autenticación móvil configurada.",
|
||||
"successTotpRemovedMessage": "Aplicación de autenticación móvil eliminada.",
|
||||
"successGrantRevokedMessage": "Permiso revocado correctamente",
|
||||
"accountUpdatedMessage": "Tu cuenta se ha actualizado.",
|
||||
"accountPasswordUpdatedMessage": "Tu contraseña se ha actualizado.",
|
||||
"missingIdentityProviderMessage": "Proveedor de identidad no indicado.",
|
||||
"invalidFederatedIdentityActionMessage": "Acción no válida o no indicada.",
|
||||
"identityProviderNotFoundMessage": "No se encontró un proveedor de identidad.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Esta identidad ya no está activa",
|
||||
"federatedIdentityRemovingLastProviderMessage": "No puedes eliminar la última identidad federada porque no tienes fijada una contraseña.",
|
||||
"identityProviderRedirectErrorMessage": "Error en la redirección al proveedor de identidad",
|
||||
"identityProviderRemovedMessage": "Proveedor de identidad borrado correctamente.",
|
||||
"accountDisabledMessage": "La cuenta está desactivada, contacta con el administrador.",
|
||||
"accountTemporarilyDisabledMessage": "La cuenta está temporalmente desactivada, contacta con el administrador o inténtalo de nuevo más tarde.",
|
||||
"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."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,150 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Sauvegarder",
|
||||
"doCancel": "Annuler",
|
||||
"doLogOutAllSessions": "Déconnexion de toutes les sessions",
|
||||
"doRemove": "Supprimer",
|
||||
"doAdd": "Ajouter",
|
||||
"doSignOut": "Déconnexion",
|
||||
"editAccountHtmlTitle": "Édition du compte",
|
||||
"federatedIdentitiesHtmlTitle": "Identités fédérées",
|
||||
"accountLogHtmlTitle": "Accès au compte",
|
||||
"changePasswordHtmlTitle": "Changer de mot de passe",
|
||||
"sessionsHtmlTitle": "Sessions",
|
||||
"accountManagementTitle": "Gestion du compte Keycloak",
|
||||
"authenticatorTitle": "Authentification",
|
||||
"applicationsHtmlTitle": "Applications",
|
||||
"authenticatorCode": "Mot de passe unique",
|
||||
"email": "Courriel",
|
||||
"firstName": "Prénom",
|
||||
"givenName": "Prénom",
|
||||
"fullName": "Nom complet",
|
||||
"lastName": "Nom",
|
||||
"familyName": "Nom de famille",
|
||||
"password": "Mot de passe",
|
||||
"passwordConfirm": "Confirmation",
|
||||
"passwordNew": "Nouveau mot de passe",
|
||||
"username": "Compte",
|
||||
"address": "Adresse",
|
||||
"street": "Rue",
|
||||
"locality": "Ville ou Localité",
|
||||
"region": "État, Province ou Région",
|
||||
"postal_code": "Code Postal",
|
||||
"country": "Pays",
|
||||
"emailVerified": "Courriel vérifié",
|
||||
"gssDelegationCredential": "Accréditation de délégation GSS",
|
||||
"role_admin": "Administrateur",
|
||||
"role_realm-admin": "Administrateur du domaine",
|
||||
"role_create-realm": "Créer un domaine",
|
||||
"role_view-realm": "Voir un domaine",
|
||||
"role_view-users": "Voir les utilisateurs",
|
||||
"role_view-applications": "Voir les applications",
|
||||
"role_view-clients": "Voir les clients",
|
||||
"role_view-events": "Voir les événements",
|
||||
"role_view-identity-providers": "Voir les fournisseurs d'identités",
|
||||
"role_manage-realm": "Gérer le domaine",
|
||||
"role_manage-users": "Gérer les utilisateurs",
|
||||
"role_manage-applications": "Gérer les applications",
|
||||
"role_manage-identity-providers": "Gérer les fournisseurs d'identités",
|
||||
"role_manage-clients": "Gérer les clients",
|
||||
"role_manage-events": "Gérer les événements",
|
||||
"role_view-profile": "Voir le profil",
|
||||
"role_manage-account": "Gérer le compte",
|
||||
"role_read-token": "Lire le jeton d'authentification",
|
||||
"role_offline-access": "Accès hors-ligne",
|
||||
"client_account": "Compte",
|
||||
"client_security-admin-console": "Console d'administration de la sécurité",
|
||||
"client_admin-cli": "Admin CLI",
|
||||
"client_realm-management": "Gestion du domaine",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Champs obligatoires",
|
||||
"allFieldsRequired": "Tous les champs sont obligatoires",
|
||||
"backToApplication": "« Revenir à l'application",
|
||||
"backTo": "Revenir à {0}",
|
||||
"date": "Date",
|
||||
"event": "Evénement",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"details": "Détails",
|
||||
"started": "Début",
|
||||
"lastAccess": "Dernier accès",
|
||||
"expires": "Expiration",
|
||||
"applications": "Applications",
|
||||
"account": "Compte",
|
||||
"federatedIdentity": "Identité fédérée",
|
||||
"authenticator": "Authentification",
|
||||
"sessions": "Sessions",
|
||||
"log": "Connexion",
|
||||
"application": "Application",
|
||||
"availablePermissions": "Permissions disponibles",
|
||||
"grantedPermissions": "Permissions accordées",
|
||||
"grantedPersonalInfo": "Informations personnelles accordées",
|
||||
"additionalGrants": "Droits additionnels",
|
||||
"action": "Action",
|
||||
"inResource": "dans",
|
||||
"fullAccess": "Accès complet",
|
||||
"offlineToken": "Jeton d'authentification hors-ligne",
|
||||
"revoke": "Révoquer un droit",
|
||||
"configureAuthenticators": "Authentifications configurées.",
|
||||
"mobile": "Téléphone mobile",
|
||||
"totpStep1": "Installez une des applications suivantes sur votre mobile",
|
||||
"totpStep2": "Ouvrez l'application et scannez le code-barres ou entrez la clef.",
|
||||
"totpStep3": "Entrez le code à usage unique fourni par l'application et cliquez sur Sauvegarder pour terminer.",
|
||||
"totpManualStep2": "Ouvrez l'application et entrez la clef",
|
||||
"totpManualStep3": "Utilisez les valeurs de configuration suivante si l'application les autorise",
|
||||
"totpUnableToScan": "Impossible de scanner ?",
|
||||
"totpScanBarcode": "Scanner le code-barres ?",
|
||||
"totp.totp": "Basé sur le temps",
|
||||
"totp.hotp": "Basé sur un compteur",
|
||||
"totpType": "Type",
|
||||
"totpAlgorithm": "Algorithme",
|
||||
"totpDigits": "Chiffres",
|
||||
"totpInterval": "Intervalle",
|
||||
"totpCounter": "Compteur",
|
||||
"missingUsernameMessage": "Veuillez entrer votre nom d'utilisateur.",
|
||||
"missingFirstNameMessage": "Veuillez entrer votre prénom.",
|
||||
"invalidEmailMessage": "Courriel invalide.",
|
||||
"missingLastNameMessage": "Veuillez entrer votre nom.",
|
||||
"missingEmailMessage": "Veuillez entrer votre courriel.",
|
||||
"missingPasswordMessage": "Veuillez entrer votre mot de passe.",
|
||||
"notMatchPasswordMessage": "Les mots de passe ne sont pas identiques",
|
||||
"missingTotpMessage": "Veuillez entrer le code d'authentification.",
|
||||
"invalidPasswordExistingMessage": "Mot de passe existant invalide.",
|
||||
"invalidPasswordConfirmMessage": "Le mot de passe de confirmation ne correspond pas.",
|
||||
"invalidTotpMessage": "Le code d'authentification est invalide.",
|
||||
"usernameExistsMessage": "Le nom d'utilisateur existe déjà.",
|
||||
"emailExistsMessage": "Le courriel existe déjà.",
|
||||
"readOnlyUserMessage": "Vous ne pouvez pas mettre à jour votre compte car il est en lecture seule.",
|
||||
"readOnlyPasswordMessage": "Vous ne pouvez pas mettre à jour votre mot de passe car votre compte est en lecture seule.",
|
||||
"successTotpMessage": "L'authentification via téléphone mobile est configurée.",
|
||||
"successTotpRemovedMessage": "L'authentification via téléphone mobile est supprimée.",
|
||||
"successGrantRevokedMessage": "Droit révoqué avec succès.",
|
||||
"accountUpdatedMessage": "Votre compte a été mis à jour.",
|
||||
"accountPasswordUpdatedMessage": "Votre mot de passe a été mis à jour.",
|
||||
"missingIdentityProviderMessage": "Le fournisseur d'identité n'est pas spécifié.",
|
||||
"invalidFederatedIdentityActionMessage": "Action manquante ou invalide.",
|
||||
"identityProviderNotFoundMessage": "Le fournisseur d'identité spécifié n'est pas trouvé.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Cette identité n'est plus active dorénavant.",
|
||||
"federatedIdentityRemovingLastProviderMessage":
|
||||
"Vous ne pouvez pas supprimer votre dernière fédération d'identité sans avoir de mot de passe spécifié.",
|
||||
"identityProviderRedirectErrorMessage": "Erreur de redirection vers le fournisseur d'identité.",
|
||||
"identityProviderRemovedMessage": "Le fournisseur d'identité a été supprimé correctement.",
|
||||
"identityProviderAlreadyLinkedMessage": "Le fournisseur d'identité retourné par {0} est déjà lié à un autre utilisateur.",
|
||||
"accountDisabledMessage": "Ce compte est désactivé, veuillez contacter votre administrateur.",
|
||||
"accountTemporarilyDisabledMessage": "Ce compte est temporairement désactivé, veuillez contacter votre administrateur ou réessayez plus tard.",
|
||||
"invalidPasswordMinLengthMessage": "Mot de passe invalide: longueur minimale {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 mots de passe."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,310 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Salva",
|
||||
"doCancel": "Annulla",
|
||||
"doLogOutAllSessions": "Effettua il logout da tutte le sessioni",
|
||||
"doRemove": "Elimina",
|
||||
"doAdd": "Aggiungi",
|
||||
"doSignOut": "Esci",
|
||||
"doLogIn": "Log In",
|
||||
"doLink": "Link",
|
||||
"editAccountHtmlTitle": "Modifica Account",
|
||||
"personalInfoHtmlTitle": "Informazioni personali",
|
||||
"federatedIdentitiesHtmlTitle": "Identità federate",
|
||||
"accountLogHtmlTitle": "Log dell'account",
|
||||
"changePasswordHtmlTitle": "Cambia password",
|
||||
"deviceActivityHtmlTitle": "Attività dei dispositivi",
|
||||
"sessionsHtmlTitle": "Sessioni",
|
||||
"accountManagementTitle": "Gestione degli account di Keycloak",
|
||||
"authenticatorTitle": "Autenticatore",
|
||||
"applicationsHtmlTitle": "Applicazioni",
|
||||
"linkedAccountsHtmlTitle": "Account collegati",
|
||||
"accountManagementWelcomeMessage": "Benvenuto nella gestione degli account di Keycloak",
|
||||
"personalInfoIntroMessage": "Gestisci le tue informazioni di base",
|
||||
"accountSecurityTitle": "Sicurezza dell'account",
|
||||
"accountSecurityIntroMessage": "Controlla la tua password e gli accessi dell'account",
|
||||
"applicationsIntroMessage": "Traccia e gestisci i permessi delle applicazioni nell'accesso al tuo account",
|
||||
"resourceIntroMessage": "Condividi le tue risorse tra i membri del team",
|
||||
"passwordLastUpdateMessage": "La tua password è stata aggiornata il",
|
||||
"updatePasswordTitle": "Aggiornamento password",
|
||||
"updatePasswordMessageTitle": "Assicurati di scegliere una password robusta",
|
||||
"updatePasswordMessage":
|
||||
"Una password robusta contiene un misto di numeri, lettere, e simboli. È difficile da indovinare, non assomiglia a una parola reale, ed è utilizzata solo per questo account.",
|
||||
"personalSubTitle": "Le tue informazioni personali",
|
||||
"personalSubMessage": "Gestisce queste informazioni di base: il tuo nome, cognome, e indirizzo email",
|
||||
"authenticatorCode": "Codice monouso",
|
||||
"email": "Email",
|
||||
"firstName": "Nome",
|
||||
"givenName": "Nome",
|
||||
"fullName": "Nome completo",
|
||||
"lastName": "Cognome",
|
||||
"familyName": "Cognome",
|
||||
"password": "Password",
|
||||
"currentPassword": "Password attuale",
|
||||
"passwordConfirm": "Conferma password",
|
||||
"passwordNew": "Nuova password",
|
||||
"username": "Username",
|
||||
"address": "Indirizzo",
|
||||
"street": "Via",
|
||||
"locality": "Città o località",
|
||||
"region": "Stato, Provincia, o Regione",
|
||||
"postal_code": "CAP",
|
||||
"country": "Paese",
|
||||
"emailVerified": "Email verificata",
|
||||
"gssDelegationCredential": "Credenziali delega GSS",
|
||||
"profileScopeConsentText": "Profilo utente",
|
||||
"emailScopeConsentText": "Indirizzo email",
|
||||
"addressScopeConsentText": "Indirizzo",
|
||||
"phoneScopeConsentText": "Numero di telefono",
|
||||
"offlineAccessScopeConsentText": "Accesso offline",
|
||||
"samlRoleListScopeConsentText": "I miei ruoli",
|
||||
"rolesScopeConsentText": "Ruoli utente",
|
||||
"role_admin": "Admin",
|
||||
"role_realm-admin": "Realm admin",
|
||||
"role_create-realm": "Crea realm",
|
||||
"role_view-realm": "Visualizza realm",
|
||||
"role_view-users": "Visualizza utenti",
|
||||
"role_view-applications": "Visualizza applicazioni",
|
||||
"role_view-clients": "Visualizza client",
|
||||
"role_view-events": "Visualizza eventi",
|
||||
"role_view-identity-providers": "Visualizza identity provider",
|
||||
"role_view-consent": "Visualizza consensi",
|
||||
"role_manage-realm": "Gestisci realm",
|
||||
"role_manage-users": "Gestisci utenti",
|
||||
"role_manage-applications": "Gestisci applicazioni",
|
||||
"role_manage-identity-providers": "Gestisci identity provider",
|
||||
"role_manage-clients": "Gestisci client",
|
||||
"role_manage-events": "Gestisci eventi",
|
||||
"role_view-profile": "Visualizza profilo",
|
||||
"role_manage-account": "Gestisci account",
|
||||
"role_manage-account-links": "Gestisci i link dell'account",
|
||||
"role_manage-consent": "Gestisci consensi",
|
||||
"role_read-token": "Leggi token",
|
||||
"role_offline-access": "Accesso offline",
|
||||
"role_uma_authorization": "Ottieni permessi",
|
||||
"client_account": "Account",
|
||||
"client_account-console": "Console account",
|
||||
"client_security-admin-console": "Console di amministrazione di sicurezza",
|
||||
"client_admin-cli": "Admin CLI",
|
||||
"client_realm-management": "Gestione realm",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Campi obbligatori",
|
||||
"allFieldsRequired": "Tutti campi obbligatori",
|
||||
"backToApplication": "« Torna all'applicazione",
|
||||
"backTo": "Torna a {0}",
|
||||
"date": "Data",
|
||||
"event": "Evento",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Client",
|
||||
"details": "Dettagli",
|
||||
"started": "Iniziato",
|
||||
"lastAccess": "Ultimo accesso",
|
||||
"expires": "Scade",
|
||||
"applications": "Applicazioni",
|
||||
"account": "Account",
|
||||
"federatedIdentity": "Identità federate",
|
||||
"authenticator": "Autenticatore",
|
||||
"device-activity": "Attività dei dispositivi",
|
||||
"sessions": "Sessioni",
|
||||
"log": "Log",
|
||||
"application": "Applicazione",
|
||||
"availablePermissions": "Autorizzazioni disponibili",
|
||||
"grantedPermissions": "Autorizzazioni concesse",
|
||||
"grantedPersonalInfo": "Informazioni personali concesse",
|
||||
"additionalGrants": "Ulteriori concessioni",
|
||||
"action": "Azione",
|
||||
"inResource": "in",
|
||||
"fullAccess": "Accesso completo",
|
||||
"offlineToken": "Token offline",
|
||||
"revoke": "Revoca concessione",
|
||||
"configureAuthenticators": "Autenticatori configurati",
|
||||
"mobile": "Dispositivo mobile",
|
||||
"totpStep1": "Installa una delle seguenti applicazioni sul tuo dispositivo mobile",
|
||||
"totpStep2": "Apri l'applicazione e scansiona il codice QR",
|
||||
"totpStep3": "Scrivi il codice monouso fornito dall'applicazione e clicca Salva per completare il setup.",
|
||||
"totpStep3DeviceName": "Fornisci il nome del dispositivo per aiutarti a gestire i dispositivi di autenticazione.",
|
||||
"totpManualStep2": "Apri l'applicazione e scrivi la chiave",
|
||||
"totpManualStep3": "Usa le seguenti impostazioni se l'applicazione lo consente",
|
||||
"totpUnableToScan": "Non riesci a scansionare il codice QR?",
|
||||
"totpScanBarcode": "Vuoi scansionare il codice QR?",
|
||||
"totp.totp": "Basato sull'ora",
|
||||
"totp.hotp": "Basato sul contatore",
|
||||
"totpType": "Tipo",
|
||||
"totpAlgorithm": "Algoritmo",
|
||||
"totpDigits": "Cifre",
|
||||
"totpInterval": "Intervallo",
|
||||
"totpCounter": "Contatore",
|
||||
"totpDeviceName": "Nome dispositivo",
|
||||
"missingUsernameMessage": "Inserisci lo username.",
|
||||
"missingFirstNameMessage": "Inserisci il nome.",
|
||||
"invalidEmailMessage": "Indirizzo email non valido.",
|
||||
"missingLastNameMessage": "Inserisci il cognome.",
|
||||
"missingEmailMessage": "Inserisci l'indirizzo email.",
|
||||
"missingPasswordMessage": "Inserisci la password.",
|
||||
"notMatchPasswordMessage": "Le password non coincidono.",
|
||||
"invalidUserMessage": "Utente non valido",
|
||||
"missingTotpMessage": "Inserisci il codice di autenticazione.",
|
||||
"missingTotpDeviceNameMessage": "Inserisci il nome del dispositivo di autenticazione.",
|
||||
"invalidPasswordExistingMessage": "Password esistente non valida.",
|
||||
"invalidPasswordConfirmMessage": "La password di conferma non coincide.",
|
||||
"invalidTotpMessage": "Codice di autenticazione non valido.",
|
||||
"usernameExistsMessage": "Username già esistente.",
|
||||
"emailExistsMessage": "Email già esistente.",
|
||||
"readOnlyUserMessage": "Non puoi aggiornare il tuo account poiché è in modalità sola lettura.",
|
||||
"readOnlyUsernameMessage": "Non puoi aggiornare il tuo nome utente poiché è in modalità sola lettura.",
|
||||
"readOnlyPasswordMessage": "Non puoi aggiornare il tuo account poiché è in modalità sola lettura.",
|
||||
"successTotpMessage": "Autenticatore mobile configurato.",
|
||||
"successTotpRemovedMessage": "Autenticatore mobile eliminato.",
|
||||
"successGrantRevokedMessage": "Concessione revocata con successo.",
|
||||
"accountUpdatedMessage": "Il tuo account è stato aggiornato.",
|
||||
"accountPasswordUpdatedMessage": "La tua password è stata aggiornata.",
|
||||
"missingIdentityProviderMessage": "Identity provider non specificato.",
|
||||
"invalidFederatedIdentityActionMessage": "Azione non valida o mancante.",
|
||||
"identityProviderNotFoundMessage": "L'identity provider specificato non è stato trovato.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Questo identity non è più attivo.",
|
||||
"federatedIdentityRemovingLastProviderMessage": "Non puoi rimuovere l'ultima identità federata poiché non hai più la password.",
|
||||
"identityProviderRedirectErrorMessage": "Il reindirizzamento all'identity provider è fallito.",
|
||||
"identityProviderRemovedMessage": "Identity provider eliminato correttamente.",
|
||||
"identityProviderAlreadyLinkedMessage": "L'identità federata restituita da {0} è già collegata ad un altro utente.",
|
||||
"staleCodeAccountMessage": "La pagina è scaduta. Prova di nuovo.",
|
||||
"consentDenied": "Consenso negato.",
|
||||
"accountDisabledMessage": "Account disabilitato, contatta l'amministratore.",
|
||||
"accountTemporarilyDisabledMessage": "L'account è temporaneamente disabilitato, contatta l'amministratore o riprova più tardi.",
|
||||
"invalidPasswordMinLengthMessage": "Password non valida: lunghezza minima {0}.",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "Password non valida: deve contenere almeno {0} caratteri minuscoli.",
|
||||
"invalidPasswordMinDigitsMessage": "Password non valida: deve contenere almeno {0} numeri.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Password non valida: deve contenere almeno {0} caratteri maiuscoli.",
|
||||
"invalidPasswordMinSpecialCharsMessage": "Password non valida: deve contenere almeno {0} caratteri speciali.",
|
||||
"invalidPasswordNotUsernameMessage": "Password non valida: non deve essere uguale allo username.",
|
||||
"invalidPasswordRegexPatternMessage": "Password non valida: fallito il match con una o più espressioni regolari.",
|
||||
"invalidPasswordHistoryMessage": "Password non valida: non deve essere uguale a una delle ultime {0} password.",
|
||||
"invalidPasswordBlacklistedMessage": "Password non valida: la password non è consentita.",
|
||||
"invalidPasswordGenericMessage": "Password non valida: la nuova password non rispetta le indicazioni previste.",
|
||||
"myResources": "Le mie risorse",
|
||||
"myResourcesSub": "Le mie risorse",
|
||||
"doDeny": "Nega",
|
||||
"doRevoke": "Revoca",
|
||||
"doApprove": "Approva",
|
||||
"doRemoveSharing": "Rimuovi condivisione",
|
||||
"doRemoveRequest": "Rimuovi richiesta",
|
||||
"peopleAccessResource": "Persone che hanno accesso a questa risorsa",
|
||||
"resourceManagedPolicies": "Permessi che danno accesso a questa risorsa",
|
||||
"resourceNoPermissionsGrantingAccess": "Nessun permesso dà accesso a questa risorsa",
|
||||
"anyAction": "Qualsiasi azione",
|
||||
"description": "Descrizione",
|
||||
"name": "Nome",
|
||||
"scopes": "Ambito",
|
||||
"resource": "Risorsa",
|
||||
"user": "Utente",
|
||||
"peopleSharingThisResource": "Persone che condividono questa risorsa",
|
||||
"shareWithOthers": "Condividi con altri",
|
||||
"needMyApproval": "Richiede la mia approvazione",
|
||||
"requestsWaitingApproval": "La tua richiesta è in attesa di approvazione",
|
||||
"icon": "Icona",
|
||||
"requestor": "Richiedente",
|
||||
"owner": "Proprietario",
|
||||
"resourcesSharedWithMe": "Risorse condivise con me",
|
||||
"permissionRequestion": "Richiesta di permesso",
|
||||
"permission": "Permesso",
|
||||
"shares": "condivisioni",
|
||||
"notBeingShared": "Questa risorsa non è in condivisione.",
|
||||
"notHaveAnyResource": "Non hai nessuna risorsa",
|
||||
"noResourcesSharedWithYou": "Non ci sono risorse condivise con te",
|
||||
"havePermissionRequestsWaitingForApproval": "Hai {0} richiesta(e) di permesso in attesa di approvazione.",
|
||||
"clickHereForDetails": "Clicca qui per i dettagli.",
|
||||
"resourceIsNotBeingShared": "La risorsa non è in condivisione",
|
||||
"locale_it": "Italiano",
|
||||
"applicaitonName": "Nome",
|
||||
"applicationType": "Tipo applicazione",
|
||||
"applicationInUse": "In-use app only",
|
||||
"clearAllFilter": "Azzera tutti i filtri",
|
||||
"activeFilters": "Filtri attivi",
|
||||
"filterByName": "Filtra per nome ...",
|
||||
"allApps": "Tutte le applicazioni",
|
||||
"internalApps": "Applicazioni interne",
|
||||
"thirdpartyApps": "Applicazioni di terze parti",
|
||||
"appResults": "Risultati",
|
||||
"clientNotFoundMessage": "Client non trovato.",
|
||||
"authorizedProvider": "Provider autorizzato",
|
||||
"authorizedProviderMessage": "Provider autorizzati collegati al tuo account",
|
||||
"identityProvider": "Identity provider",
|
||||
"identityProviderMessage": "Collegare il tuo account con gli identity provider che hai configurato",
|
||||
"socialLogin": "Social Login",
|
||||
"userDefined": "Definito dall'utente",
|
||||
"removeAccess": "Rimuovi accesso",
|
||||
"removeAccessMessage": "Devi concedere di nuovo l'accesso, se vuoi utilizzare l'account di questa applicazione.",
|
||||
"authenticatorStatusMessage": "L'autenticazione a due fattori è attualmente",
|
||||
"authenticatorFinishSetUpTitle": "La tua autenticazione a due fattori",
|
||||
"authenticatorFinishSetUpMessage":
|
||||
"Ogni volta che effettui l'accesso al tuo account Keycloak, ti verrà richiesto di fornire il tuo codice di autenticazione a due fattori.",
|
||||
"authenticatorSubTitle": "Imposta l'autenticazione a due fattori",
|
||||
"authenticatorSubMessage":
|
||||
"Per incrementare la sicurezza del tuo account, attiva almeno uno dei metodi disponibili per l'autenticazione a due fattori.",
|
||||
"authenticatorMobileTitle": "Autenticatore mobile",
|
||||
"authenticatorMobileMessage": "Utilizza l'autenticatore mobile per ottenere i codici di verifica per l'autenticazione a due fattori.",
|
||||
"authenticatorMobileFinishSetUpMessage": "L'autenticatore è stato collegato al tuo telefono.",
|
||||
"authenticatorActionSetup": "Set up",
|
||||
"authenticatorSMSTitle": "Codice SMS",
|
||||
"authenticatorSMSMessage": "Keycloak invierà il codice di verifica al tuo telefono per l'autenticazione a due fattori.",
|
||||
"authenticatorSMSFinishSetUpMessage": "I messaggi di testo vengono inviati a",
|
||||
"authenticatorDefaultStatus": "Default",
|
||||
"authenticatorChangePhone": "Cambia numero di telefono",
|
||||
"authenticatorBackupCodesTitle": "Codici di backup",
|
||||
"authenticatorBackupCodesMessage": "Ottieni i tuoi codici di backup a otto cifre",
|
||||
"authenticatorBackupCodesFinishSetUpMessage": "Sono stati generati dodici codici di backup. Ognuno può essere usato una sola volta.",
|
||||
"authenticatorMobileSetupTitle": "Setup autenticatore mobile",
|
||||
"smscodeIntroMessage": "Inserisci il tuo numero di telefono e ti verrà inviato un codice di verifica.",
|
||||
"mobileSetupStep1": "Installa un'applicazione di autenticazione sul tuo telefono. Sono supportate le applicazioni qui elencate.",
|
||||
"mobileSetupStep2": "Apri l'applicazione e scansiona il codice QR:",
|
||||
"mobileSetupStep3": "Inserisci il codice monouso fornito dall'applicazione e clicca Salva per completare il setup.",
|
||||
"scanBarCode": "Vuoi scansionare il codice QR?",
|
||||
"enterBarCode": "Inserisci il codice monouso",
|
||||
"doCopy": "Copia",
|
||||
"doFinish": "Termina",
|
||||
"authenticatorSMSCodeSetupTitle": "Setup codice SMS",
|
||||
"chooseYourCountry": "Scegli la tua nazione",
|
||||
"enterYourPhoneNumber": "Inserisci il tuo numero di telefono",
|
||||
"sendVerficationCode": "Invia il codice di verifica",
|
||||
"enterYourVerficationCode": "Inserisci il codice di verifica",
|
||||
"authenticatorBackupCodesSetupTitle": "Setup backup codici",
|
||||
"backupcodesIntroMessage":
|
||||
"Se non disponi più del tuo telefono, puoi comunque accedere al tuo account attraverso i codici di backup. Conservali in un posto sicuro e accessibile.",
|
||||
"realmName": "Realm",
|
||||
"doDownload": "Download",
|
||||
"doPrint": "Stampa",
|
||||
"backupCodesTips-1": "Ogni codice di backup può essere usato una sola volta.",
|
||||
"backupCodesTips-2": "Questi codici sono stati generati il",
|
||||
"generateNewBackupCodes": "Genera dei nuovi codici di backup",
|
||||
"backupCodesTips-3": "Quando generi dei nuovi codici di backup, quelli attuali non funzioneranno più.",
|
||||
"backtoAuthenticatorPage": "Torna alla pagina dell'autenticatore",
|
||||
"resources": "Risorse",
|
||||
"sharedwithMe": "Condiviso con me",
|
||||
"share": "Condiviso",
|
||||
"sharedwith": "Condiviso con",
|
||||
"accessPermissions": "Permessi di accesso",
|
||||
"permissionRequests": "Richieste di permesso",
|
||||
"approve": "Approva",
|
||||
"approveAll": "Approva tutti",
|
||||
"people": "persone",
|
||||
"perPage": "per pagina",
|
||||
"currentPage": "Pagina corrente",
|
||||
"sharetheResource": "Condividi la risorsa",
|
||||
"group": "Gruppo",
|
||||
"selectPermission": "Seleziona permessi",
|
||||
"addPeople": "Aggiungi persone con le quali condividere la tua risorsa",
|
||||
"addTeam": "Aggiungi gruppi con i quali condividere la tua risorsa",
|
||||
"myPermissions": "Miei permessi",
|
||||
"waitingforApproval": "Attesa dell'approvazione",
|
||||
"anyPermission": "Qualsiasi permesso",
|
||||
"openshift.scope.user_info": "Informazioni utente",
|
||||
"openshift.scope.user_check-access": "Informazioni per l'accesso dell'utente",
|
||||
"openshift.scope.user_full": "Accesso completo",
|
||||
"openshift.scope.list-projects": "Elenca progetti"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,324 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "保存",
|
||||
"doCancel": "キャンセル",
|
||||
"doLogOutAllSessions": "全セッションからログアウト",
|
||||
"doRemove": "削除",
|
||||
"doAdd": "追加",
|
||||
"doSignOut": "サインアウト",
|
||||
"doLogIn": "ログイン",
|
||||
"doLink": "リンク",
|
||||
"editAccountHtmlTitle": "アカウントの編集",
|
||||
"personalInfoHtmlTitle": "個人情報",
|
||||
"federatedIdentitiesHtmlTitle": "連携済みアイデンティティー",
|
||||
"accountLogHtmlTitle": "アカウントログ",
|
||||
"changePasswordHtmlTitle": "パスワード変更",
|
||||
"deviceActivityHtmlTitle": "デバイス・アクティビティー",
|
||||
"sessionsHtmlTitle": "セッション",
|
||||
"accountManagementTitle": "Keycloakアカウント管理",
|
||||
"authenticatorTitle": "オーセンティケーター",
|
||||
"applicationsHtmlTitle": "アプリケーション",
|
||||
"linkedAccountsHtmlTitle": "リンクされたアカウント",
|
||||
"accountManagementWelcomeMessage": "Keycloakアカウント管理へようこそ",
|
||||
"personalInfoIntroMessage": "基本情報を管理する",
|
||||
"accountSecurityTitle": "アカウント・セキュリティー",
|
||||
"accountSecurityIntroMessage": "パスワードとアカウント・アクセスを制御する",
|
||||
"applicationsIntroMessage": "アカウントへアクセスするためにアプリのパーミッションを追跡して管理する",
|
||||
"resourceIntroMessage": "チームメンバー間でリソースを共有する",
|
||||
"passwordLastUpdateMessage": "パスワードは更新されました",
|
||||
"updatePasswordTitle": "パスワードの更新",
|
||||
"updatePasswordMessageTitle": "強力なパスワードを選択してください",
|
||||
"updatePasswordMessage":
|
||||
"強力なパスワードは、数字、文字、記号を含みます。推測が難しく、実在する言葉に似ておらず、このアカウントだけで使用されています。",
|
||||
"personalSubTitle": "個人情報",
|
||||
"personalSubMessage": "この基本情報を管理してください:名、姓、メール",
|
||||
"authenticatorCode": "ワンタイムコード",
|
||||
"email": "Eメール",
|
||||
"firstName": "名",
|
||||
"givenName": "名",
|
||||
"fullName": "氏名",
|
||||
"lastName": "姓",
|
||||
"familyName": "姓",
|
||||
"password": "パスワード",
|
||||
"currentPassword": "現在のパスワード",
|
||||
"passwordConfirm": "新しいパスワード(確認)",
|
||||
"passwordNew": "新しいパスワード",
|
||||
"username": "ユーザー名",
|
||||
"address": "住所",
|
||||
"street": "番地",
|
||||
"locality": "市区町村",
|
||||
"region": "都道府県",
|
||||
"postal_code": "郵便番号",
|
||||
"country": "国",
|
||||
"emailVerified": "確認済みEメール",
|
||||
"gssDelegationCredential": "GSS委譲クレデンシャル",
|
||||
"profileScopeConsentText": "ユーザー・プロファイル",
|
||||
"emailScopeConsentText": "メールアドレス",
|
||||
"addressScopeConsentText": "アドレス",
|
||||
"phoneScopeConsentText": "電話番号",
|
||||
"offlineAccessScopeConsentText": "オフライン・アクセス",
|
||||
"samlRoleListScopeConsentText": "ロール",
|
||||
"rolesScopeConsentText": "ユーザーロール",
|
||||
"role_admin": "管理者",
|
||||
"role_realm-admin": "レルム管理者",
|
||||
"role_create-realm": "レルムの作成",
|
||||
"role_view-realm": "レルムの参照",
|
||||
"role_view-users": "ユーザーの参照",
|
||||
"role_view-applications": "アプリケーションの参照",
|
||||
"role_view-clients": "クライアントの参照",
|
||||
"role_view-events": "イベントの参照",
|
||||
"role_view-identity-providers": "アイデンティティー・プロバイダーの参照",
|
||||
"role_view-consent": "同意の参照",
|
||||
"role_manage-realm": "レルムの管理",
|
||||
"role_manage-users": "ユーザーの管理",
|
||||
"role_manage-applications": "アプリケーションの管理",
|
||||
"role_manage-identity-providers": "アイデンティティー・プロバイダーの管理",
|
||||
"role_manage-clients": "クライアントの管理",
|
||||
"role_manage-events": "イベントの管理",
|
||||
"role_view-profile": "プロファイルの参照",
|
||||
"role_manage-account": "アカウントの管理",
|
||||
"role_manage-account-links": "アカウントリンクの管理",
|
||||
"role_manage-consent": "同意の管理",
|
||||
"role_read-token": "トークンの参照",
|
||||
"role_offline-access": "オフライン・アクセス",
|
||||
"role_uma_authorization": "パーミッションの取得",
|
||||
"client_account": "アカウント",
|
||||
"client_account-console": "アカウント・コンソール",
|
||||
"client_security-admin-console": "セキュリティー管理コンソール",
|
||||
"client_admin-cli": "管理CLI",
|
||||
"client_realm-management": "レルム管理",
|
||||
"client_broker": "ブローカー",
|
||||
"requiredFields": "必須",
|
||||
"allFieldsRequired": "全ての入力項目が必須",
|
||||
"backToApplication": "« アプリケーションに戻る",
|
||||
"backTo": "{0}に戻る",
|
||||
"date": "日付",
|
||||
"event": "イベント",
|
||||
"ip": "IP",
|
||||
"client": "クライアント",
|
||||
"clients": "クライアント",
|
||||
"details": "詳細",
|
||||
"started": "開始",
|
||||
"lastAccess": "最終アクセス",
|
||||
"expires": "有効期限",
|
||||
"applications": "アプリケーション",
|
||||
"account": "アカウント",
|
||||
"federatedIdentity": "連携済みアイデンティティー",
|
||||
"authenticator": "オーセンティケーター",
|
||||
"device-activity": "デバイス・アクティビティー",
|
||||
"sessions": "セッション",
|
||||
"log": "ログ",
|
||||
"application": "アプリケーション",
|
||||
"availableRoles": "利用可能なロール",
|
||||
"grantedPermissions": "許可されたパーミッション",
|
||||
"grantedPersonalInfo": "許可された個人情報",
|
||||
"additionalGrants": "追加の許可",
|
||||
"action": "アクション",
|
||||
"inResource": "in",
|
||||
"fullAccess": "フルアクセス",
|
||||
"offlineToken": "オフライン・トークン",
|
||||
"revoke": "許可の取り消し",
|
||||
"configureAuthenticators": "設定済みのオーセンティケーター",
|
||||
"mobile": "モバイル",
|
||||
"totpStep1": "モバイルに以下のアプリケーションのいずれかをインストールしてください。",
|
||||
"totpStep2": "アプリケーションを開き、バーコードをスキャンしてください。",
|
||||
"totpStep3": "アプリケーションで提供されたワンタイムコードを入力して保存をクリックし、セットアップを完了してください。",
|
||||
"totpStep3DeviceName": "OTPデバイスの管理に役立つようなデバイス名を指定してください。",
|
||||
"totpManualStep2": "アプリケーションを開き、キーを入力してください。",
|
||||
"totpManualStep3": "アプリケーションが設定できる場合は、次の設定値を使用してください。",
|
||||
"totpUnableToScan": "スキャンできませんか?",
|
||||
"totpScanBarcode": "バーコードをスキャンしますか?",
|
||||
"totp.totp": "時間ベース",
|
||||
"totp.hotp": "カウンターベース",
|
||||
"totpType": "タイプ",
|
||||
"totpAlgorithm": "アルゴリズム",
|
||||
"totpDigits": "数字",
|
||||
"totpInterval": "間隔",
|
||||
"totpCounter": "カウンター",
|
||||
"totpDeviceName": "デバイス名",
|
||||
"missingUsernameMessage": "ユーザー名を入力してください。",
|
||||
"missingFirstNameMessage": "名を入力してください。",
|
||||
"invalidEmailMessage": "無効なメールアドレスです。",
|
||||
"missingLastNameMessage": "姓を入力してください。",
|
||||
"missingEmailMessage": "Eメールを入力してください。",
|
||||
"missingPasswordMessage": "パスワードを入力してください。",
|
||||
"notMatchPasswordMessage": "パスワードが一致していません。",
|
||||
"invalidUserMessage": "無効なユーザーです。",
|
||||
"missingTotpMessage": "オーセンティケーター・コードを入力してください。",
|
||||
"missingTotpDeviceNameMessage": "デバイス名を指定してください。",
|
||||
"invalidPasswordExistingMessage": "既存のパスワードが不正です。",
|
||||
"invalidPasswordConfirmMessage": "新しいパスワード(確認)と一致していません。",
|
||||
"invalidTotpMessage": "無効なオーセンティケーター・コードです。",
|
||||
"usernameExistsMessage": "既に存在するユーザー名です。",
|
||||
"emailExistsMessage": "既に存在するEメールです。",
|
||||
"readOnlyUserMessage": "読み取り専用のため、アカウントを更新することはできません。",
|
||||
"readOnlyUsernameMessage": "読み取り専用のため、ユーザー名を更新することはできません。",
|
||||
"readOnlyPasswordMessage": "読み取り専用のため、パスワードを更新することはできません。",
|
||||
"successTotpMessage": "モバイル・オーセンティケーターが設定されました。",
|
||||
"successTotpRemovedMessage": "モバイル・オーセンティケーターが削除されました。",
|
||||
"successGrantRevokedMessage": "許可が正常に取り消しされました。",
|
||||
"accountUpdatedMessage": "アカウントが更新されました。",
|
||||
"accountPasswordUpdatedMessage": "パスワードが更新されました。",
|
||||
"missingIdentityProviderMessage": "アイデンティティー・プロバイダーが指定されていません。",
|
||||
"invalidFederatedIdentityActionMessage": "無効または存在しないアクションです。",
|
||||
"identityProviderNotFoundMessage": "指定されたアイデンティティー・プロバイダーが見つかりません。",
|
||||
"federatedIdentityLinkNotActiveMessage": "このアイデンティティーは有効ではありません。",
|
||||
"federatedIdentityRemovingLastProviderMessage": "パスワードがないため、最後の連携済みアイデンティティーが削除できません。",
|
||||
"identityProviderRedirectErrorMessage": "アイデンティティー・プロバイダーへのリダイレクトに失敗しました。",
|
||||
"identityProviderRemovedMessage": "アイデンティティー・プロバイダーが正常に削除されました。",
|
||||
"identityProviderAlreadyLinkedMessage": "{0}から返された連携済みアイデンティティーは既に他のユーザーに関連付けされています。",
|
||||
"staleCodeAccountMessage": "有効期限切れです。再度お試しください。",
|
||||
"consentDenied": "同意が拒否されました。",
|
||||
"accountDisabledMessage": "アカウントが無効です。管理者に連絡してください。",
|
||||
"accountTemporarilyDisabledMessage": "アカウントが一時的に無効です。管理者に連絡するか、しばらく時間をおいてから再度お試しください。",
|
||||
"invalidPasswordMinLengthMessage": "無効なパスワード: 最小{0}の長さが必要です。",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "無効なパスワード: 少なくとも{0}文字の小文字を含む必要があります。",
|
||||
"invalidPasswordMinDigitsMessage": "無効なパスワード: 少なくとも{0}文字の数字を含む必要があります。",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "無効なパスワード:少なくとも{0}文字の大文字を含む必要があります。",
|
||||
"invalidPasswordMinSpecialCharsMessage": "無効なパスワード: 少なくとも{0}文字の特殊文字を含む必要があります。",
|
||||
"invalidPasswordNotUsernameMessage": "無効なパスワード: ユーザー名と同じパスワードは禁止されています。",
|
||||
"invalidPasswordRegexPatternMessage": "無効なパスワード: 正規表現パターンと一致しません。",
|
||||
"invalidPasswordHistoryMessage": "無効なパスワード: 最近の{0}パスワードのいずれかと同じパスワードは禁止されています。",
|
||||
"invalidPasswordBlacklistedMessage": "無効なパスワード: パスワードがブラックリストに含まれています。",
|
||||
"invalidPasswordGenericMessage": "無効なパスワード: 新しいパスワードはパスワード・ポリシーと一致しません。",
|
||||
"myResources": "マイリソース",
|
||||
"myResourcesSub": "マイリソース",
|
||||
"doDeny": "拒否",
|
||||
"doRevoke": "取り消し",
|
||||
"doApprove": "承認",
|
||||
"doRemoveSharing": "共有の削除",
|
||||
"doRemoveRequest": "要求の削除",
|
||||
"peopleAccessResource": "このリソースにアクセスできる人",
|
||||
"resourceManagedPolicies": "このリソースへのアクセスを許可するパーミッション",
|
||||
"resourceNoPermissionsGrantingAccess": "このリソースへのアクセスを許可する権限はありません",
|
||||
"anyAction": "任意のアクション",
|
||||
"description": "説明",
|
||||
"name": "名前",
|
||||
"scopes": "スコープ",
|
||||
"resource": "リソース",
|
||||
"user": "ユーザー",
|
||||
"peopleSharingThisResource": "このリソースを共有している人",
|
||||
"shareWithOthers": "他人と共有",
|
||||
"needMyApproval": "承認が必要",
|
||||
"requestsWaitingApproval": "承認待ちの要求",
|
||||
"icon": "アイコン",
|
||||
"requestor": "要求者",
|
||||
"owner": "オーナー",
|
||||
"resourcesSharedWithMe": "共有しているリソース",
|
||||
"permissionRequestion": "パーミッションの要求",
|
||||
"permission": "パーミッション",
|
||||
"shares": "共有(複数)",
|
||||
"notBeingShared": "このリソースは共有されていません。",
|
||||
"notHaveAnyResource": "リソースがありません。",
|
||||
"noResourcesSharedWithYou": "共有しているリソースはありません",
|
||||
"havePermissionRequestsWaitingForApproval": "承認を待っている{0}個のパーミッションの要求があります。",
|
||||
"clickHereForDetails": "詳細はこちらをクリックしてください。",
|
||||
"resourceIsNotBeingShared": "リソースは共有されていません。",
|
||||
"locale_ca": "Català",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "English",
|
||||
"locale_es": "Español",
|
||||
"locale_fr": "Français",
|
||||
"locale_it": "Italian",
|
||||
"locale_ja": "日本語",
|
||||
"locale_nl": "Nederlands",
|
||||
"locale_no": "Norsk",
|
||||
"locale_lt": "Lietuvių",
|
||||
"locale_pt-BR": "Português (Brasil)",
|
||||
"locale_ru": "Русский",
|
||||
"locale_sk": "Slovenčina",
|
||||
"locale_sv": "Svenska",
|
||||
"locale_tr": "Turkish",
|
||||
"locale_zh-CN": "中文简体",
|
||||
"applicaitonName": "名前",
|
||||
"applicationType": "アプリケーション・タイプ",
|
||||
"applicationInUse": "使用中のアプリケーションのみ",
|
||||
"clearAllFilter": "すべてのフィルターをクリア",
|
||||
"activeFilters": "アクティブなフィルター",
|
||||
"filterByName": "名前でフィルタリング...",
|
||||
"allApps": "すべてのアプリケーション",
|
||||
"internalApps": "内部アプリケーション",
|
||||
"thirdpartyApps": "サードパーティーのアプリケーション",
|
||||
"appResults": "結果",
|
||||
"clientNotFoundMessage": "クライアントが見つかりません。",
|
||||
"authorizedProvider": "認可済みプロバイダー",
|
||||
"authorizedProviderMessage": "アカウントにリンクされた認可済みプロバイダー",
|
||||
"identityProvider": "アイデンティティー・プロバイダー",
|
||||
"identityProviderMessage": "アカウントと設定したアイデンティティー・プロバイダーをリンクするには",
|
||||
"socialLogin": "ソーシャル・ログイン",
|
||||
"userDefined": "ユーザー定義",
|
||||
"removeAccess": "アクセス権の削除",
|
||||
"removeAccessMessage": "このアプリ・アカウントを使用する場合は、アクセス権を再度付与する必要があります。",
|
||||
"authenticatorStatusMessage": "2要素認証は現在",
|
||||
"authenticatorFinishSetUpTitle": "あなたの2要素認証",
|
||||
"authenticatorFinishSetUpMessage": "Keycloakアカウントにサインインするたびに、2要素認証コードを入力するように求められます。",
|
||||
"authenticatorSubTitle": "2要素認証を設定する",
|
||||
"authenticatorSubMessage": "アカウントのセキュリティーを強化するには、利用可能な2要素認証の方式のうち少なくとも1つを有効にします。",
|
||||
"authenticatorMobileTitle": "モバイル・オーセンティケーター",
|
||||
"authenticatorMobileMessage": "モバイル・オーセンティケーターを使用して、2要素認証として確認コードを取得します。",
|
||||
"authenticatorMobileFinishSetUpMessage": "オーセンティケーターはあなたの携帯電話にバインドされています。",
|
||||
"authenticatorActionSetup": "セットアップ",
|
||||
"authenticatorSMSTitle": "SMSコード",
|
||||
"authenticatorSMSMessage": "Keycloakは、2要素認証として確認コードを携帯電話に送信します。",
|
||||
"authenticatorSMSFinishSetUpMessage": "テキスト・メッセージが次の電話番号宛に送信されます:",
|
||||
"authenticatorDefaultStatus": "デフォルト",
|
||||
"authenticatorChangePhone": "電話番号の変更",
|
||||
"authenticatorBackupCodesTitle": "バックアップ・コード",
|
||||
"authenticatorBackupCodesMessage": "8桁のバックアップ・コードの入手",
|
||||
"authenticatorBackupCodesFinishSetUpMessage": "この時点で12個のバックアップ・コードが生成されました。それぞれ一度だけ使用できます。",
|
||||
"authenticatorMobileSetupTitle": "モバイル・オーセンティケーターのセットアップ",
|
||||
"smscodeIntroMessage": "電話番号を入力すると、確認コードがあなたの電話に送信されます。",
|
||||
"mobileSetupStep1":
|
||||
"携帯電話にオーセンティケーター・アプリケーションをインストールします。ここにリストされているアプリケーションがサポートされています。",
|
||||
"mobileSetupStep2": "アプリケーションを開き、バーコードをスキャンしてください。",
|
||||
"mobileSetupStep3": "アプリケーションから提供されたワンタイムコードを入力し、保存をクリックしてセットアップを終了します。",
|
||||
"scanBarCode": "バーコードをスキャンしますか?",
|
||||
"enterBarCode": "ワンタイムコードを入力してください",
|
||||
"doCopy": "コピー",
|
||||
"doFinish": "終了",
|
||||
"authenticatorSMSCodeSetupTitle": "SMSコードのセットアップ",
|
||||
"chooseYourCountry": "国を選んでください",
|
||||
"enterYourPhoneNumber": "電話番号を入力してください",
|
||||
"sendVerficationCode": "確認コードの送信",
|
||||
"enterYourVerficationCode": "確認コードを入力してください",
|
||||
"authenticatorBackupCodesSetupTitle": "バックアップ・コードのセットアップ",
|
||||
"backupcodesIntroMessage":
|
||||
"携帯電話にアクセスできない場合でも、バックアップ・コードを使用してアカウントにログインできます。どこか安全でアクセス可能な場所に保管してください。",
|
||||
"realmName": "レルム",
|
||||
"doDownload": "ダウンロード",
|
||||
"doPrint": "印刷",
|
||||
"backupCodesTips-1": "各バックアップ・コードは1回使用できます。",
|
||||
"backupCodesTips-2": "これらのコードはこの日に生成されました:",
|
||||
"generateNewBackupCodes": "新しいバックアップ・コードを生成する",
|
||||
"backupCodesTips-3": "新しいバックアップ・コードを生成すると、現在のコードは機能しなくなります。",
|
||||
"backtoAuthenticatorPage": "オーセンティケーター・ページに戻る",
|
||||
"resources": "リソース",
|
||||
"sharedwithMe": "私と共有",
|
||||
"share": "共有",
|
||||
"sharedwith": "共有",
|
||||
"accessPermissions": "アクセス・パーミッション",
|
||||
"permissionRequests": "パーミッションの要求",
|
||||
"approve": "承認",
|
||||
"approveAll": "すべて承認",
|
||||
"people": "人",
|
||||
"perPage": "1ページあたり",
|
||||
"currentPage": "現在のページ",
|
||||
"sharetheResource": "リソースの共有",
|
||||
"group": "グループ",
|
||||
"selectPermission": "パーミッションを選択",
|
||||
"addPeople": "あなたのリソースを共有する人を追加",
|
||||
"addTeam": "あなたのリソースを共有するチームを追加",
|
||||
"myPermissions": "私のパーミッション",
|
||||
"waitingforApproval": "承認待ち",
|
||||
"anyPermission": "任意のパーミッション",
|
||||
"openshift.scope.user_info": "ユーザー情報",
|
||||
"openshift.scope.user_check-access": "ユーザーアクセス情報",
|
||||
"openshift.scope.user_full": "フルアクセス",
|
||||
"openshift.scope.list-projects": "プロジェクトの一覧表示"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,143 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Saugoti",
|
||||
"doCancel": "Atšaukti",
|
||||
"doLogOutAllSessions": "Atjungti visas sesijas",
|
||||
"doRemove": "Šalinti",
|
||||
"doAdd": "Pridėti",
|
||||
"doSignOut": "Atsijungti",
|
||||
"editAccountHtmlTitle": "Redaguoti paskyrą",
|
||||
"federatedIdentitiesHtmlTitle": "Susietos paskyros",
|
||||
"accountLogHtmlTitle": "Paskyros žurnalas",
|
||||
"changePasswordHtmlTitle": "Keisti slaptažodį",
|
||||
"sessionsHtmlTitle": "Prisijungimo sesijos",
|
||||
"accountManagementTitle": "Keycloak Naudotojų Administravimas",
|
||||
"authenticatorTitle": "Autentifikatorius",
|
||||
"applicationsHtmlTitle": "Programos",
|
||||
"authenticatorCode": "Vienkartinis kodas",
|
||||
"email": "El. paštas",
|
||||
"firstName": "Vardas",
|
||||
"givenName": "Pavardė",
|
||||
"fullName": "Pilnas vardas",
|
||||
"lastName": "Pavardė",
|
||||
"familyName": "Pavardė",
|
||||
"password": "Slaptažodis",
|
||||
"passwordConfirm": "Pakartotas slaptažodis",
|
||||
"passwordNew": "Naujas slaptažodis",
|
||||
"username": "Naudotojo vardas",
|
||||
"address": "Adresas",
|
||||
"street": "Gatvė",
|
||||
"locality": "Miestas arba vietovė",
|
||||
"region": "Rajonas",
|
||||
"postal_code": "Pašto kodas",
|
||||
"country": "Šalis",
|
||||
"emailVerified": "El. pašto adresas patvirtintas",
|
||||
"gssDelegationCredential": "GSS prisijungimo duomenų delegavimas",
|
||||
"role_admin": "Administratorius",
|
||||
"role_realm-admin": "Srities administravimas",
|
||||
"role_create-realm": "Kurti sritį",
|
||||
"role_view-realm": "Peržiūrėti sritį",
|
||||
"role_view-users": "Peržiūrėti naudotojus",
|
||||
"role_view-applications": "Peržiūrėti programas",
|
||||
"role_view-clients": "Peržiūrėti klientines programas",
|
||||
"role_view-events": "Peržiūrėti įvykių žurnalą",
|
||||
"role_view-identity-providers": "Peržiūrėti tapatybės teikėjus",
|
||||
"role_manage-realm": "Valdyti sritis",
|
||||
"role_manage-users": "Valdyti naudotojus",
|
||||
"role_manage-applications": "Valdyti programas",
|
||||
"role_manage-identity-providers": "Valdyti tapatybės teikėjus",
|
||||
"role_manage-clients": "Valdyti programas",
|
||||
"role_manage-events": "Valdyti įvykius",
|
||||
"role_view-profile": "Peržiūrėti paskyrą",
|
||||
"role_manage-account": "Valdyti paskyrą",
|
||||
"role_read-token": "Skaityti prieigos rakšą",
|
||||
"role_offline-access": "Darbas neprisijungus",
|
||||
"role_uma_authorization": "Įgauti UMA autorizavimo teises",
|
||||
"client_account": "Paskyra",
|
||||
"client_security-admin-console": "Saugumo administravimo konsolė",
|
||||
"client_admin-cli": "Administravimo CLI",
|
||||
"client_realm-management": "Srities valdymas",
|
||||
"client_broker": "Tarpininkas",
|
||||
"requiredFields": "Privalomi laukai",
|
||||
"allFieldsRequired": "Visi laukai yra privalomi",
|
||||
"backToApplication": "« Grįžti į programą",
|
||||
"backTo": "Atgal į {0}",
|
||||
"date": "Data",
|
||||
"event": "Įvykis",
|
||||
"ip": "IP",
|
||||
"client": "Klientas",
|
||||
"clients": "Klientai",
|
||||
"details": "Detaliau",
|
||||
"started": "Sukūrimo laikas",
|
||||
"lastAccess": "Vėliausia prieiga",
|
||||
"expires": "Galioja iki",
|
||||
"applications": "Programos",
|
||||
"account": "Paskyra",
|
||||
"federatedIdentity": "Susieta tapatybė",
|
||||
"authenticator": "Autentifikatorius",
|
||||
"sessions": "Sesijos",
|
||||
"log": "Įvykiai",
|
||||
"application": "Programa",
|
||||
"availablePermissions": "Galimos teisės",
|
||||
"grantedPermissions": "Įgalintos teisės",
|
||||
"grantedPersonalInfo": "Įgalinta asmeninė informacija",
|
||||
"additionalGrants": "Papildomi įgaliojimai",
|
||||
"action": "Veiksmas",
|
||||
"inResource": "yra",
|
||||
"fullAccess": "Pilna prieiga",
|
||||
"offlineToken": "Režimo neprisijungus raktas (token)",
|
||||
"revoke": "Atšaukti įgaliojimą",
|
||||
"configureAuthenticators": "Sukonfigūruotas autentifikatorius",
|
||||
"mobile": "Mobilus",
|
||||
"totpStep1":
|
||||
'Įdiekite <a href="https://freeotp.github.io/" target="_blank">FreeOTP</a> arba Google Authenticator savo įrenginyje. Programėlės prieinamos <a href="https://play.google.com">Google Play</a> ir Apple App Store.',
|
||||
"totpStep2": "Atidarykite programėlę ir nuskenuokite barkodą arba įveskite kodą.",
|
||||
"totpStep3": "Įveskite programėlėje sugeneruotą vieną kartą galiojantį kodą ir paspauskite Saugoti norėdami prisijungti.",
|
||||
"missingUsernameMessage": "Prašome įvesti naudotojo vardą.",
|
||||
"missingFirstNameMessage": "Prašome įvesti vardą.",
|
||||
"invalidEmailMessage": "Neteisingas el. pašto adresas.",
|
||||
"missingLastNameMessage": "Prašome įvesti pavardę.",
|
||||
"missingEmailMessage": "Prašome įvesti el. pašto adresą.",
|
||||
"missingPasswordMessage": "Prašome įvesti slaptažodį.",
|
||||
"notMatchPasswordMessage": "Slaptažodžiai nesutampa.",
|
||||
"missingTotpMessage": "Prašome įvesti autentifikacijos kodą.",
|
||||
"invalidPasswordExistingMessage": "Neteisingas dabartinis slaptažodis.",
|
||||
"invalidPasswordConfirmMessage": "Pakartotas slaptažodis nesutampa.",
|
||||
"invalidTotpMessage": "Neteisingas autentifikacijos kodas.",
|
||||
"usernameExistsMessage": "Toks naudotojas jau egzistuoja.",
|
||||
"emailExistsMessage": "El. pašto adresas jau egzistuoja.",
|
||||
"readOnlyUserMessage": "Tik skaitymui sukonfigūruotos paskyros duomenų atnaujinti neleidžiama.",
|
||||
"readOnlyPasswordMessage": "Tik skaitymui sukonfigūruotos paskyros slaptažodžio atnaujinti neleidžiama.",
|
||||
"successTotpMessage": "Mobilus autentifikatorius sukonfigūruotas.",
|
||||
"successTotpRemovedMessage": "Mobilus autentifikatorius pašalintas.",
|
||||
"successGrantRevokedMessage": "Įgalinimas pašalintas sėkmingai.",
|
||||
"accountUpdatedMessage": "Jūsų paskyros duomenys sėkmingai atnaujinti.",
|
||||
"accountPasswordUpdatedMessage": "Jūsų paskyros slaptažodis pakeistas.",
|
||||
"missingIdentityProviderMessage": "Nenurodytas tapatybės teikėjas.",
|
||||
"invalidFederatedIdentityActionMessage": "Neteisingas arba nežinomas veiksmas.",
|
||||
"identityProviderNotFoundMessage": "Nurodytas tapatybės teikėjas nerastas.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Nurodyta susieta tapatybė neaktyvi.",
|
||||
"federatedIdentityRemovingLastProviderMessage":
|
||||
"Jūs negalite pašalinti paskutinio tapatybės teikėjo sąsajos, nes Jūs neturite nusistatę paskyros slaptažodžio.",
|
||||
"identityProviderRedirectErrorMessage": "Klaida nukreipiant į tapatybės teikėjo puslapį.",
|
||||
"identityProviderRemovedMessage": "Tapatybės teikėjas sėkmingai pašalintas.",
|
||||
"identityProviderAlreadyLinkedMessage": "Susieta tapatybė iš {0} jau susieta su kita paskyra.",
|
||||
"staleCodeAccountMessage": "Puslapio galiojimas baigėsi. Bandykite dar kartą.",
|
||||
"consentDenied": "Prieiga draudžiama.",
|
||||
"accountDisabledMessage": "Paskyros galiojimas sustabdytas, kreipkitės į administratorių.",
|
||||
"accountTemporarilyDisabledMessage": "Paskyros galiojimas laikinai sustabdytas. Kreipkitės į administratorių arba pabandykite vėliau.",
|
||||
"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."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,143 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Opslaan",
|
||||
"doCancel": "Annuleer",
|
||||
"doLogOutAllSessions": "Alle sessies uitloggen",
|
||||
"doRemove": "Verwijder",
|
||||
"doAdd": "Voeg toe",
|
||||
"doSignOut": "Afmelden",
|
||||
"editAccountHtmlTitle": "Bewerk account",
|
||||
"federatedIdentitiesHtmlTitle": "Federated Identities",
|
||||
"accountLogHtmlTitle": "Account log",
|
||||
"changePasswordHtmlTitle": "Verander wachtwoord",
|
||||
"sessionsHtmlTitle": "Sessies",
|
||||
"accountManagementTitle": "Keycloak Accountbeheer",
|
||||
"authenticatorTitle": "Authenticator",
|
||||
"applicationsHtmlTitle": "Toepassingen",
|
||||
"authenticatorCode": "Eenmalige code",
|
||||
"email": "E-mailadres",
|
||||
"firstName": "Voornaam",
|
||||
"givenName": "Voornaam",
|
||||
"fullName": "Volledige naam",
|
||||
"lastName": "Achternaam",
|
||||
"familyName": "Achternaam",
|
||||
"password": "Wachtwoord",
|
||||
"passwordConfirm": "Bevestiging",
|
||||
"passwordNew": "Nieuw Wachtwoord",
|
||||
"username": "Gebruikersnaam",
|
||||
"address": "Adres",
|
||||
"street": "Straat",
|
||||
"locality": "Stad of plaats",
|
||||
"region": "Staat, provincie of regio",
|
||||
"postal_code": "Postcode",
|
||||
"country": "Land",
|
||||
"emailVerified": "E-mailadres geverifieerd",
|
||||
"gssDelegationCredential": "GSS gedelegeerde aanmeldgegevens",
|
||||
"role_admin": "Beheer",
|
||||
"role_realm-admin": "Realmbeheer",
|
||||
"role_create-realm": "Creëer realm",
|
||||
"role_view-realm": "Bekijk realm",
|
||||
"role_view-users": "Bekijk gebruikers",
|
||||
"role_view-applications": "Bekijk toepassingen",
|
||||
"role_view-clients": "Bekijk clients",
|
||||
"role_view-events": "Bekijk gebeurtenissen",
|
||||
"role_view-identity-providers": "Bekijk identity providers",
|
||||
"role_manage-realm": "Beheer realm",
|
||||
"role_manage-users": "Beheer gebruikers",
|
||||
"role_manage-applications": "Beheer toepassingen",
|
||||
"role_manage-identity-providers": "Beheer identity providers",
|
||||
"role_manage-clients": "Beheer clients",
|
||||
"role_manage-events": "Beheer gebeurtenissen",
|
||||
"role_view-profile": "Bekijk profiel",
|
||||
"role_manage-account": "Beheer account",
|
||||
"role_manage-account-links": "Beheer accountkoppelingen",
|
||||
"role_read-token": "Lees token",
|
||||
"role_offline-access": "Offline toegang",
|
||||
"role_uma_authorization": "Verkrijg UMA rechten",
|
||||
"client_account": "Account",
|
||||
"client_security-admin-console": "Console Veligheidsbeheer",
|
||||
"client_admin-cli": "Beheer CLI",
|
||||
"client_realm-management": "Realmbeheer",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Verplichte velden",
|
||||
"allFieldsRequired": "Alle velden verplicht",
|
||||
"backToApplication": "« Terug naar toepassing",
|
||||
"backTo": "Terug naar {0}",
|
||||
"date": "Datum",
|
||||
"event": "Gebeurtenis",
|
||||
"ip": "IP",
|
||||
"client": "Client",
|
||||
"clients": "Clients",
|
||||
"details": "Details",
|
||||
"started": "Gestart",
|
||||
"lastAccess": "Laatste toegang",
|
||||
"expires": "Vervalt",
|
||||
"applications": "Toepassingen",
|
||||
"account": "Account",
|
||||
"federatedIdentity": "Federated Identity",
|
||||
"authenticator": "Authenticator",
|
||||
"sessions": "Sessies",
|
||||
"log": "Log",
|
||||
"application": "Toepassing",
|
||||
"availablePermissions": "Beschikbare rechten",
|
||||
"grantedPermissions": "Gegunde rechten",
|
||||
"grantedPersonalInfo": "Gegunde Persoonsgegevens",
|
||||
"additionalGrants": "Verdere vergunningen",
|
||||
"action": "Actie",
|
||||
"inResource": "in",
|
||||
"fullAccess": "Volledige toegang",
|
||||
"offlineToken": "Offline Token",
|
||||
"revoke": "Vergunning intrekken",
|
||||
"configureAuthenticators": "Ingestelde authenticators",
|
||||
"mobile": "Mobiel nummer",
|
||||
"totpStep1": "Installeer een van de onderstaande applicaties op uw mobiele apparaat:",
|
||||
"totpStep2": "Open de toepassing en scan de QR-code of voer de sleutel in.",
|
||||
"totpStep3": "Voer de door de toepassing gegeven eenmalige code in en klik op Opslaan om de configuratie af te ronden.",
|
||||
"missingUsernameMessage": "Gebruikersnaam ontbreekt.",
|
||||
"missingFirstNameMessage": "Voornaam onbreekt.",
|
||||
"invalidEmailMessage": "Ongeldig e-mailadres.",
|
||||
"missingLastNameMessage": "Achternaam ontbreekt.",
|
||||
"missingEmailMessage": "E-mailadres ontbreekt.",
|
||||
"missingPasswordMessage": "Wachtwoord ontbreekt.",
|
||||
"notMatchPasswordMessage": "Wachtwoorden komen niet overeen.",
|
||||
"missingTotpMessage": "Authenticatiecode ontbreekt.",
|
||||
"invalidPasswordExistingMessage": "Ongeldig bestaand wachtwoord.",
|
||||
"invalidPasswordConfirmMessage": "Wachtwoordbevestiging komt niet overeen.",
|
||||
"invalidTotpMessage": "Ongeldige authenticatiecode.",
|
||||
"emailExistsMessage": "E-mailadres bestaat reeds.",
|
||||
"readOnlyUserMessage": "U kunt uw account niet bijwerken aangezien het account alleen-lezen is.",
|
||||
"readOnlyPasswordMessage": "U kunt uw wachtwoord niet wijzigen omdat uw account alleen-lezen is.",
|
||||
"successTotpMessage": "Mobiele authenticator geconfigureerd.",
|
||||
"successTotpRemovedMessage": "Mobiele authenticator verwijderd.",
|
||||
"successGrantRevokedMessage": "Vergunning succesvol ingetrokken",
|
||||
"accountUpdatedMessage": "Uw account is gewijzigd.",
|
||||
"accountPasswordUpdatedMessage": "Uw wachtwoord is gewijzigd.",
|
||||
"missingIdentityProviderMessage": "Geen identity provider aangegeven.",
|
||||
"invalidFederatedIdentityActionMessage": "Ongeldige of ontbrekende actie op federated identity.",
|
||||
"identityProviderNotFoundMessage": "Gespecificeerde identity provider niet gevonden.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Deze federated identity is niet langer geldig.",
|
||||
"federatedIdentityRemovingLastProviderMessage":
|
||||
"U kunt de laatste federated identity provider niet verwijderen aangezien u dan niet langer zou kunnen inloggen.",
|
||||
"identityProviderRedirectErrorMessage": "Kon niet herverwijzen naar identity provider.",
|
||||
"identityProviderRemovedMessage": "Identity provider met succes verwijderd.",
|
||||
"identityProviderAlreadyLinkedMessage": "Door {0} teruggegeven federated identity is al gekoppeld aan een andere gebruiker.",
|
||||
"staleCodeAccountMessage": "De pagina is verlopen. Probeer het nogmaals.",
|
||||
"consentDenied": "Toestemming geweigerd",
|
||||
"accountDisabledMessage": "Account is gedeactiveerd. Contacteer de beheerder.",
|
||||
"accountTemporarilyDisabledMessage": "Account is tijdelijk deactiveerd, neem contact op met de beheerder of probeer het later opnieuw.",
|
||||
"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."
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,153 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {
|
||||
"doSave": "Lagre",
|
||||
"doCancel": "Avbryt",
|
||||
"doLogOutAllSessions": "Logg ut av alle sesjoner",
|
||||
"doRemove": "Fjern",
|
||||
"doAdd": "Legg til",
|
||||
"doSignOut": "Logg ut",
|
||||
"editAccountHtmlTitle": "Rediger konto",
|
||||
"federatedIdentitiesHtmlTitle": "Federerte identiteter",
|
||||
"accountLogHtmlTitle": "Kontologg",
|
||||
"changePasswordHtmlTitle": "Endre passord",
|
||||
"sessionsHtmlTitle": "Sesjoner",
|
||||
"accountManagementTitle": "Keycloak kontoadministrasjon",
|
||||
"authenticatorTitle": "Autentikator",
|
||||
"applicationsHtmlTitle": "Applikasjoner",
|
||||
"authenticatorCode": "Engangskode",
|
||||
"email": "E-post",
|
||||
"firstName": "Fornavn",
|
||||
"givenName": "Fornavn",
|
||||
"fullName": "Fullt navn",
|
||||
"lastName": "Etternavn",
|
||||
"familyName": "Etternavn",
|
||||
"password": "Passord",
|
||||
"passwordConfirm": "Bekreftelse",
|
||||
"passwordNew": "Nytt passord",
|
||||
"username": "Brukernavn",
|
||||
"address": "Adresse",
|
||||
"street": "Gate-/veinavn + husnummer",
|
||||
"locality": "By",
|
||||
"region": "Fylke",
|
||||
"postal_code": "Postnummer",
|
||||
"country": "Land",
|
||||
"emailVerified": "E-post bekreftet",
|
||||
"gssDelegationCredential": "GSS legitimasjonsdelegering",
|
||||
"role_admin": "Administrator",
|
||||
"role_realm-admin": "Administrator for sikkerhetsdomene",
|
||||
"role_create-realm": "Opprette sikkerhetsdomene",
|
||||
"role_view-realm": "Se sikkerhetsdomene",
|
||||
"role_view-users": "Se brukere",
|
||||
"role_view-applications": "Se applikasjoner",
|
||||
"role_view-clients": "Se klienter",
|
||||
"role_view-events": "Se hendelser",
|
||||
"role_view-identity-providers": "Se identitetsleverandører",
|
||||
"role_manage-realm": "Administrere sikkerhetsdomene",
|
||||
"role_manage-users": "Administrere brukere",
|
||||
"role_manage-applications": "Administrere applikasjoner",
|
||||
"role_manage-identity-providers": "Administrere identitetsleverandører",
|
||||
"role_manage-clients": "Administrere klienter",
|
||||
"role_manage-events": "Administrere hendelser",
|
||||
"role_view-profile": "Se profil",
|
||||
"role_manage-account": "Administrere konto",
|
||||
"role_read-token": "Lese token",
|
||||
"role_offline-access": "Frakoblet tilgang",
|
||||
"role_uma_authorization": "Skaffe tillatelser",
|
||||
"client_account": "Konto",
|
||||
"client_security-admin-console": "Sikkerhetsadministrasjonskonsoll",
|
||||
"client_admin-cli": "Kommandolinje-grensesnitt for administrator",
|
||||
"client_realm-management": "Sikkerhetsdomene-administrasjon",
|
||||
"client_broker": "Broker",
|
||||
"requiredFields": "Obligatoriske felt",
|
||||
"allFieldsRequired": "Alle felt må fylles ut",
|
||||
"backToApplication": "« Tilbake til applikasjonen",
|
||||
"backTo": "Tilbake til {0}",
|
||||
"date": "Dato",
|
||||
"event": "Hendelse",
|
||||
"ip": "IP",
|
||||
"client": "Klient",
|
||||
"clients": "Klienter",
|
||||
"details": "Detaljer",
|
||||
"started": "Startet",
|
||||
"lastAccess": "Sist benyttet",
|
||||
"expires": "Utløper",
|
||||
"applications": "Applikasjoner",
|
||||
"account": "Konto",
|
||||
"federatedIdentity": "Federert identitet",
|
||||
"authenticator": "Autentikator",
|
||||
"sessions": "Sesjoner",
|
||||
"log": "Logg",
|
||||
"application": "Applikasjon",
|
||||
"availablePermissions": "Tilgjengelige rettigheter",
|
||||
"grantedPermissions": "Innvilgede rettigheter",
|
||||
"grantedPersonalInfo": "Innvilget personlig informasjon",
|
||||
"additionalGrants": "Ekstra rettigheter",
|
||||
"action": "Handling",
|
||||
"inResource": "i",
|
||||
"fullAccess": "Full tilgang",
|
||||
"offlineToken": "Offline token",
|
||||
"revoke": "Opphev rettighet",
|
||||
"configureAuthenticators": "Konfigurerte autentikatorer",
|
||||
"mobile": "Mobiltelefon",
|
||||
"totpStep1": "Installer ett av følgende programmer på mobilen din.",
|
||||
"totpStep2": "Åpne applikasjonen og skann strekkoden eller skriv inn koden.",
|
||||
"totpStep3": "Skriv inn engangskoden gitt av applikasjonen og klikk Lagre for å fullføre.",
|
||||
"missingUsernameMessage": "Vennligst oppgi brukernavn.",
|
||||
"missingFirstNameMessage": "Vennligst oppgi fornavn.",
|
||||
"invalidEmailMessage": "Ugyldig e-postadresse.",
|
||||
"missingLastNameMessage": "Vennligst oppgi etternavn.",
|
||||
"missingEmailMessage": "Vennligst oppgi e-postadresse.",
|
||||
"missingPasswordMessage": "Vennligst oppgi passord.",
|
||||
"notMatchPasswordMessage": "Passordene er ikke like.",
|
||||
"missingTotpMessage": "Vennligst oppgi engangskode.",
|
||||
"invalidPasswordExistingMessage": "Ugyldig eksisterende passord.",
|
||||
"invalidPasswordConfirmMessage": "Passordene er ikke like.",
|
||||
"invalidTotpMessage": "Ugyldig engangskode.",
|
||||
"usernameExistsMessage": "Brukernavnet finnes allerede.",
|
||||
"emailExistsMessage": "E-postadressen finnes allerede.",
|
||||
"readOnlyUserMessage": "Du kan ikke oppdatere kontoen din ettersom den er skrivebeskyttet.",
|
||||
"readOnlyPasswordMessage": "Du kan ikke oppdatere passordet ditt ettersom kontoen din er skrivebeskyttet.",
|
||||
"successTotpMessage": "Autentikator for mobiltelefon er konfigurert.",
|
||||
"successTotpRemovedMessage": "Autentikator for mobiltelefon er fjernet.",
|
||||
"successGrantRevokedMessage": "Vellykket oppheving av rettighet.",
|
||||
"accountUpdatedMessage": "Kontoen din har blitt oppdatert.",
|
||||
"accountPasswordUpdatedMessage": "Ditt passord har blitt oppdatert.",
|
||||
"missingIdentityProviderMessage": "Identitetsleverandør er ikke spesifisert.",
|
||||
"invalidFederatedIdentityActionMessage": "Ugyldig eller manglende handling.",
|
||||
"identityProviderNotFoundMessage": "Spesifisert identitetsleverandør ikke funnet.",
|
||||
"federatedIdentityLinkNotActiveMessage": "Denne identiteten er ikke lenger aktiv.",
|
||||
"federatedIdentityRemovingLastProviderMessage": "Du kan ikke fjerne siste federerte identitet ettersom du ikke har et passord.",
|
||||
"identityProviderRedirectErrorMessage": "Redirect til identitetsleverandør feilet.",
|
||||
"identityProviderRemovedMessage": "Fjerning av identitetsleverandør var vellykket.",
|
||||
"identityProviderAlreadyLinkedMessage": "Federert identitet returnert av {0} er allerede koblet til en annen bruker.",
|
||||
"staleCodeAccountMessage": "Siden har utløpt. Vennligst prøv en gang til.",
|
||||
"consentDenied": "Samtykke avslått.",
|
||||
"accountDisabledMessage": "Konto er deaktivert, kontakt administrator.",
|
||||
"accountTemporarilyDisabledMessage": "Konto er midlertidig deaktivert, kontakt administrator eller prøv igjen senere.",
|
||||
"invalidPasswordMinLengthMessage": "Ugyldig passord: minimum lengde {0}.",
|
||||
"invalidPasswordMinLowerCaseCharsMessage": "Ugyldig passord: må inneholde minimum {0} små bokstaver.",
|
||||
"invalidPasswordMinDigitsMessage": "Ugyldig passord: må inneholde minimum {0} sifre.",
|
||||
"invalidPasswordMinUpperCaseCharsMessage": "Ugyldig passord: må inneholde minimum {0} store bokstaver.",
|
||||
"invalidPasswordMinSpecialCharsMessage": "Ugyldig passord: må inneholde minimum {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.",
|
||||
"locale_ca": "Català",
|
||||
"locale_de": "Deutsch",
|
||||
"locale_en": "English",
|
||||
"locale_es": "Español",
|
||||
"locale_fr": "Français",
|
||||
"locale_it": "Italian",
|
||||
"locale_ja": "日本語",
|
||||
"locale_no": "Norsk",
|
||||
"locale_nl": "Nederlands",
|
||||
"locale_pt-BR": "Português (Brasil)",
|
||||
"locale_ru": "Русский",
|
||||
"locale_zh-CN": "中文简体"
|
||||
};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|
@ -1,8 +0,0 @@
|
||||
//This code was automatically generated by running dist/bin/generate-i18n-messages.js
|
||||
//PLEASE DO NOT EDIT MANUALLY
|
||||
|
||||
/* spell-checker: disable */
|
||||
const messages = {};
|
||||
|
||||
export default messages;
|
||||
/* spell-checker: enable */
|