Compare commits

...

69 Commits

Author SHA1 Message Date
d8b00da3a1 Update tss-react 2022-09-08 15:27:41 +02:00
a24945bc1b Bump version 2022-09-08 15:18:21 +02:00
158759493f Merge pull request #170 from Tasyp/feature/silent
feat: add silent flag
2022-09-08 15:17:45 +02:00
36e32d6ddc Update src/bin/download-builtin-keycloak-theme.ts 2022-09-08 15:13:21 +02:00
84908e2ec0 Update src/bin/generate-i18n-messages.ts 2022-09-08 15:13:15 +02:00
a2dc51d811 Update src/bin/create-keycloak-email-directory.ts 2022-09-08 15:13:09 +02:00
fb3b0e2c29 fix: make sure external assets flag is a boolean 2022-09-08 15:46:27 +03:00
1a3e4c68bb test: update tests to include silent flag 2022-09-08 15:43:03 +03:00
11b2342da0 feat: add silent flag 2022-09-08 13:52:10 +03:00
80d4a808d3 Update readme 2022-09-07 13:45:34 +02:00
da4146eb59 Bump version (changelog ignore) 2022-09-07 13:40:18 +02:00
a0be35db8b Fix compat with StrictMode react 18 2022-09-07 13:39:54 +02:00
14db9cd523 Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 12:02:35 +02:00
0c315385dd Update fix tests 2022-09-07 12:00:23 +02:00
c0a0eb02fb Remove Open collectivity 2022-09-07 11:59:42 +02:00
ee407c32ad Create FUNDING.yaml 2022-09-07 11:53:18 +02:00
9262d21829 Bump version 2022-09-07 11:50:41 +02:00
a13f710325 Important fix for --external-assets 2022-09-07 11:50:24 +02:00
eac1a6036f Merge branch 'main' of https://github.com/InseeFrLab/keycloakify 2022-09-07 11:26:18 +02:00
987f3d7586 Bump version (changelog ignore) 2022-09-07 11:26:10 +02:00
875322669c Rename isAppAndKeycloakServerSharingSameDomain to areAppAndKeycloakServerSharingSameDomain #145 2022-09-07 11:25:46 +02:00
33a264b3d0 Update README.md 2022-09-07 00:32:38 +02:00
c059eff170 Update README 2022-09-06 19:14:39 +02:00
b4a22fc9dd Fix readme 2022-09-06 19:13:46 +02:00
6d1cbdc463 Bump version 2022-09-06 19:12:59 +02:00
2bfbba4daf Upgrade tss-react 2022-09-06 17:43:30 +02:00
21ffe82bde Bump version 2022-09-06 17:40:06 +02:00
8e6f597027 Fix bug with --external-assets 2022-09-06 17:39:47 +02:00
16c5065560 Bump version 2022-09-05 00:09:07 +02:00
c4b985f1a4 Fix replacers 2022-09-05 00:08:50 +02:00
042747c7d2 Bump version 2022-09-04 23:19:53 +02:00
e4a46f31de Make it allright not to provide validators object on mock data 2022-09-04 23:19:33 +02:00
6d9e62d2b4 Remove unessesary log 2022-09-04 21:48:46 +02:00
9caaa507b1 Bump version 2022-09-01 22:35:15 +02:00
5c7d3c5b44 lib target ES2017 instead of ES2020 2022-09-01 22:35:01 +02:00
8bac57d87a Bump version 2022-09-01 21:31:41 +02:00
b8d759cd63 Minor refactor for dryness 2022-09-01 21:31:27 +02:00
da72e3e5ac Bump version (changelog ignore) 2022-09-01 21:16:39 +02:00
2afd36fee0 Avoid fetching locale twitch (react 18 useEffect) 2022-09-01 21:16:25 +02:00
b7e75d8828 Bump beta version 2022-09-01 17:22:35 +02:00
30e20f4e7d Avoid redefining the properties 2022-09-01 17:22:15 +02:00
ce0ab8dccf Better linking script 2022-09-01 16:36:38 +02:00
5b20ab2f7c Upgrade to tss-react v4 2022-09-01 15:13:24 +02:00
daaaed43df Rename keycloakify-demo-app in keycloakify-starter 2022-09-01 14:58:59 +02:00
3a4bd791ad Fix release (changelog ignore) 2022-08-28 12:37:03 +07:00
eecddd7f6b Update CI (changelog ignore) 2022-08-28 12:31:58 +07:00
a34eaa136e Update ts-ci (changelog ignore) 2022-08-28 12:16:27 +07:00
53be8b5e96 Updat README 2022-08-28 12:10:45 +07:00
f0ae5ea908 Enable the CI to run on the v6 branch 2022-08-26 16:17:03 +07:00
9910556a8b Bump version (changelog ignore) 2022-08-26 15:43:50 +07:00
5997416e1b Update i18n API JSDoc comments 2022-08-26 15:43:32 +07:00
9a9fc56f85 Improve documentation comment i18n (changelog ignore) 2022-08-23 07:58:39 +07:00
2a5e919f29 Bump version (changelog ignore) 2022-08-22 17:18:09 +07:00
8031d51e15 Rename build-keycloak-theme -> keycloakify 2022-08-22 17:17:35 +07:00
56ce9c0d0d Bump version (changelog ignore) 2022-08-20 14:56:40 +07:00
8cd584cbd5 Implementation of #160 and #103 for v6 2022-08-20 14:56:20 +07:00
f5b87f4669 Fix option parsing 2022-08-20 14:04:47 +07:00
a1a65c5529 Prettier: switch to trailing coma: none 2022-08-20 11:44:48 +07:00
832434095e Restore tsproject.json 2022-08-16 14:45:18 +07:00
b85f1ef351 Merge branch 'v6' of https://github.com/InseeFrLab/keycloakify into v6 2022-08-16 14:42:05 +07:00
8bee5d788e #145 2022-08-16 14:41:06 +07:00
0752d857e2 Merge pull request #155 from schlich/hotfix-ci 2022-08-14 11:58:07 +07:00
07e4056694 change outDir to path at root per NPM CI error message 2022-08-13 23:00:09 -05:00
0eb4ab85b3 Fix lazy loading of css chunks #141 2022-08-02 21:00:52 +02:00
69ef47daf8 Bump beta version (changelog ignore) 2022-08-01 06:04:23 +02:00
6eaa1f69ac Fix dynamic imports 2022-08-01 06:03:48 +02:00
5aab75fae0 Only downoad terms on the Terms page 2022-08-01 04:54:02 +02:00
7407c98005 Stop self promotion in log 2022-08-01 04:49:48 +02:00
dcd4322e44 Fix CI 2022-08-01 04:06:10 +02:00
282 changed files with 1733 additions and 1110 deletions

2
.gitattributes vendored
View File

@ -1,3 +1,3 @@
src/lib/i18n/generated_kcMessages/* linguist-documentation
src/bin/build-keycloak-theme/index.ts -linguist-detectable
src/bin/keycloakify/index.ts -linguist-detectable
src/bin/install-builtin-keycloak-themes.ts -linguist-detectable

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

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

View File

@ -35,7 +35,7 @@ jobs:
steps:
- name: Tell if project is using npm or yarn
id: step1
uses: garronej/ts-ci@v1.1.4
uses: garronej/ts-ci@v1.1.7
with:
action_name: tell_if_project_uses_npm_or_yarn
- uses: actions/checkout@v2.3.4
@ -67,7 +67,7 @@ jobs:
is_upgraded_version: ${{ steps.step1.outputs.is_upgraded_version }}
is_release_beta: ${{steps.step1.outputs.is_release_beta }}
steps:
- uses: garronej/ts-ci@v1.1.4
- uses: garronej/ts-ci@v1.1.7
id: step1
with:
action_name: is_package_json_version_upgraded
@ -117,7 +117,7 @@ jobs:
PACKAGE_MANAGER=yarn
fi
$PACKAGE_MANAGER run build
- run: npx -y -p denoify@0.11.7 denoify_enable_short_npm_import_path
- run: npx -y -p denoify@1.0.2 enable_short_npm_import_path
env:
DRY_RUN: "0"
- name: Publishing on NPM

View File

@ -5,7 +5,7 @@
"semi": true,
"singleQuote": false,
"quoteProps": "preserve",
"trailingComma": "all",
"trailingComma": "none",
"bracketSpacing": true,
"arrowParens": "avoid"
}

View File

@ -2,7 +2,7 @@
<img src="https://user-images.githubusercontent.com/6702424/109387840-eba11f80-7903-11eb-9050-db1dad883f78.png">
</p>
<p align="center">
<i>🔏 Create Keycloak themes using React 🔏</i>
<i>🔏 Create Keycloak themes using React 🔏</i>
<br>
<br>
<a href="https://github.com/garronej/keycloakify/actions">
@ -27,7 +27,14 @@
<a href="https://www.keycloakify.dev">Home</a>
-
<a href="https://docs.keycloakify.dev">Documentation</a>
</p>
</p>
<p align="center"> ---- Project starter / Demo setup ---- </p>
<p align="center">
<a href="https://github.com/garronej/keycloakify-starter">CSS Level customization</a>
-
<a href="https://github.com/garronej/keycloakify-advanced-starter">Component Level customization</a>
</p>
<p align="center"> ---- </p>
</p>
@ -36,8 +43,21 @@
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
</p>
> 🗣 V6 have been released 🎉
> [It features major improvements](https://github.com/InseeFrLab/keycloakify#600).
> Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6).
# Changelog highlights
## 6.0.0
- Bundle size drastically reduced, locals and component dynamically loaded.
- First print much quicker, use of React.lazy() everywhere.
- Real i18n API.
- Actual documentation for build options.
Checkout [the migration guide](https://docs.keycloakify.dev/v5-to-v6)
## 5.8.0
- [React.lazy()](https://reactjs.org/docs/code-splitting.html#reactlazy) support 🎉. [#141](https://github.com/InseeFrLab/keycloakify/issues/141)

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "6.0.0-beta.3",
"version": "6.1.0",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -21,7 +21,7 @@
"format:check": "yarn _format --list-different"
},
"bin": {
"build-keycloak-theme": "dist/bin/build-keycloak-theme/index.js",
"keycloakify": "dist/bin/keycloakify/index.js",
"create-keycloak-email-directory": "dist/bin/create-keycloak-email-directory.js",
"download-builtin-keycloak-theme": "dist/bin/download-builtin-keycloak-theme.js"
},
@ -60,6 +60,7 @@
"devDependencies": {
"@emotion/react": "^11.4.1",
"@types/memoizee": "^0.4.7",
"@types/minimist": "^1.2.2",
"@types/node": "^17.0.25",
"@types/react": "18.0.9",
"copyfiles": "^2.4.1",
@ -75,14 +76,16 @@
"@octokit/rest": "^18.12.0",
"cheerio": "^1.0.0-rc.5",
"cli-select": "^1.1.2",
"evt": "^2.3.1",
"evt": "^2.4.0",
"memoizee": "^0.4.15",
"minimal-polyfills": "^2.2.1",
"minimal-polyfills": "^2.2.2",
"minimist": "^1.2.6",
"path-browserify": "^1.0.1",
"powerhooks": "^0.20.10",
"powerhooks": "^0.20.15",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tsafe": "^0.10.1",
"tss-react": "^3.7.1"
"tsafe": "^1.0.1",
"tss-react": "^4.1.3",
"zod": "^3.17.10"
}
}

View File

@ -1,139 +0,0 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode, replaceImportsInInlineCssCode, generateCssCodeToDefineGlobals } from "../replaceImportFromStatic";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [
"login.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl",
] as const;
export type PageId = typeof pageIds[number];
export function generateFtlFilesCodeFactory(params: {
cssGlobalsToDefine: Record<string, string>;
indexHtmlCode: string;
urlPathname: string;
urlOrigin: undefined | string;
}) {
const { cssGlobalsToDefine, indexHtmlCode, urlPathname, urlOrigin } = params;
const $ = cheerio.load(indexHtmlCode);
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!,
urlOrigin,
});
$(element).text(fixedJsCode);
});
$("style").each((...[, element]) => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!,
"urlPathname": params.urlPathname,
urlOrigin,
});
$(element).text(fixedCssCode);
});
(
[
["link", "href"],
["script", "src"],
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
const href = $(element).attr(attrName);
if (href === undefined) {
return;
}
$(element).attr(
attrName,
urlOrigin !== undefined
? href.replace(/^\//, `${urlOrigin}/`)
: href.replace(new RegExp(`^${urlPathname.replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/"),
);
}),
);
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>',
" </#list>",
"</#if>",
].join("\n"),
};
$("head").prepend(
[
...(Object.keys(cssGlobalsToDefine).length === 0
? []
: [
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
urlPathname,
}).cssCodeToPrependInHead,
"</style>",
"",
]),
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1],
].join("\n"),
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params: { pageId: string }): {
ftlCode: string;
} {
const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html();
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId,
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
return { ftlCode };
}
return { generateFtlFilesCode };
}

View File

@ -1,8 +0,0 @@
#!/usr/bin/env node
export * from "./build-keycloak-theme";
import { main } from "./build-keycloak-theme";
if (require.main === module) {
main();
}

View File

@ -1,116 +0,0 @@
import * as crypto from "crypto";
import { ftlValuesGlobalName } from "./ftlValuesGlobalName";
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; urlOrigin: undefined | string }): { fixedJsCode: string } {
/*
NOTE:
When we have urlOrigin defined it means that
we are building with --external-assets
so we have to make sur that the fixed js code will run
inside and outside keycloak.
When urlOrigin isn't defined we can assume the fixedJsCode
will always run in keycloak context.
*/
const { jsCode, urlOrigin } = params;
const fixedJsCode = jsCode
.replace(
/([a-zA-Z]+)\.([a-zA-Z]+)=function\(([a-zA-Z]+)\){return"static\/js\/"/g,
(...[, n, u, e]) => `
${n}[(function(){
${
urlOrigin === undefined
? `
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){}
});
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + p; },
set: function (value){ p = value;}
});
`
}
return "${u}";
})()] = function(${e}) { return "${urlOrigin === undefined ? "/build/" : ""}static/js/"`,
)
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
urlOrigin === undefined
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group} + "static/`,
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
urlOrigin === undefined
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${urlOrigin}" : "") + ${group2} + ${group3},`,
);
return { fixedJsCode };
}
export function replaceImportsInInlineCssCode(params: { cssCode: string; urlPathname: string; urlOrigin: undefined | string }): {
fixedCssCode: string;
} {
const { cssCode, urlPathname, urlOrigin } = params;
const fixedCssCode = cssCode.replace(
urlPathname === "/" ? /url\(["']?\/([^/][^)"']+)["']?\)/g : new RegExp(`url\\(["']?${urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) => `url(${urlOrigin === undefined ? "${url.resourcesPath}/build/" + group : params.urlOrigin + urlPathname + group})`,
);
return { fixedCssCode };
}
export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match),
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`)),
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; urlPathname: string }): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine, urlPathname } = params;
return {
"cssCodeToPrependInHead": [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName =>
[
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${urlPathname.replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/",
),
].join(" "),
)
.map(line => ` ${line};`),
"}",
].join("\n"),
};
}

View File

@ -1,16 +1,20 @@
#!/usr/bin/env node
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { keycloakThemeEmailDirPath } from "./build-keycloak-theme";
import { keycloakThemeEmailDirPath } from "./keycloakify";
import { join as pathJoin, basename as pathBasename } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./promptKeycloakVersion";
import * as fs from "fs";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
if (require.main === module) {
(async () => {
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
if (fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
logger.warn(`There is already a ./${pathBasename(keycloakThemeEmailDirPath)} directory in your project. Aborting.`);
process.exit(-1);
}
@ -22,14 +26,15 @@ if (require.main === module) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": builtinKeycloakThemeTmpDirPath,
isSilent
});
transformCodebase({
"srcDirPath": pathJoin(builtinKeycloakThemeTmpDirPath, "base", "email"),
"destDirPath": keycloakThemeEmailDirPath,
"destDirPath": keycloakThemeEmailDirPath
});
console.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
logger.log(`./${pathBasename(keycloakThemeEmailDirPath)} ready to be customized`);
fs.rmSync(builtinKeycloakThemeTmpDirPath, { "recursive": true, "force": true });
})();

View File

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

View File

@ -5,6 +5,8 @@ import { crawl } from "./tools/crawl";
import { downloadBuiltinKeycloakTheme } from "./download-builtin-keycloak-theme";
import { getProjectRoot } from "./tools/getProjectRoot";
import { rm_rf, rm_r } from "./tools/rm";
import { getCliOptions } from "./tools/cliOptions";
import { getLogger } from "./tools/logger";
//NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
@ -12,8 +14,11 @@ import { rm_rf, rm_r } from "./tools/rm";
//@ts-ignore
const propertiesParser = require("properties-parser");
const { isSilent } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
console.log({ keycloakVersion });
logger.log(JSON.stringify({ keycloakVersion }));
const tmpDirPath = pathJoin(getProjectRoot(), "tmp_xImOef9dOd44");
@ -22,6 +27,7 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent
});
type Dictionary = { [idiomId: string]: string };
@ -42,8 +48,8 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
Object.entries(propertiesParser.parse(fs.readFileSync(pathJoin(baseThemeDirPath, filePath)).toString("utf8"))).map(
([key, value]: any) => [key, value.replace(/''/g, "'")],
),
([key, value]: any) => [key, value.replace(/''/g, "'")]
)
);
});
}
@ -69,13 +75,13 @@ for (const keycloakVersion of ["11.0.3", "15.0.2", "18.0.1"]) {
`const messages= ${JSON.stringify(recordForPageType[language], null, 2)};`,
"",
"export default messages;",
"/* spell-checker: enable */",
"/* spell-checker: enable */"
].join("\n"),
"utf8",
),
"utf8"
)
);
console.log(`${filePath} wrote`);
logger.log(`${filePath} wrote`);
});
});
}

View File

@ -0,0 +1,180 @@
import { z } from "zod";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import { id } from "tsafe/id";
import { parse as urlParse } from "url";
type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
keycloakify?: {
extraPages?: string[];
extraThemeProperties?: string[];
areAppAndKeycloakServerSharingSameDomain?: boolean;
};
};
const zParsedPackageJson = z.object({
"name": z.string(),
"version": z.string(),
"homepage": z.string().optional(),
"keycloakify": z
.object({
"extraPages": z.array(z.string()).optional(),
"extraThemeProperties": z.array(z.string()).optional(),
"areAppAndKeycloakServerSharingSameDomain": z.boolean().optional()
})
.optional()
});
assert<Equals<ReturnType<typeof zParsedPackageJson["parse"]>, ParsedPackageJson>>();
/** Consolidated build option gathered form CLI arguments and config in package.json */
export type BuildOptions = BuildOptions.Standalone | BuildOptions.ExternalAssets;
export namespace BuildOptions {
export type Common = {
isSilent: boolean;
version: string;
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
//NOTE: Only for the pom.xml file, questionable utility...
groupId: string;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
export function readBuildOptions(params: {
packageJson: string;
CNAME: string | undefined;
isExternalAssetsCliParamProvided: boolean;
isSilent: boolean;
}): BuildOptions {
const { packageJson, CNAME, isExternalAssetsCliParamProvided, isSilent } = params;
const parsedPackageJson = zParsedPackageJson.parse(JSON.parse(packageJson));
const url = (() => {
const { homepage } = parsedPackageJson;
let url: URL | undefined = undefined;
if (homepage !== undefined) {
url = new URL(homepage);
}
if (CNAME !== undefined) {
url = new URL(`https://${CNAME.replace(/\s+$/, "")}`);
}
if (url === undefined) {
return undefined;
}
return {
"origin": url.origin,
"pathname": (() => {
const out = url.pathname.replace(/([^/])$/, "$1/");
return out === "/" ? undefined : out;
})()
};
})();
const common: BuildOptions.Common = (() => {
const { name, keycloakify = {}, version, homepage } = parsedPackageJson;
const { extraPages, extraThemeProperties } = keycloakify ?? {};
const themeName = name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
return {
themeName,
"groupId": (() => {
const fallbackGroupId = `${themeName}.keycloak`;
return (
(!homepage
? fallbackGroupId
: urlParse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})(),
"version": version,
extraPages,
extraThemeProperties,
isSilent
};
})();
if (isExternalAssetsCliParamProvided) {
const commonExternalAssets = id<BuildOptions.ExternalAssets.CommonExternalAssets>({
...common,
"isStandalone": false
});
if (parsedPackageJson.keycloakify?.areAppAndKeycloakServerSharingSameDomain) {
return id<BuildOptions.ExternalAssets.SameDomain>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": true
});
} else {
assert(
url !== undefined,
[
"Can't compile in external assets mode if we don't know where",
"the app will be hosted.",
"You should provide a homepage field in the package.json (or create a",
"public/CNAME file.",
"Alternatively, if your app and the Keycloak server are on the same domain, ",
"eg https://example.com is your app and https://example.com/auth is the keycloak",
'admin UI, you can set "keycloakify": { "areAppAndKeycloakServerSharingSameDomain": true }',
"in your package.json"
].join(" ")
);
return id<BuildOptions.ExternalAssets.DifferentDomains>({
...commonExternalAssets,
"areAppAndKeycloakServerSharingSameDomain": false,
"urlOrigin": url.origin,
"urlPathname": url.pathname
});
}
}
return id<BuildOptions.Standalone>({
...common,
"isStandalone": true,
"urlPathname": url?.pathname
});
}

View File

@ -0,0 +1,185 @@
import cheerio from "cheerio";
import { replaceImportsFromStaticInJsCode } from "../replacers/replaceImportsFromStaticInJsCode";
import { generateCssCodeToDefineGlobals } from "../replacers/replaceImportsInCssCode";
import { replaceImportsInInlineCssCode } from "../replacers/replaceImportsInInlineCssCode";
import * as fs from "fs";
import { join as pathJoin } from "path";
import { objectKeys } from "tsafe/objectKeys";
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
// https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/forms/login/freemarker/Templates.java
export const pageIds = [
"login.ftl",
"register.ftl",
"register-user-profile.ftl",
"info.ftl",
"error.ftl",
"login-reset-password.ftl",
"login-verify-email.ftl",
"terms.ftl",
"login-otp.ftl",
"login-update-profile.ftl",
"login-update-password.ftl",
"login-idp-link-confirm.ftl",
"login-idp-link-email.ftl",
"login-page-expired.ftl",
"login-config-totp.ftl",
"logout-confirm.ftl"
] as const;
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export type PageId = typeof pageIds[number];
export function generateFtlFilesCodeFactory(params: {
indexHtmlCode: string;
//NOTE: Expected to be an empty object if external assets mode is enabled.
cssGlobalsToDefine: Record<string, string>;
buildOptions: BuildOptionsLike;
}) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions } = params;
const $ = cheerio.load(indexHtmlCode);
fix_imports_statements: {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
break fix_imports_statements;
}
$("script:not([src])").each((...[, element]) => {
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": $(element).html()!,
buildOptions
});
$(element).text(fixedJsCode);
});
$("style").each((...[, element]) => {
const { fixedCssCode } = replaceImportsInInlineCssCode({
"cssCode": $(element).html()!,
buildOptions
});
$(element).text(fixedCssCode);
});
(
[
["link", "href"],
["script", "src"]
] as const
).forEach(([selector, attrName]) =>
$(selector).each((...[, element]) => {
const href = $(element).attr(attrName);
if (href === undefined) {
return;
}
$(element).attr(
attrName,
buildOptions.isStandalone
? href.replace(new RegExp(`^${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`), "${url.resourcesPath}/build/")
: href.replace(/^\//, `${buildOptions.urlOrigin}/`)
);
})
);
if (Object.keys(cssGlobalsToDefine).length !== 0) {
$("head").prepend(
[
"",
"<style>",
generateCssCodeToDefineGlobals({
cssGlobalsToDefine,
buildOptions
}).cssCodeToPrependInHead,
"</style>",
""
].join("\n")
);
}
}
//FTL is no valid html, we can't insert with cheerio, we put placeholder for injecting later.
const replaceValueBySearchValue = {
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }': fs
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1],
"<!-- xIdLqMeOedErIdLsPdNdI9dSlxI -->": [
"<#if scripts??>",
" <#list scripts as script>",
' <script src="${script}" type="text/javascript"></script>',
" </#list>",
"</#if>"
].join("\n")
};
$("head").prepend(
[
"<script>",
` window.${ftlValuesGlobalName}= ${objectKeys(replaceValueBySearchValue)[0]};`,
"</script>",
"",
objectKeys(replaceValueBySearchValue)[1]
].join("\n")
);
const partiallyFixedIndexHtmlCode = $.html();
function generateFtlFilesCode(params: { pageId: string }): {
ftlCode: string;
} {
const { pageId } = params;
const $ = cheerio.load(partiallyFixedIndexHtmlCode);
let ftlCode = $.html();
Object.entries({
...replaceValueBySearchValue,
//If updated, don't forget to change in the ftl script as well.
"PAGE_ID_xIgLsPgGId9D8e": pageId
}).map(([searchValue, replaceValue]) => (ftlCode = ftlCode.replace(searchValue, replaceValue)));
return { ftlCode };
}
return { generateFtlFilesCode };
}

View File

@ -1,37 +1,39 @@
import * as url from "url";
import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
groupId: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateJavaStackFiles(params: {
version: string;
themeName: string;
homepage?: string;
keycloakThemeBuildingDirPath: string;
doBundleEmailTemplate: boolean;
doBundlesEmailTemplate: boolean;
buildOptions: BuildOptionsLike;
}): {
jarFilePath: string;
} {
const { themeName, version, homepage, keycloakThemeBuildingDirPath, doBundleEmailTemplate } = params;
const {
version,
buildOptions: { groupId, themeName },
keycloakThemeBuildingDirPath,
doBundlesEmailTemplate
} = params;
{
const { pomFileCode } = (function generatePomFileCode(): {
pomFileCode: string;
} {
const groupId = (() => {
const fallbackGroupId = `there.was.no.homepage.field.in.the.package.json.${themeName}`;
return (
(!homepage
? fallbackGroupId
: url
.parse(homepage)
.host?.replace(/:[0-9]+$/, "")
?.split(".")
.reverse()
.join(".") ?? fallbackGroupId) + ".keycloak"
);
})();
const artefactId = `${themeName}-keycloak-theme`;
const pomFileCode = [
@ -45,7 +47,7 @@ export function generateJavaStackFiles(params: {
` <version>${version}</version>`,
` <name>${artefactId}</name>`,
` <description />`,
`</project>`,
`</project>`
].join("\n");
return { pomFileCode };
@ -69,19 +71,19 @@ export function generateJavaStackFiles(params: {
"themes": [
{
"name": themeName,
"types": ["login", ...(doBundleEmailTemplate ? ["email"] : [])],
},
],
"types": ["login", ...(doBundlesEmailTemplate ? ["email"] : [])]
}
]
},
null,
2,
2
),
"utf8",
),
"utf8"
)
);
}
return {
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`),
"jarFilePath": pathJoin(keycloakThemeBuildingDirPath, "target", `${themeName}-${version}.jar`)
};
}

View File

@ -1,116 +1,150 @@
import { transformCodebase } from "../tools/transformCodebase";
import * as fs from "fs";
import { join as pathJoin, basename as pathBasename } from "path";
import { replaceImportsInCssCode, replaceImportsFromStaticInJsCode } from "./replaceImportFromStatic";
import { replaceImportsFromStaticInJsCode } from "./replacers/replaceImportsFromStaticInJsCode";
import { replaceImportsInCssCode } from "./replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory, pageIds } from "./generateFtl";
import { downloadBuiltinKeycloakTheme } from "../download-builtin-keycloak-theme";
import * as child_process from "child_process";
import { mockTestingResourcesCommonPath, mockTestingResourcesPath, mockTestingSubDirOfPublicDirBasename } from "../mockTestingResourcesPath";
import { isInside } from "../tools/isInside";
import type { BuildOptions } from "./BuildOptions";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import { getLogger } from "../tools/logger";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
themeName: string;
extraPages?: string[];
extraThemeProperties?: string[];
isSilent: boolean;
};
export type Standalone = Common & {
isStandalone: true;
urlPathname: string | undefined;
};
export type ExternalAssets = ExternalAssets.SameDomain | ExternalAssets.DifferentDomains;
export namespace ExternalAssets {
export type CommonExternalAssets = Common & {
isStandalone: false;
};
export type SameDomain = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: true;
};
export type DifferentDomains = CommonExternalAssets & {
areAppAndKeycloakServerSharingSameDomain: false;
urlOrigin: string;
urlPathname: string | undefined;
};
}
}
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function generateKeycloakThemeResources(params: {
themeName: string;
reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string;
keycloakThemeEmailDirPath: string;
urlPathname: string;
//If urlOrigin is not undefined then it means --externals-assets
urlOrigin: undefined | string;
extraPagesId: string[];
extraThemeProperties: string[];
keycloakVersion: string;
}): { doBundleEmailTemplate: boolean } {
const {
themeName,
reactAppBuildDirPath,
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
urlPathname,
urlOrigin,
extraPagesId,
extraThemeProperties,
keycloakVersion,
} = params;
buildOptions: BuildOptionsLike;
}): { doBundlesEmailTemplate: boolean } {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, keycloakThemeEmailDirPath, keycloakVersion, buildOptions } = params;
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", themeName, "login");
const logger = getLogger({ isSilent: buildOptions.isSilent });
const themeDirPath = pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, "login");
let allCssGlobalsToDefine: Record<string, string> = {};
transformCodebase({
"destDirPath": urlOrigin === undefined ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"destDirPath": buildOptions.isStandalone ? pathJoin(themeDirPath, "resources", "build") : reactAppBuildDirPath,
"srcDirPath": reactAppBuildDirPath,
"transformSourceCode": ({ filePath, sourceCode }) => {
//NOTE: Prevent cycles, excludes the folder we generated for debug in public/
if (
urlOrigin === undefined &&
buildOptions.isStandalone &&
isInside({
"dirPath": pathJoin(reactAppBuildDirPath, mockTestingSubDirOfPublicDirBasename),
filePath,
filePath
})
) {
return undefined;
}
if (urlOrigin === undefined && /\.css?$/i.test(filePath)) {
if (/\.css?$/i.test(filePath)) {
if (!buildOptions.isStandalone) {
return undefined;
}
const { cssGlobalsToDefine, fixedCssCode } = replaceImportsInCssCode({
"cssCode": sourceCode.toString("utf8"),
"cssCode": sourceCode.toString("utf8")
});
allCssGlobalsToDefine = {
...allCssGlobalsToDefine,
...cssGlobalsToDefine,
...cssGlobalsToDefine
};
return {
"modifiedSourceCode": Buffer.from(fixedCssCode, "utf8"),
};
return { "modifiedSourceCode": Buffer.from(fixedCssCode, "utf8") };
}
if (/\.js?$/i.test(filePath)) {
if (!buildOptions.isStandalone && buildOptions.areAppAndKeycloakServerSharingSameDomain) {
return undefined;
}
const { fixedJsCode } = replaceImportsFromStaticInJsCode({
"jsCode": sourceCode.toString("utf8"),
urlOrigin,
buildOptions
});
return {
"modifiedSourceCode": Buffer.from(fixedJsCode, "utf8"),
};
return { "modifiedSourceCode": Buffer.from(fixedJsCode, "utf8") };
}
return urlOrigin === undefined ? { "modifiedSourceCode": sourceCode } : undefined;
},
return buildOptions.isStandalone ? { "modifiedSourceCode": sourceCode } : undefined;
}
});
let doBundleEmailTemplate: boolean;
let doBundlesEmailTemplate: boolean;
email: {
if (!fs.existsSync(keycloakThemeEmailDirPath)) {
console.log(
logger.log(
[
`Not bundling email template because ${pathBasename(keycloakThemeEmailDirPath)} does not exist`,
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`,
].join("\n"),
`To start customizing the email template, run: 👉 npx create-keycloak-email-directory 👈`
].join("\n")
);
doBundleEmailTemplate = false;
doBundlesEmailTemplate = false;
break email;
}
doBundleEmailTemplate = true;
doBundlesEmailTemplate = true;
transformCodebase({
"srcDirPath": keycloakThemeEmailDirPath,
"destDirPath": pathJoin(themeDirPath, "..", "email"),
"destDirPath": pathJoin(themeDirPath, "..", "email")
});
}
const { generateFtlFilesCode } = generateFtlFilesCodeFactory({
"cssGlobalsToDefine": allCssGlobalsToDefine,
"indexHtmlCode": fs.readFileSync(pathJoin(reactAppBuildDirPath, "index.html")).toString("utf8"),
urlPathname,
urlOrigin,
"cssGlobalsToDefine": allCssGlobalsToDefine,
"buildOptions": buildOptions
});
[...pageIds, ...extraPagesId].forEach(pageId => {
[...pageIds, ...(buildOptions.extraPages ?? [])].forEach(pageId => {
const { ftlCode } = generateFtlFilesCode({ pageId });
fs.mkdirSync(themeDirPath, { "recursive": true });
@ -124,25 +158,26 @@ export function generateKeycloakThemeResources(params: {
downloadBuiltinKeycloakTheme({
keycloakVersion,
"destDirPath": tmpDirPath,
isSilent: buildOptions.isSilent
});
const themeResourcesDirPath = pathJoin(themeDirPath, "resources");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "login", "resources"),
"destDirPath": themeResourcesDirPath,
"destDirPath": themeResourcesDirPath
});
const reactAppPublicDirPath = pathJoin(reactAppBuildDirPath, "..", "public");
transformCodebase({
"srcDirPath": pathJoin(tmpDirPath, "keycloak", "common", "resources"),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath)),
"destDirPath": pathJoin(themeResourcesDirPath, pathBasename(mockTestingResourcesCommonPath))
});
transformCodebase({
"srcDirPath": themeResourcesDirPath,
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath),
"destDirPath": pathJoin(reactAppPublicDirPath, mockTestingResourcesPath)
});
const keycloakResourcesWithinPublicDirPath = pathJoin(reactAppPublicDirPath, mockTestingSubDirOfPublicDirBasename);
@ -150,8 +185,8 @@ export function generateKeycloakThemeResources(params: {
fs.writeFileSync(
pathJoin(keycloakResourcesWithinPublicDirPath, "README.txt"),
Buffer.from(
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" "),
),
["This is just a test folder that helps develop", "the login and register page without having to run a Keycloak container"].join(" ")
)
);
fs.writeFileSync(pathJoin(keycloakResourcesWithinPublicDirPath, ".gitignore"), Buffer.from("*", "utf8"));
@ -161,8 +196,8 @@ export function generateKeycloakThemeResources(params: {
fs.writeFileSync(
pathJoin(themeDirPath, "theme.properties"),
Buffer.from("parent=keycloak".concat("\n\n", extraThemeProperties.join("\n\n")), "utf8"),
Buffer.from(["parent=keycloak", ...(buildOptions.extraThemeProperties ?? [])].join("\n\n"), "utf8")
);
return { doBundleEmailTemplate };
return { doBundlesEmailTemplate };
}

View File

@ -1,13 +1,34 @@
import * as fs from "fs";
import { join as pathJoin } from "path";
import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions";
export type BuildOptionsLike = {
themeName: string;
};
{
const buildOptions = Reflect<BuildOptions>();
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
generateStartKeycloakTestingContainer.basename = "start_keycloak_testing_container.sh";
const containerName = "keycloak-testing-container";
/** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: { keycloakVersion: string; themeName: string; keycloakThemeBuildingDirPath: string }) {
const { themeName, keycloakThemeBuildingDirPath, keycloakVersion } = params;
export function generateStartKeycloakTestingContainer(params: {
keycloakVersion: string;
keycloakThemeBuildingDirPath: string;
buildOptions: BuildOptionsLike;
}) {
const {
keycloakThemeBuildingDirPath,
keycloakVersion,
buildOptions: { themeName }
} = params;
fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
@ -31,14 +52,14 @@ export function generateStartKeycloakTestingContainer(params: { keycloakVersion:
"main",
"resources",
"theme",
themeName,
themeName
)}:/opt/keycloak/themes/${themeName}:rw \\`,
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev`,
"",
""
].join("\n"),
"utf8",
"utf8"
),
{ "mode": 0o755 },
{ "mode": 0o755 }
);
}

View File

@ -0,0 +1,8 @@
#!/usr/bin/env node
export * from "./keycloakify";
import { main } from "./keycloakify";
if (require.main === module) {
main();
}

View File

@ -3,92 +3,56 @@ import { generateJavaStackFiles } from "./generateJavaStackFiles";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import * as child_process from "child_process";
import { generateStartKeycloakTestingContainer } from "./generateStartKeycloakTestingContainer";
import { URL } from "url";
import * as fs from "fs";
type ParsedPackageJson = {
name: string;
version: string;
homepage?: string;
};
import { readBuildOptions } from "./BuildOptions";
import { getLogger } from "../tools/logger";
import { getCliOptions } from "../tools/cliOptions";
const reactProjectDirPath = process.cwd();
const doUseExternalAssets = process.argv[2]?.toLowerCase() === "--external-assets";
const parsedPackageJson: ParsedPackageJson = require(pathJoin(reactProjectDirPath, "package.json"));
export const keycloakThemeBuildingDirPath = pathJoin(reactProjectDirPath, "build_keycloak");
export const keycloakThemeEmailDirPath = pathJoin(keycloakThemeBuildingDirPath, "..", "keycloak_email");
function sanitizeThemeName(name: string) {
return name
.replace(/^@(.*)/, "$1")
.split("/")
.join("-");
}
export function main() {
console.log("🔏 Building the keycloak theme...⌚");
const { isSilent, hasExternalAssets } = getCliOptions(process.argv.slice(2));
const logger = getLogger({ isSilent });
logger.log("🔏 Building the keycloak theme...⌚");
const extraPagesId: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraPages"] ?? [];
const extraThemeProperties: string[] = (parsedPackageJson as any)["keycloakify"]?.["extraThemeProperties"] ?? [];
const themeName = sanitizeThemeName(parsedPackageJson.name);
const buildOptions = readBuildOptions({
"packageJson": fs.readFileSync(pathJoin(reactProjectDirPath, "package.json")).toString("utf8"),
"CNAME": (() => {
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
const { doBundleEmailTemplate } = generateKeycloakThemeResources({
if (!fs.existsSync(cnameFilePath)) {
return undefined;
}
return fs.readFileSync(cnameFilePath).toString("utf8");
})(),
"isExternalAssetsCliParamProvided": hasExternalAssets,
"isSilent": isSilent
});
const { doBundlesEmailTemplate } = generateKeycloakThemeResources({
keycloakThemeBuildingDirPath,
keycloakThemeEmailDirPath,
"reactAppBuildDirPath": pathJoin(reactProjectDirPath, "build"),
themeName,
...(() => {
const url = (() => {
const { homepage } = parsedPackageJson;
if (homepage !== undefined) {
return new URL(homepage);
}
const cnameFilePath = pathJoin(reactProjectDirPath, "public", "CNAME");
if (fs.existsSync(cnameFilePath)) {
return new URL(`https://${fs.readFileSync(cnameFilePath).toString("utf8").replace(/\s+$/, "")}`);
}
return undefined;
})();
return {
"urlPathname": url === undefined ? "/" : url.pathname.replace(/([^/])$/, "$1/"),
"urlOrigin": !doUseExternalAssets
? undefined
: (() => {
if (url === undefined) {
console.error("ERROR: You must specify 'homepage' in your package.json");
process.exit(-1);
}
return url.origin;
})(),
};
})(),
extraPagesId,
extraThemeProperties,
buildOptions,
//We have to leave it at that otherwise we break our default theme.
//Problem is that we can't guarantee that the the old resources
//will still be available on the newer keycloak version.
"keycloakVersion": "11.0.3",
"keycloakVersion": "11.0.3"
});
const { jarFilePath } = generateJavaStackFiles({
"version": parsedPackageJson.version,
themeName,
"homepage": parsedPackageJson.homepage,
"version": buildOptions.version,
keycloakThemeBuildingDirPath,
doBundleEmailTemplate,
doBundlesEmailTemplate,
buildOptions
});
child_process.execSync("mvn package", {
"cwd": keycloakThemeBuildingDirPath,
"cwd": keycloakThemeBuildingDirPath
});
//We want, however, to test in a container running the latest Keycloak version
@ -96,11 +60,11 @@ export function main() {
generateStartKeycloakTestingContainer({
keycloakThemeBuildingDirPath,
themeName,
"keycloakVersion": containerKeycloakVersion,
buildOptions
});
console.log(
logger.log(
[
"",
`✅ Your keycloak theme has been generated and bundled into ./${pathRelative(reactProjectDirPath, jarFilePath)} 🚀`,
@ -145,11 +109,11 @@ export function main() {
"- Log into the admin console 👉 http://localhost:8080/admin username: admin, password: admin 👈",
'- Create a realm named "myrealm"',
'- Create a client with ID: "myclient", "Root URL": "https://www.keycloak.org/app/" and "Valid redirect URIs": "https://www.keycloak.org/app/*"',
`- Select Login Theme: ${themeName} (don't forget to save at the bottom of the page)`,
`- Select Login Theme: ${buildOptions.themeName} (don't forget to save at the bottom of the page)`,
`- Go to 👉 https://www.keycloak.org/app/ 👈 Click "Save" then "Sign in". You should see your login page`,
"",
"Video demoing this process: https://youtu.be/N3wlBoH4hKg",
"",
].join("\n"),
""
].join("\n")
);
}

View File

@ -0,0 +1,86 @@
import { ftlValuesGlobalName } from "../ftlValuesGlobalName";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Standalone = {
isStandalone: true;
};
export type ExternalAssets = {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsFromStaticInJsCode(params: { jsCode: string; buildOptions: BuildOptionsLike }): { fixedJsCode: string } {
/*
NOTE:
When we have urlOrigin defined it means that
we are building with --external-assets
so we have to make sur that the fixed js code will run
inside and outside keycloak.
When urlOrigin isn't defined we can assume the fixedJsCode
will always run in keycloak context.
*/
const { jsCode, buildOptions } = params;
const getReplaceArgs = (language: "js" | "css"): Parameters<typeof String.prototype.replace> => [
new RegExp(`([a-zA-Z_]+)\\.([a-zA-Z]+)=function\\(([a-zA-Z]+)\\){return"static\\/${language}\\/"`, "g"),
(...[, n, u, e]) => `
${n}[(function(){
var pd= Object.getOwnPropertyDescriptor(${n}, "p");
if( pd === undefined || pd.configurable ){
${
buildOptions.isStandalone
? `
Object.defineProperty(${n}, "p", {
get: function() { return window.${ftlValuesGlobalName}.url.resourcesPath; },
set: function (){}
});
`
: `
var p= "";
Object.defineProperty(${n}, "p", {
get: function() { return "${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : p; },
set: function (value){ p = value;}
});
`
}
}
return "${u}";
})()] = function(${e}) { return "${buildOptions.isStandalone ? "/build/" : ""}static/${language}/"`
];
const fixedJsCode = jsCode
.replace(...getReplaceArgs("js"))
.replace(...getReplaceArgs("css"))
.replace(/([a-zA-Z]+\.[a-zA-Z]+)\+"static\//g, (...[, group]) =>
buildOptions.isStandalone
? `window.${ftlValuesGlobalName}.url.resourcesPath + "/build/static/`
: `("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group}) + "static/`
)
//TODO: Write a test case for this
.replace(/".chunk.css",([a-zA-Z])+=([a-zA-Z]+\.[a-zA-Z]+)\+([a-zA-Z]+),/, (...[, group1, group2, group3]) =>
buildOptions.isStandalone
? `".chunk.css",${group1} = window.${ftlValuesGlobalName}.url.resourcesPath + "/build/" + ${group3},`
: `".chunk.css",${group1} = ("${ftlValuesGlobalName}" in window ? "${buildOptions.urlOrigin}/" : ${group2}) + ${group3},`
);
return { fixedJsCode };
}

View File

@ -0,0 +1,64 @@
import * as crypto from "crypto";
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = {
urlPathname: string | undefined;
};
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInCssCode(params: { cssCode: string }): {
fixedCssCode: string;
cssGlobalsToDefine: Record<string, string>;
} {
const { cssCode } = params;
const cssGlobalsToDefine: Record<string, string> = {};
new Set(cssCode.match(/url\(["']?\/[^/][^)"']+["']?\)[^;}]*/g) ?? []).forEach(
match => (cssGlobalsToDefine["url" + crypto.createHash("sha256").update(match).digest("hex").substring(0, 15)] = match)
);
let fixedCssCode = cssCode;
Object.keys(cssGlobalsToDefine).forEach(
cssVariableName =>
//NOTE: split/join pattern ~ replace all
(fixedCssCode = fixedCssCode.split(cssGlobalsToDefine[cssVariableName]).join(`var(--${cssVariableName})`))
);
return { fixedCssCode, cssGlobalsToDefine };
}
export function generateCssCodeToDefineGlobals(params: { cssGlobalsToDefine: Record<string, string>; buildOptions: BuildOptionsLike }): {
cssCodeToPrependInHead: string;
} {
const { cssGlobalsToDefine, buildOptions } = params;
return {
"cssCodeToPrependInHead": [
":root {",
...Object.keys(cssGlobalsToDefine)
.map(cssVariableName =>
[
`--${cssVariableName}:`,
cssGlobalsToDefine[cssVariableName].replace(
new RegExp(`url\\(${(buildOptions.urlPathname ?? "/").replace(/\//g, "\\/")}`, "g"),
"url(${url.resourcesPath}/build/"
)
].join(" ")
)
.map(line => ` ${line};`),
"}"
].join("\n")
};
}

View File

@ -0,0 +1,47 @@
import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { Reflect } from "tsafe/Reflect";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
export namespace BuildOptionsLike {
export type Common = {
urlPathname: string | undefined;
};
export type Standalone = Common & {
isStandalone: true;
};
export type ExternalAssets = Common & {
isStandalone: false;
urlOrigin: string;
};
}
{
const buildOptions = Reflect<BuildOptions>();
assert(!is<BuildOptions.ExternalAssets.CommonExternalAssets>(buildOptions));
assert<typeof buildOptions extends BuildOptionsLike ? true : false>();
}
export function replaceImportsInInlineCssCode(params: { cssCode: string; buildOptions: BuildOptionsLike }): {
fixedCssCode: string;
} {
const { cssCode, buildOptions } = params;
const fixedCssCode = cssCode.replace(
buildOptions.urlPathname === undefined
? /url\(["']?\/([^/][^)"']+)["']?\)/g
: new RegExp(`url\\(["']?${buildOptions.urlPathname}([^)"']+)["']?\\)`, "g"),
(...[, group]) =>
`url(${
buildOptions.isStandalone ? "${url.resourcesPath}/build/" + group : buildOptions.urlOrigin + (buildOptions.urlPathname ?? "/") + group
})`
);
return { fixedCssCode };
}

View File

@ -1,5 +1,6 @@
import { execSync } from "child_process";
import { join as pathJoin, relative as pathRelative } from "path";
import { exclude } from "tsafe/exclude";
import * as fs from "fs";
const keycloakifyDirPath = pathJoin(__dirname, "..", "..");
@ -14,14 +15,14 @@ fs.writeFileSync(
return {
...packageJsonParsed,
"main": packageJsonParsed["main"].replace(/^dist\//, ""),
"types": packageJsonParsed["types"].replace(/^dist\//, ""),
"types": packageJsonParsed["types"].replace(/^dist\//, "")
};
})(),
null,
2,
2
),
"utf8",
),
"utf8"
)
);
const commonThirdPartyDeps = (() => {
@ -33,10 +34,10 @@ const commonThirdPartyDeps = (() => {
.map(namespaceModuleName =>
fs
.readdirSync(pathJoin(keycloakifyDirPath, "node_modules", namespaceModuleName))
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`),
.map(submoduleName => `${namespaceModuleName}/${submoduleName}`)
)
.reduce((prev, curr) => [...prev, ...curr], []),
...standaloneModuleNames,
...standaloneModuleNames
];
})();
@ -55,16 +56,37 @@ const execYarnLink = (params: { targetModuleName?: string; cwd: string }) => {
cwd,
"env": {
...process.env,
"HOME": yarnHomeDirPath,
},
"HOME": yarnHomeDirPath
}
});
};
const testAppNames = [process.argv[2] ?? "keycloakify-demo-app"] as const;
const testAppPaths = (() => {
const arg = process.argv[2];
const getTestAppPath = (testAppName: typeof testAppNames[number]) => pathJoin(keycloakifyDirPath, "..", testAppName);
const testAppNames = arg !== undefined ? [arg] : ["keycloakify-starter", "keycloakify-advanced-starter"];
testAppNames.forEach(testAppName => execSync("yarn install", { "cwd": getTestAppPath(testAppName) }));
return testAppNames
.map(testAppName => {
const testAppPath = pathJoin(keycloakifyDirPath, "..", testAppName);
if (fs.existsSync(testAppPath)) {
return testAppPath;
}
console.warn(`Skipping ${testAppName} since it cant be found here: ${testAppPath}`);
return undefined;
})
.filter(exclude(undefined));
})();
if (testAppPaths.length === 0) {
console.error("No test app to link into!");
process.exit(-1);
}
testAppPaths.forEach(testAppPath => execSync("yarn install", { "cwd": testAppPath }));
console.log("=== Linking common dependencies ===");
@ -77,26 +99,28 @@ commonThirdPartyDeps.forEach(commonThirdPartyDep => {
console.log(`${current}/${total} ${commonThirdPartyDep}`);
const localInstallPath = pathJoin(
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])],
...[keycloakifyDirPath, "node_modules", ...(commonThirdPartyDep.startsWith("@") ? commonThirdPartyDep.split("/") : [commonThirdPartyDep])]
);
execYarnLink({ "cwd": localInstallPath });
testAppNames.forEach(testAppName =>
execYarnLink({
"cwd": getTestAppPath(testAppName),
"targetModuleName": commonThirdPartyDep,
}),
);
});
commonThirdPartyDeps.forEach(commonThirdPartyDep =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": testAppPath,
"targetModuleName": commonThirdPartyDep
})
)
);
console.log("=== Linking in house dependencies ===");
execYarnLink({ "cwd": pathJoin(keycloakifyDirPath, "dist") });
testAppNames.forEach(testAppName =>
testAppPaths.forEach(testAppPath =>
execYarnLink({
"cwd": getTestAppPath(testAppName),
"targetModuleName": "keycloakify",
}),
"cwd": testAppPath,
"targetModuleName": "keycloakify"
})
);

View File

@ -24,9 +24,9 @@ export async function promptKeycloakVersion() {
"count": 10,
"doIgnoreBeta": true,
"owner": "keycloak",
"repo": "keycloak",
"repo": "keycloak"
}).then(arr => arr.map(({ tag }) => tag))),
"11.0.3",
"11.0.3"
];
if (process.env["GITHUB_ACTIONS"] === "true") {
@ -34,7 +34,7 @@ export async function promptKeycloakVersion() {
}
const { value: keycloakVersion } = await cliSelect<string>({
"values": tags,
"values": tags
}).catch(() => {
console.log("Aborting");

View File

@ -20,7 +20,7 @@ export namespace NpmModuleVersion {
...(() => {
const str = match[4];
return str === undefined ? {} : { "betaPreRelease": parseInt(str) };
})(),
})()
};
}

View File

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

View File

@ -2,31 +2,80 @@ import { basename as pathBasename, join as pathJoin } from "path";
import { execSync } from "child_process";
import * as fs from "fs";
import { transformCodebase } from "./transformCodebase";
import { rm_rf, rm, rm_r } from "./rm";
import { rm, rm_rf } from "./rm";
import * as crypto from "crypto";
/** assert url ends with .zip */
export function downloadAndUnzip(params: { url: string; destDirPath: string; pathOfDirToExtractInArchive?: string }) {
const { url, destDirPath, pathOfDirToExtractInArchive } = params;
export function downloadAndUnzip(params: {
isSilent: boolean;
url: string;
destDirPath: string;
pathOfDirToExtractInArchive?: string;
cacheDirPath: string;
}) {
const { url, destDirPath, pathOfDirToExtractInArchive, cacheDirPath } = params;
const tmpDirPath = pathJoin(destDirPath, "..", "tmp_xxKdOxnEdx");
const zipFilePath = pathBasename(url);
const extractDirPath = pathJoin(
cacheDirPath,
`_${crypto.createHash("sha256").update(JSON.stringify({ url, pathOfDirToExtractInArchive })).digest("hex").substring(0, 15)}`
);
rm_rf(tmpDirPath);
fs.mkdirSync(cacheDirPath, { "recursive": true });
fs.mkdirSync(tmpDirPath, { "recursive": true });
const { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath } = (() => {
const filePath = pathJoin(cacheDirPath, "isSuccessByExtractDirPath.json");
execSync(`curl -L ${url} -o ${zipFilePath}`, { "cwd": tmpDirPath });
type IsSuccessByExtractDirPath = Record<string, boolean | undefined>;
execSync(`unzip -o ${zipFilePath}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": tmpDirPath,
});
function readIsSuccessByExtractDirPath(): IsSuccessByExtractDirPath {
if (!fs.existsSync(filePath)) {
return {};
}
rm(pathBasename(url), { "cwd": tmpDirPath });
return JSON.parse(fs.readFileSync(filePath).toString("utf8"));
}
function writeIsSuccessByExtractDirPath(isSuccessByExtractDirPath: IsSuccessByExtractDirPath): void {
fs.writeFileSync(filePath, Buffer.from(JSON.stringify(isSuccessByExtractDirPath, null, 2), "utf8"));
}
return { readIsSuccessByExtractDirPath, writeIsSuccessByExtractDirPath };
})();
downloadAndUnzip: {
const isSuccessByExtractDirPath = readIsSuccessByExtractDirPath();
if (isSuccessByExtractDirPath[extractDirPath]) {
break downloadAndUnzip;
}
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: false
});
rm_rf(extractDirPath);
fs.mkdirSync(extractDirPath);
const zipFileBasename = pathBasename(url);
execSync(`curl -L ${url} -o ${zipFileBasename} ${params.isSilent ? "-s" : ""}`, { "cwd": extractDirPath });
execSync(`unzip -o ${zipFileBasename}${pathOfDirToExtractInArchive === undefined ? "" : ` "${pathOfDirToExtractInArchive}/**/*"`}`, {
"cwd": extractDirPath
});
rm(zipFileBasename, { "cwd": extractDirPath });
writeIsSuccessByExtractDirPath({
...isSuccessByExtractDirPath,
[extractDirPath]: true
});
}
transformCodebase({
"srcDirPath": pathOfDirToExtractInArchive === undefined ? tmpDirPath : pathJoin(tmpDirPath, pathOfDirToExtractInArchive),
destDirPath,
"srcDirPath": pathOfDirToExtractInArchive === undefined ? extractDirPath : pathJoin(extractDirPath, pathOfDirToExtractInArchive),
destDirPath
});
rm_r(tmpDirPath);
}

View File

@ -5,6 +5,6 @@ import * as fs from "fs";
Object.entries<string>(JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["bin"]).forEach(([, scriptPath]) =>
child_process.execSync(`chmod +x ${scriptPath}`, {
"cwd": getProjectRoot(),
}),
"cwd": getProjectRoot()
})
);

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

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

View File

@ -19,7 +19,7 @@ export function listTagsFactory(params: { octokit: Octokit }) {
owner,
repo,
per_page,
"page": page++,
"page": page++
});
for (const branch of resp.data.map(({ name }) => name)) {

View File

@ -10,7 +10,7 @@ export function rm(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": undefined,
"cwd": options?.cwd,
"cwd": options?.cwd
});
}
@ -18,7 +18,7 @@ export function rm_r(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": "r",
"cwd": options?.cwd,
"cwd": options?.cwd
});
}
@ -26,6 +26,6 @@ export function rm_rf(pathToRemove: string, options?: { cwd: string }) {
rmInternal({
pathToRemove,
"args": "rf",
"cwd": options?.cwd,
"cwd": options?.cwd
});
}

View File

@ -16,8 +16,8 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
srcDirPath,
destDirPath,
transformSourceCode = id<TransformSourceCode>(({ sourceCode }) => ({
"modifiedSourceCode": sourceCode,
})),
"modifiedSourceCode": sourceCode
}))
} = params;
for (const file_relative_path of crawl(srcDirPath)) {
@ -25,7 +25,7 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
const transformSourceCodeResult = transformSourceCode({
"sourceCode": fs.readFileSync(filePath),
"filePath": path.join(srcDirPath, file_relative_path),
"filePath": path.join(srcDirPath, file_relative_path)
});
if (transformSourceCodeResult === undefined) {
@ -33,14 +33,14 @@ export function transformCodebase(params: { srcDirPath: string; destDirPath: str
}
fs.mkdirSync(path.dirname(path.join(destDirPath, file_relative_path)), {
"recursive": true,
"recursive": true
});
const { newFileName, modifiedSourceCode } = transformSourceCodeResult;
fs.writeFileSync(
path.join(path.dirname(path.join(destDirPath, file_relative_path)), newFileName ?? path.basename(file_relative_path)),
modifiedSourceCode,
modifiedSourceCode
);
}
}

View File

@ -21,12 +21,12 @@ const LoginIdpLinkEmail = lazy(() => import("./LoginIdpLinkEmail"));
const LoginConfigTotp = lazy(() => import("./LoginConfigTotp"));
const LogoutConfirm = lazy(() => import("./LogoutConfirm"));
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...kcProps }: { kcContext: KcContextBase; i18n?: I18n } & KcProps) => {
const i18n = (function useClosure() {
const i18n = useI18n({
kcContext,
"extraMessages": {},
"doSkip": userProvidedI18n !== undefined,
"doSkip": userProvidedI18n !== undefined
});
return userProvidedI18n ?? i18n;
@ -36,42 +36,44 @@ const KcApp = memo(({ kcContext, i18n: userProvidedI18n, ...props }: { kcContext
return null;
}
const props = { i18n, ...kcProps };
return (
<Suspense>
{(() => {
switch (kcContext.pageId) {
case "login.ftl":
return <Login {...{ kcContext, i18n, ...props }} />;
return <Login {...{ kcContext, ...props }} />;
case "register.ftl":
return <Register {...{ kcContext, i18n, ...props }} />;
return <Register {...{ kcContext, ...props }} />;
case "register-user-profile.ftl":
return <RegisterUserProfile {...{ kcContext, i18n, ...props }} />;
return <RegisterUserProfile {...{ kcContext, ...props }} />;
case "info.ftl":
return <Info {...{ kcContext, i18n, ...props }} />;
return <Info {...{ kcContext, ...props }} />;
case "error.ftl":
return <Error {...{ kcContext, i18n, ...props }} />;
return <Error {...{ kcContext, ...props }} />;
case "login-reset-password.ftl":
return <LoginResetPassword {...{ kcContext, i18n, ...props }} />;
return <LoginResetPassword {...{ kcContext, ...props }} />;
case "login-verify-email.ftl":
return <LoginVerifyEmail {...{ kcContext, i18n, ...props }} />;
return <LoginVerifyEmail {...{ kcContext, ...props }} />;
case "terms.ftl":
return <Terms {...{ kcContext, i18n, ...props }} />;
return <Terms {...{ kcContext, ...props }} />;
case "login-otp.ftl":
return <LoginOtp {...{ kcContext, i18n, ...props }} />;
return <LoginOtp {...{ kcContext, ...props }} />;
case "login-update-password.ftl":
return <LoginUpdatePassword {...{ kcContext, i18n, ...props }} />;
return <LoginUpdatePassword {...{ kcContext, ...props }} />;
case "login-update-profile.ftl":
return <LoginUpdateProfile {...{ kcContext, i18n, ...props }} />;
return <LoginUpdateProfile {...{ kcContext, ...props }} />;
case "login-idp-link-confirm.ftl":
return <LoginIdpLinkConfirm {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkConfirm {...{ kcContext, ...props }} />;
case "login-idp-link-email.ftl":
return <LoginIdpLinkEmail {...{ kcContext, i18n, ...props }} />;
return <LoginIdpLinkEmail {...{ kcContext, ...props }} />;
case "login-page-expired.ftl":
return <LoginPageExpired {...{ kcContext, i18n, ...props }} />;
return <LoginPageExpired {...{ kcContext, ...props }} />;
case "login-config-totp.ftl":
return <LoginConfigTotp {...{ kcContext, i18n, ...props }} />;
return <LoginConfigTotp {...{ kcContext, ...props }} />;
case "logout-confirm.ftl":
return <LogoutConfirm {...{ kcContext, i18n, ...props }} />;
return <LogoutConfirm {...{ kcContext, ...props }} />;
}
})()}
</Suspense>

View File

@ -37,7 +37,7 @@ export const defaultKcTemplateProps = {
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"scripts": [],
@ -60,7 +60,7 @@ export const defaultKcTemplateProps = {
"kcFormGroupClass": ["form-group"],
"kcLabelWrapperClass": ["col-xs-12", "col-sm-12", "col-md-12", "col-lg-12"],
"kcSignUpClass": ["login-pf-signup"],
"kcInfoAreaWrapperClass": [],
"kcInfoAreaWrapperClass": []
} as const;
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
@ -192,7 +192,7 @@ export const defaultKcProps = {
"kcSelectOTPListItemClass": ["card-pf-body", "card-pf-top-element"],
"kcAuthenticatorOtpCircleClass": ["fa", "fa-mobile", "card-pf-icon-circle"],
"kcSelectOTPItemHeadingClass": ["card-pf-title", "text-center"],
"kcFormOptionsWrapperClass": [],
"kcFormOptionsWrapperClass": []
} as const;
assert<typeof defaultKcProps extends KcProps ? true : false>();

View File

@ -2,7 +2,7 @@ import React, { useState, memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { FormEventHandler } from "react";
import type { I18n } from "../i18n";
@ -74,7 +74,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
? { "disabled": true }
: {
"autoFocus": true,
"autoComplete": "off",
"autoComplete": "off"
})}
/>
</>
@ -106,7 +106,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
type="checkbox"
{...(login.rememberMe
? {
"checked": true,
"checked": true
}
: {})}
/>
@ -132,7 +132,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
name="credentialId"
{...(auth?.selectedCredential !== undefined
? {
"value": auth.selectedCredential,
"value": auth.selectedCredential
}
: {})}
/>
@ -142,7 +142,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
name="login"
id="kc-login"
@ -159,7 +159,7 @@ const Login = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Lo
<ul
className={cx(
props.kcFormSocialAccountListClass,
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass,
social.providers.length > 4 && props.kcFormSocialAccountDoubleListClass
)}
>
{social.providers.map(p => (

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginConfigTotp; i18n: I18n } & KcProps) => {
@ -15,7 +15,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
const algToKeyUriAlg: Record<KcContextBase.LoginConfigTotp["totp"]["policy"]["algorithm"], string> = {
HmacSHA1: "SHA1",
HmacSHA256: "SHA256",
HmacSHA512: "SHA512",
HmacSHA512: "SHA512"
};
return (
@ -159,7 +159,7 @@ const LoginConfigTotp = memo(({ kcContext, i18n, ...props }: { kcContext: KcCont
props.kcButtonClass,
props.kcButtonDefaultClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
id="cancelTOTPBtn"
name="cancel-aia"

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginIdpLinkConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginIdpLinkConfirm; i18n: I18n } & KcProps) => {

View File

@ -4,7 +4,7 @@ import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginOtp; i18n: I18n } & KcProps) => {
@ -19,7 +19,7 @@ const LoginOtp = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
headInsert({
"type": "javascript",
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js"),
"src": pathJoin(kcContext.url.resourcesCommonPath, "node_modules/jquery/dist/jquery.min.js")
}).then(() => {
if (isCleanedUp) return;

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginResetPassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginResetPassword; i18n: I18n } & KcProps) => {

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdatePassword; i18n: I18n } & KcProps) => {
@ -102,7 +102,7 @@ const LoginUpdatePassword = memo(({ kcContext, i18n, ...props }: { kcContext: Kc
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.LoginUpdateProfile; i18n: I18n } & KcProps) => {
@ -105,7 +105,7 @@ const LoginUpdateProfile = memo(({ kcContext, i18n, ...props }: { kcContext: KcC
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
type="submit"
defaultValue={msgStr("doSubmit")}

View File

@ -1,5 +1,5 @@
import React, { memo } from "react";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
@ -35,7 +35,7 @@ const LogoutConfirm = memo(({ kcContext, i18n, ...props }: { kcContext: KcContex
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonBlockClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
name="confirmLogout"
id="kc-logout"

View File

@ -2,7 +2,7 @@ import React, { memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Register; i18n: I18n } & KcProps) => {
@ -111,7 +111,7 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase
<div
className={cx(
props.kcFormGroupClass,
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass),
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass)
)}
>
<div className={cx(props.kcLabelWrapperClass)}>

View File

@ -2,7 +2,7 @@ import React, { useMemo, memo, useEffect, useState, Fragment } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase, Attribute } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { ReactComponent } from "../tools/ReactComponent";
import { useCallbackFactory } from "powerhooks/useCallbackFactory";
import { useFormValidationSlice } from "../useFormValidationSlice";
@ -18,9 +18,9 @@ const RegisterUserProfile = memo(({ kcContext, i18n, ...props_ }: { kcContext: K
const props = useMemo(
() => ({
...props_,
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 })),
"kcFormGroupClass": cx(props_.kcFormGroupClass, css({ "marginBottom": 20 }))
}),
[cx, css],
[cx, css]
);
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
@ -79,10 +79,10 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
formValidationReducer,
attributesWithPassword,
attributesWithPassword
} = useFormValidationSlice({
kcContext,
i18n,
i18n
});
useEffect(() => {
@ -94,22 +94,22 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
[name]: [string],
[
{
target: { value },
},
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>],
target: { value }
}
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>]
) =>
formValidationReducer({
"action": "update value",
name,
"newValue": value,
}),
"newValue": value
})
);
const onBlurFactory = useCallbackFactory(([name]: [string]) =>
formValidationReducer({
"action": "focus lost",
name,
}),
name
})
);
let currentGroup = "";
@ -200,8 +200,8 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
props.kcInputErrorMessageClass,
css({
"position": displayableErrors.length === 1 ? "absolute" : undefined,
"& > span": { "display": "block" },
}),
"& > span": { "display": "block" }
})
)}
aria-live="polite"
>

View File

@ -7,7 +7,7 @@ import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../../bin/tools/pathJoin";
import { useConstCallback } from "powerhooks/useConstCallback";
import type { KcTemplateProps } from "./KcProps";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import type { I18n } from "../i18n";
export type TemplateProps = {
@ -39,15 +39,11 @@ const Template = memo((props: TemplateProps) => {
infoNode = null,
kcContext,
i18n,
doFetchDefaultThemeResources,
doFetchDefaultThemeResources
} = props;
const { cx } = useCssAndCx();
useEffect(() => {
console.log("Rendering this page with react using keycloakify");
}, []);
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const onChangeLanguageClickFactory = useCallbackFactory(([kcLanguageTag]: [string]) => changeLocale(kcLanguageTag));
@ -72,16 +68,16 @@ const Template = memo((props: TemplateProps) => {
Promise.all(
[
...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath)),
...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
"type": "css",
href,
"position": "prepend",
}),
),
"position": "prepend"
})
)
).then(() => {
if (isUnmounted) {
return;
@ -93,8 +89,8 @@ const Template = memo((props: TemplateProps) => {
toArr(props.scripts).forEach(relativePath =>
headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath),
}),
"src": pathJoin(url.resourcesPath, relativePath)
})
);
if (props.kcHtmlClass !== undefined) {
@ -215,7 +211,7 @@ const Template = memo((props: TemplateProps) => {
<span
className="kc-feedback-text"
dangerouslySetInnerHTML={{
"__html": message.summary,
"__html": message.summary
}}
/>
</div>

View File

@ -2,7 +2,7 @@ import React, { useEffect, memo } from "react";
import Template from "./Template";
import type { KcProps } from "./KcProps";
import type { KcContextBase } from "../getKcContext/KcContextBase";
import { useCssAndCx } from "tss-react";
import { useCssAndCx } from "../tools/useCssAndCx";
import { Evt } from "evt";
import { useRerenderOnStateChange } from "evt/hooks";
import { assert } from "tsafe/assert";
@ -15,6 +15,7 @@ import { useConstCallback } from "powerhooks/useConstCallback";
export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
export type KcContextLike = {
pageId: KcContextBase["pageId"];
locale?: {
currentLanguageTag: string;
};
@ -35,15 +36,19 @@ export function useDownloadTerms(params: {
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
const downloadTermMarkdownMemoized = useConst(() =>
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }), { "promise": true }),
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }), { "promise": true })
);
return { downloadTermMarkdownMemoized };
})();
useEffect(() => {
if (kcContext.pageId !== "terms.ftl") {
return;
}
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
thermMarkdown => (evtTermMarkdown.state = thermMarkdown),
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
);
}, []);
}
@ -77,7 +82,7 @@ const Terms = memo(({ kcContext, i18n, ...props }: { kcContext: KcContextBase.Te
props.kcButtonClass,
props.kcButtonClass,
props.kcButtonPrimaryClass,
props.kcButtonLargeClass,
props.kcButtonLargeClass
)}
name="accept"
id="kc-accept"

View File

@ -1,4 +1,4 @@
import type { PageId } from "../../bin/build-keycloak-theme/generateFtl";
import type { PageId } from "../../bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKeyBase } from "../i18n";

View File

@ -29,8 +29,8 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`,
].join("\n"),
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
@ -38,13 +38,13 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
deepAssign({
"target": kcContext,
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock },
"source": kcContextDefaultMock !== undefined ? kcContextDefaultMock : { "pageId": mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
"target": kcContext,
"source": partialKcContextCustomMock,
"source": partialKcContextCustomMock
});
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
@ -56,17 +56,19 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName = {};
const partialAttributes = [
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? []),
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
console.log("====>", attribute);
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute,
"source": attribute
});
if (partialAttribute !== undefined) {
@ -74,7 +76,7 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute,
"source": partialAttribute
});
}
@ -82,14 +84,16 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes.forEach(partialAttribute => {
const { name } = partialAttribute;
partialAttributes
.map(partialAttribute => ({ "validators": {}, ...partialAttribute }))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
}
}

View File

@ -1,6 +1,6 @@
import type { KcContextBase } from "./KcContextBase";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
import { ftlValuesGlobalName } from "../../bin/keycloakify/ftlValuesGlobalName";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
? KcContextBase

View File

@ -13,20 +13,20 @@ export const kcContextCommonMock: KcContextBase.Common = {
"resourcesPath": pathJoin(PUBLIC_URL, mockTestingResourcesPath),
"resourcesCommonPath": pathJoin(PUBLIC_URL, mockTestingResourcesCommonPath),
"loginRestartFlowUrl": "/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
"loginUrl": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg"
},
"realm": {
"name": "myrealm",
"displayName": "myrealm",
"displayNameHtml": "myrealm",
"internationalizationEnabled": true,
"registrationEmailAsUsername": false,
"registrationEmailAsUsername": false
},
"messagesPerField": {
"printIfExists": (...[, x]) => x,
"existsError": () => true,
"get": key => `Fake error for ${key}`,
"exists": () => true,
"exists": () => true
},
"locale": {
"supported": [
@ -34,117 +34,117 @@ export const kcContextCommonMock: KcContextBase.Common = {
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
"label": "Deutsch",
"languageTag": "de",
"languageTag": "de"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
"label": "Norsk",
"languageTag": "no",
"languageTag": "no"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
"label": "Русский",
"languageTag": "ru",
"languageTag": "ru"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
"label": "Svenska",
"languageTag": "sv",
"languageTag": "sv"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
"label": "Português (Brasil)",
"languageTag": "pt-BR",
"languageTag": "pt-BR"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
"label": "Lietuvių",
"languageTag": "lt",
"languageTag": "lt"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
"label": "English",
"languageTag": "en",
"languageTag": "en"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
"label": "Italiano",
"languageTag": "it",
"languageTag": "it"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
"label": "Français",
"languageTag": "fr",
"languageTag": "fr"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
"label": "中文简体",
"languageTag": "zh-CN",
"languageTag": "zh-CN"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
"label": "Español",
"languageTag": "es",
"languageTag": "es"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
"label": "Čeština",
"languageTag": "cs",
"languageTag": "cs"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
"label": "日本語",
"languageTag": "ja",
"languageTag": "ja"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
"label": "Slovenčina",
"languageTag": "sk",
"languageTag": "sk"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
"label": "Polski",
"languageTag": "pl",
"languageTag": "pl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
"label": "Català",
"languageTag": "ca",
"languageTag": "ca"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
"label": "Nederlands",
"languageTag": "nl",
"languageTag": "nl"
},
{
"url": "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
"label": "Türkçe",
"languageTag": "tr",
},
"languageTag": "tr"
}
/* spell-checker: enable */
],
"currentLanguageTag": "en",
"currentLanguageTag": "en"
},
"auth": {
"showUsername": false,
"showResetCredentials": false,
"showTryAnotherWayLink": false,
"showTryAnotherWayLink": false
},
"client": {
"clientId": "myApp",
"clientId": "myApp"
},
"scripts": [],
"message": {
"type": "success",
"summary": "This is a test message",
"summary": "This is a test message"
},
"isAppInitiatedAction": false,
"isAppInitiatedAction": false
};
const loginUrl = {
...kcContextCommonMock.url,
"loginResetCredentialsUrl": "/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
"registrationUrl": "/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg"
};
export const kcContextMocks: KcContextBase[] = [
@ -158,17 +158,17 @@ export const kcContextMocks: KcContextBase[] = [
"rememberMe": true,
"password": true,
"resetPasswordAllowed": true,
"registrationAllowed": true,
"registrationAllowed": true
},
"auth": kcContextCommonMock.auth!,
"social": {
"displayInfo": true,
"displayInfo": true
},
"usernameEditDisabled": false,
"login": {
"rememberMe": false,
"rememberMe": false
},
"registrationDisabled": false,
"registrationDisabled": false
}),
...(() => {
const registerCommon: KcContextBase.RegisterCommon = {
@ -176,15 +176,15 @@ export const kcContextMocks: KcContextBase[] = [
"url": {
...loginUrl,
"registrationAction":
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0",
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
},
"scripts": [],
"isAppInitiatedAction": false,
"passwordRequired": true,
"recaptchaRequired": false,
"social": {
"displayInfo": true,
},
"displayInfo": true
}
};
return [
@ -192,8 +192,8 @@ export const kcContextMocks: KcContextBase[] = [
"pageId": "register.ftl",
...registerCommon,
"register": {
"formData": {},
},
"formData": {}
}
}),
id<KcContextBase.RegisterUserProfile>({
"pageId": "register-user-profile.ftl",
@ -205,16 +205,16 @@ export const kcContextMocks: KcContextBase[] = [
{
"validators": {
"username-prohibited-characters": {
"ignore.empty.value": true,
"ignore.empty.value": true
},
"up-username-has-value": {},
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "255",
"max": "255"
},
"up-duplicate-username": {},
"up-username-mutation": {},
"up-username-mutation": {}
},
"displayName": "${username}",
"annotations": {},
@ -223,27 +223,27 @@ export const kcContextMocks: KcContextBase[] = [
"autocomplete": "username",
"readOnly": false,
"name": "username",
"value": "xxxx",
"value": "xxxx"
},
{
"validators": {
"up-email-exists-as-username": {},
"length": {
"max": "255",
"ignore.empty.value": true,
"ignore.empty.value": true
},
"up-blank-attribute-value": {
"error-message": "missingEmailMessage",
"fail-on-null": false,
"fail-on-null": false
},
"up-duplicate-email": {},
"email": {
"ignore.empty.value": true,
"ignore.empty.value": true
},
"pattern": {
"ignore.empty.value": true,
"pattern": "gmail\\.com$",
},
"pattern": "gmail\\.com$"
}
},
"displayName": "${email}",
"annotations": {},
@ -251,55 +251,55 @@ export const kcContextMocks: KcContextBase[] = [
"groupAnnotations": {},
"autocomplete": "email",
"readOnly": false,
"name": "email",
"name": "email"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true,
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true,
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${firstName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "firstName",
"name": "firstName"
},
{
"validators": {
"length": {
"max": "255",
"ignore.empty.value": true,
"ignore.empty.value": true
},
"person-name-prohibited-characters": {
"ignore.empty.value": true,
"ignore.empty.value": true
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {},
"up-attribute-required-by-metadata-value": {}
},
"displayName": "${lastName}",
"annotations": {},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "lastName",
},
"name": "lastName"
}
];
return {
attributes,
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any,
"attributesByName": Object.fromEntries(attributes.map(attribute => [attribute.name, attribute])) as any
} as any;
})(),
},
}),
})()
}
})
];
})(),
id<KcContextBase.Info>({
@ -311,39 +311,39 @@ export const kcContextMocks: KcContextBase[] = [
"actionUri": "#",
"client": {
"clientId": "myApp",
"baseUrl": "#",
},
"baseUrl": "#"
}
}),
id<KcContextBase.Error>({
...kcContextCommonMock,
"pageId": "error.ftl",
"client": {
"clientId": "myApp",
"baseUrl": "#",
"baseUrl": "#"
},
"message": {
"type": "error",
"summary": "This is the error message",
},
"summary": "This is the error message"
}
}),
id<KcContextBase.LoginResetPassword>({
...kcContextCommonMock,
"pageId": "login-reset-password.ftl",
"realm": {
...kcContextCommonMock.realm,
"loginWithEmailAllowed": false,
},
"loginWithEmailAllowed": false
}
}),
id<KcContextBase.LoginVerifyEmail>({
...kcContextCommonMock,
"pageId": "login-verify-email.ftl",
"user": {
"email": "john.doe@gmail.com",
},
"email": "john.doe@gmail.com"
}
}),
id<KcContextBase.Terms>({
...kcContextCommonMock,
"pageId": "terms.ftl",
"pageId": "terms.ftl"
}),
id<KcContextBase.LoginOtp>({
...kcContextCommonMock,
@ -352,19 +352,19 @@ export const kcContextMocks: KcContextBase[] = [
"userOtpCredentials": [
{
"id": "id1",
"userLabel": "label1",
"userLabel": "label1"
},
{
"id": "id2",
"userLabel": "label2",
},
],
},
"userLabel": "label2"
}
]
}
}),
id<KcContextBase.LoginUpdatePassword>({
...kcContextCommonMock,
"pageId": "login-update-password.ftl",
"username": "anUsername",
"username": "anUsername"
}),
id<KcContextBase.LoginUpdateProfile>({
...kcContextCommonMock,
@ -374,21 +374,21 @@ export const kcContextMocks: KcContextBase[] = [
"username": "anUsername",
"email": "foo@example.com",
"firstName": "aFirstName",
"lastName": "aLastName",
},
"lastName": "aLastName"
}
}),
id<KcContextBase.LoginIdpLinkConfirm>({
...kcContextCommonMock,
"pageId": "login-idp-link-confirm.ftl",
"idpAlias": "FranceConnect",
"idpAlias": "FranceConnect"
}),
id<KcContextBase.LoginIdpLinkEmail>({
...kcContextCommonMock,
"pageId": "login-idp-link-email.ftl",
"idpAlias": "FranceConnect",
"brokerContext": {
"username": "anUsername",
},
"username": "anUsername"
}
}),
id<KcContextBase.LoginConfigTotp>({
...kcContextCommonMock,
@ -407,21 +407,21 @@ export const kcContextMocks: KcContextBase[] = [
digits: 6,
lookAheadWindow: 1,
type: "totp",
period: 30,
},
},
period: 30
}
}
}),
id<KcContextBase.LogoutConfirm>({
...kcContextCommonMock,
"pageId": "logout-confirm.ftl",
"url": {
...kcContextCommonMock.url,
"logoutConfirmAction": "Continuer?",
"logoutConfirmAction": "Continuer?"
},
"client": {
"clientId": "myApp",
"baseUrl": "#",
"baseUrl": "#"
},
"logoutConfirm": { "code": "123", skipLink: false },
}),
"logoutConfirm": { "code": "123", skipLink: false }
})
];

View File

@ -130,7 +130,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes.",
"invalidPasswordHistoryMessage": "Contrasenya incorrecta: no pot ser igual a cap de les últimes {0} contrasenyes."
};
export default messages;

View File

@ -149,7 +149,7 @@ const messages = {
"invalidPasswordRegexPatternMessage": "Neplatné heslo: neshoduje se zadaným regulárním výrazem.",
"invalidPasswordHistoryMessage": "Neplatné heslo: Nesmí se opakovat žádné z posledních {0} hesel.",
"invalidPasswordBlacklistedMessage": "Neplatné heslo: heslo je na černé listině.",
"invalidPasswordGenericMessage": "Neplatné heslo: nové heslo neodpovídá pravidlům hesla.",
"invalidPasswordGenericMessage": "Neplatné heslo: nové heslo neodpovídá pravidlům hesla."
};
export default messages;

View File

@ -149,7 +149,7 @@ const messages = {
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: Es entspricht nicht dem Regex-Muster.",
"invalidPasswordHistoryMessage": "Ungültiges Passwort: Es darf nicht einem der letzten {0} Passwörter entsprechen.",
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Das Passwort steht auf der Blocklist (schwarzen Liste).",
"invalidPasswordGenericMessge": "Ungültiges Passwort: Das neue Passwort verletzt die Passwort-Richtlinien.",
"invalidPasswordGenericMessge": "Ungültiges Passwort: Das neue Passwort verletzt die Passwort-Richtlinien."
};
export default messages;

View File

@ -318,7 +318,7 @@ const messages = {
"openshift.scope.user_info": "User information",
"openshift.scope.user_check-access": "User access information",
"openshift.scope.user_full": "Full Access",
"openshift.scope.list-projects": "List projects",
"openshift.scope.list-projects": "List projects"
};
export default messages;

View File

@ -130,7 +130,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas."
};
export default messages;

View File

@ -143,7 +143,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide: doit contenir au moins {0} caractère(s) spéciaux.",
"invalidPasswordNotUsernameMessage": "Mot de passe invalide: ne doit pas être identique au nom d'utilisateur.",
"invalidPasswordRegexPatternMessage": "Mot de passe invalide: ne valide pas l'expression rationnelle.",
"invalidPasswordHistoryMessage": "Mot de passe invalide: ne doit pas être égal aux {0} derniers mots de passe.",
"invalidPasswordHistoryMessage": "Mot de passe invalide: ne doit pas être égal aux {0} derniers mots de passe."
};
export default messages;

View File

@ -303,7 +303,7 @@ const messages = {
"openshift.scope.user_info": "Informazioni utente",
"openshift.scope.user_check-access": "Informazioni per l'accesso dell'utente",
"openshift.scope.user_full": "Accesso completo",
"openshift.scope.list-projects": "Elenca progetti",
"openshift.scope.list-projects": "Elenca progetti"
};
export default messages;

View File

@ -317,7 +317,7 @@ const messages = {
"openshift.scope.user_info": "ユーザー情報",
"openshift.scope.user_check-access": "ユーザーアクセス情報",
"openshift.scope.user_full": "フルアクセス",
"openshift.scope.list-projects": "プロジェクトの一覧表示",
"openshift.scope.list-projects": "プロジェクトの一覧表示"
};
export default messages;

View File

@ -136,7 +136,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Neteisingas slaptažodis: privaloma įvesti {0} specialų simbolį.",
"invalidPasswordNotUsernameMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su naudotojo vardu.",
"invalidPasswordRegexPatternMessage": "Neteisingas slaptažodis: slaptažodis netenkina regex taisyklės(ių).",
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais.",
"invalidPasswordHistoryMessage": "Neteisingas slaptažodis: slaptažodis negali sutapti su prieš tai buvusiais {0} slaptažodžiais."
};
export default messages;

View File

@ -136,7 +136,7 @@ const messages = {
"invalidPasswordNotUsernameMessage": "Ongeldig wachtwoord: het mag niet overeenkomen met de gebruikersnaam.",
"invalidPasswordRegexPatternMessage": "Ongeldig wachtwoord: het voldoet niet aan het door de beheerder ingestelde patroon.",
"invalidPasswordHistoryMessage": "Ongeldig wachtwoord: het mag niet overeen komen met een van de laatste {0} wachtwoorden.",
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid.",
"invalidPasswordGenericMessage": "Ongeldig wachtwoord: het nieuwe wachtwoord voldoet niet aan het wachtwoordbeleid."
};
export default messages;

View File

@ -146,7 +146,7 @@ const messages = {
"locale_nl": "Nederlands",
"locale_pt-BR": "Português (Brasil)",
"locale_ru": "Русский",
"locale_zh-CN": "中文简体",
"locale_zh-CN": "中文简体"
};
export default messages;

View File

@ -133,7 +133,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Senha inválida: deve conter pelo menos {0} caractere(s) especial",
"invalidPasswordNotUsernameMessage": "Senha inválida: não deve ser igual ao nome de usuário",
"invalidPasswordRegexPatternMessage": "Senha inválida: não corresponde ao padrão da expressão regular.",
"invalidPasswordHistoryMessage": "Senha inválida: não pode ser igual a qualquer uma das {0} últimas senhas.",
"invalidPasswordHistoryMessage": "Senha inválida: não pode ser igual a qualquer uma das {0} últimas senhas."
};
export default messages;

View File

@ -136,7 +136,7 @@ const messages = {
"invalidPasswordNotUsernameMessage": "Некорректный пароль: пароль не должен совпадать с именем пользователя.",
"invalidPasswordRegexPatternMessage": "Некорректный пароль: пароль не удовлетворяет регулярному выражению.",
"invalidPasswordHistoryMessage": "Некорректный пароль: пароль не должен совпадать с последним(и) {0} паролями.",
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля.",
"invalidPasswordGenericMessage": "Некорректный пароль: новый пароль не соответствует правилам пароля."
};
export default messages;

View File

@ -173,7 +173,7 @@ const messages = {
"resourcesSharedWithMe": "Zdroje zdieľané so mnou",
"permissionRequestion": "Žiadosti o povolenie",
"permission": "Oprávnenie",
"shares": "podiel (y)",
"shares": "podiel (y)"
};
export default messages;

View File

@ -132,7 +132,7 @@ const messages = {
"invalidPasswordNotUsernameMessage": "Ogiltigt lösenord: Får inte vara samma som användarnamnet.",
"invalidPasswordRegexPatternMessage": "Ogiltigt lösenord: matchar inte kravet för lösenordsmönster.",
"invalidPasswordHistoryMessage": "Ogiltigt lösenord: Får inte vara samma som de senaste {0} lösenorden.",
"invalidPasswordGenericMessage": "Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn.",
"invalidPasswordGenericMessage": "Ogiltigt lösenord: Det nya lösenordet stämmer inte med lösenordspolicyn."
};
export default messages;

View File

@ -302,7 +302,7 @@ const messages = {
"addTeam": "Kaynağınızı paylaşmak için ekip ekleyin",
"myPermissions": "İzinlerim",
"waitingforApproval": "Onay bekleniyor",
"anyPermission": "Herhangi bir izin",
"anyPermission": "Herhangi bir izin"
};
export default messages;

View File

@ -148,7 +148,7 @@ const messages = {
"locale_lt": "Lietuvių",
"locale_pt-BR": "Português (Brasil)",
"locale_ru": "Русский",
"locale_zh-CN": "中文简体",
"locale_zh-CN": "中文简体"
};
export default messages;

View File

@ -10,7 +10,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} caràcters especials.",
"invalidPasswordMinUpperCaseCharsMessage": "Contrasenya incorrecta: ha de contenir almenys {0} lletres majúscules.",
"invalidPasswordNotUsernameMessage": "Contrasenya incorrecta: no pot ser igual al nom d'usuari.",
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular.",
"invalidPasswordRegexPatternMessage": "Contrasenya incorrecta: no compleix l'expressió regular."
};
export default messages;

View File

@ -12,7 +12,7 @@ const messages = {
"invalidPasswordRegexPatternMessage": "Ungültiges Passwort: stimmt nicht mit Regex-Muster überein.",
"invalidPasswordHistoryMessage": "Ungültiges Passwort: darf nicht identisch mit einem der letzten {0} Passwörter sein.",
"invalidPasswordBlacklistedMessage": "Ungültiges Passwort: Passwort ist zu bekannt und auf der schwarzen Liste.",
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht.",
"invalidPasswordGenericMessage": "Ungültiges Passwort: neues Passwort erfüllt die Passwort-Anforderungen nicht."
};
export default messages;

View File

@ -37,7 +37,7 @@ const messages = {
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "Failed to get redirect URIs from the Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs does not match redirect URIs fetched from the Sector Identifier URI."
};
export default messages;

View File

@ -10,7 +10,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Contraseña incorrecta: debe contener al menos {0} caracteres especiales.",
"invalidPasswordNotUsernameMessage": "Contraseña incorrecta: no puede ser igual al nombre de usuario.",
"invalidPasswordRegexPatternMessage": "Contraseña incorrecta: no cumple la expresión regular.",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas.",
"invalidPasswordHistoryMessage": "Contraseña incorrecta: no puede ser igual a ninguna de las últimas {0} contraseñas."
};
export default messages;

View File

@ -10,7 +10,7 @@ const messages = {
"invalidPasswordMinSpecialCharsMessage": "Mot de passe invalide : doit contenir au moins {0} caractère(s) spéciaux.",
"invalidPasswordNotUsernameMessage": "Mot de passe invalide : ne doit pas être identique au nom d'utilisateur.",
"invalidPasswordRegexPatternMessage": "Mot de passe invalide : ne valide pas l'expression rationnelle.",
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe.",
"invalidPasswordHistoryMessage": "Mot de passe invalide : ne doit pas être égal aux {0} derniers mot de passe."
};
export default messages;

View File

@ -30,7 +30,7 @@ const messages = {
"設定されたセレクター識別子URIがない場合は、クライアントのリダイレクトURIは複数のホスト・コンポーネントを含むことはできません。",
"pairwiseMalformedSectorIdentifierURI": "不正なセレクター識別子URIです。",
"pairwiseFailedToGetRedirectURIs": "セクター識別子URIからリダイレクトURIを取得できませんでした。",
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。",
"pairwiseRedirectURIsMismatch": "クライアントのリダイレクトURIは、セクター識別子URIからフェッチされたリダイレクトURIと一致しません。"
};
export default messages;

View File

@ -24,7 +24,7 @@ const messages = {
"Kuomet nesukonfigūruotas sektoriaus identifikatoriaus URL, kliento nukreipimo nuorodos privalo talpinti ne daugiau kaip vieną skirtingą serverio vardo komponentą.",
"pairwiseMalformedSectorIdentifierURI": "Neteisinga sektoriaus identifikatoriaus URI.",
"pairwiseFailedToGetRedirectURIs": "Nepavyko gauti nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI.",
"pairwiseRedirectURIsMismatch": "Kliento nukreipimo nuoroda neatitinka nukreipimo nuorodų iš sektoriaus identifikatoriaus URI."
};
export default messages;

View File

@ -27,7 +27,7 @@ const messages = {
"Zonder een geconfigureerde Sector Identifier URI mogen client redirect URIs niet meerdere host componenten hebben.",
"pairwiseMalformedSectorIdentifierURI": "Onjuist notatie in Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "Kon geen redirect URIs verkrijgen van de Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI.",
"pairwiseRedirectURIsMismatch": "Client redirect URIs komen niet overeen met redict URIs ontvangen van de Sector Identifier URI."
};
export default messages;

View File

@ -15,7 +15,7 @@ const messages = {
"ldapErrorMissingClientId": "KlientID må være tilgjengelig i config når sikkerhetsdomenerollemapping ikke brukes.",
"ldapErrorCantPreserveGroupInheritanceWithUIDMembershipType": "Ikke mulig å bevare gruppearv og samtidig bruke UID medlemskapstype.",
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Kan ikke sette write-only når LDAP leverandør-modus ikke er WRITABLE",
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only",
"ldapErrorCantWriteOnlyAndReadOnly": "Kan ikke sette både write-only og read-only"
};
export default messages;

View File

@ -18,7 +18,7 @@ const messages = {
"ldapErrorCantWriteOnlyForReadOnlyLdap": "Não é possível definir modo de somente escrita quando o provedor LDAP não suporta escrita",
"ldapErrorCantWriteOnlyAndReadOnly": "Não é possível definir somente escrita e somente leitura ao mesmo tempo",
"clientRedirectURIsFragmentError": "URIs de redirecionamento não podem conter fragmentos",
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos",
"clientRootURLFragmentError": "URL raiz não pode conter fragmentos"
};
export default messages;

View File

@ -25,7 +25,7 @@ const messages = {
"Без конфигурации по части идентификатора URI, URI перенаправления клиента не может содержать несколько компонентов хоста.",
"pairwiseMalformedSectorIdentifierURI": "Искаженная часть идентификатора URI.",
"pairwiseFailedToGetRedirectURIs": "Не удалось получить идентификаторы URI перенаправления из части идентификатора URI.",
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI.",
"pairwiseRedirectURIsMismatch": "Клиент URI переадресации не соответствует URI переадресации, полученной из части идентификатора URI."
};
export default messages;

View File

@ -25,7 +25,7 @@ const messages = {
"Without a configured Sector Identifier URI, client redirect URIs must not contain multiple host components.",
"pairwiseMalformedSectorIdentifierURI": "Malformed Sector Identifier URI.",
"pairwiseFailedToGetRedirectURIs": "无法从服务器获得重定向URL",
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。",
"pairwiseRedirectURIsMismatch": "客户端的重定向URI与服务器端获取的URI配置不匹配。"
};
export default messages;

View File

@ -34,7 +34,7 @@ const messages = {
"eventUpdateTotpSubject": "Actualització de OTP",
"eventUpdateTotpBody": "OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.",
"eventUpdateTotpBodyHtml":
"<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>",
"<p>OTP s'ha actualitzat al teu compte el {0} des de {1}. Si no has estat tu, si us plau contacta amb l'administrador.</p>"
};
export default messages;

View File

@ -62,7 +62,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days.1": "den",
"linkExpirationFormatter.timePeriodUnit.days.2": "dny",
"linkExpirationFormatter.timePeriodUnit.days.3": "dny",
"linkExpirationFormatter.timePeriodUnit.days.4": "dny",
"linkExpirationFormatter.timePeriodUnit.days.4": "dny"
};
export default messages;

View File

@ -38,7 +38,7 @@ const messages = {
"<p>Ihr Passwort wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
"eventUpdateTotpSubject": "OTP Aktualisiert",
"eventUpdateTotpBody": "OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.",
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>",
"eventUpdateTotpBodyHtml": "<p>OTP wurde am {0} von {1} geändert. Falls das nicht Sie waren, dann kontaktieren Sie bitte Ihren Admin.</p>"
};
export default messages;

View File

@ -54,7 +54,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "days",
"linkExpirationFormatter.timePeriodUnit.days.1": "day",
"emailVerificationBodyCode": "Please verify your email address by entering in the following code.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>Please verify your email address by entering in the following code.</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -33,7 +33,7 @@ const messages = {
"eventUpdateTotpSubject": "Actualización de OTP",
"eventUpdateTotpBody": "OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.",
"eventUpdateTotpBodyHtml":
"<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>",
"<p>OTP se ha actualizado en tu cuenta el {0} desde {1}. Si no has sido tú, por favor contacta con el administrador.</p>"
};
export default messages;

View File

@ -37,7 +37,7 @@ const messages = {
"eventUpdateTotpBody":
"Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.",
"eventUpdateTotpBodyHtml":
"<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>",
"<p>Le OTP a été mis à jour pour votre compte {0} depuis {1}. Si vous n'étiez pas à l'origine de cette requête, veuillez contacter votre administrateur.</p>"
};
export default messages;

View File

@ -59,7 +59,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "giorni",
"linkExpirationFormatter.timePeriodUnit.days.1": "giorno",
"emailVerificationBodyCode": "Per favore verifica il tuo indirizzo email inserendo il codice seguente.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Per favore verifica il tuo indirizzo email inserendo il codice seguente.</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>Per favore verifica il tuo indirizzo email inserendo il codice seguente.</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -53,7 +53,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "日",
"linkExpirationFormatter.timePeriodUnit.days.1": "日",
"emailVerificationBodyCode": "次のコードを入力してメールアドレスを確認してください。\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>次のコードを入力してメールアドレスを確認してください。</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>次のコードを入力してメールアドレスを確認してください。</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -37,7 +37,7 @@ const messages = {
"eventUpdatePasswordBodyHtml": "<p>{1} paskyroje {0} pakeisas jūsų slaptažodis. Jei Jūs nekeitėte, tuomet susisiekite su administratoriumi</p>",
"eventUpdateTotpSubject": "OTP atnaujinimas",
"eventUpdateTotpBody": "OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi",
"eventUpdateTotpBodyHtml": "<p>OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>",
"eventUpdateTotpBodyHtml": "<p>OTP Jūsų {1} paskyroje su {0} buvo atnaujintas. Jei tai nebuvote Jūs, tuomet susisiekite su administratoriumi</p>"
};
export default messages;

View File

@ -47,7 +47,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.hours": "uur",
"linkExpirationFormatter.timePeriodUnit.hours.1": "uur",
"linkExpirationFormatter.timePeriodUnit.days": "dagen",
"linkExpirationFormatter.timePeriodUnit.days.1": "dag",
"linkExpirationFormatter.timePeriodUnit.days.1": "dag"
};
export default messages;

View File

@ -38,7 +38,7 @@ const messages = {
"eventUpdateTotpSubject": "Oppdater engangskode",
"eventUpdateTotpBody": "Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator.",
"eventUpdateTotpBodyHtml":
"<p>Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>",
"<p>Engangskode ble oppdatert for kontoen din på {0} fra {1}. Hvis dette ikke var deg, vennligst kontakt administrator. </p>"
};
export default messages;

View File

@ -65,7 +65,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "dni",
"linkExpirationFormatter.timePeriodUnit.days.1": "dzień",
"emailVerificationBodyCode": "Potwierdź swój adres e-mail wprowadzając następujący kod.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Potwierdź swój adres e-mail, wprowadzając następujący kod.</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>Potwierdź swój adres e-mail, wprowadzając następujący kod.</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -56,7 +56,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "dias",
"linkExpirationFormatter.timePeriodUnit.days.1": "dia",
"emailVerificationBodyCode": "Verifique seu endereço de e-mail digitando o seguinte código.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Verifique seu endereço de e-mail digitando o seguinte código.</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>Verifique seu endereço de e-mail digitando o seguinte código.</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -38,7 +38,7 @@ const messages = {
"eventUpdateTotpSubject": "Обновление OTP",
"eventUpdateTotpBody": "OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.",
"eventUpdateTotpBodyHtml":
"<p>OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>",
"<p>OTP был обновлен в вашей учетной записи {0} с {1}. Если это были не Вы, пожалуйста, свяжитесь с администратором.</p>"
};
export default messages;

View File

@ -51,7 +51,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.hours": "hodiny",
"linkExpirationFormatter.timePeriodUnit.hours.1": "hodina",
"linkExpirationFormatter.timePeriodUnit.days": "dni",
"linkExpirationFormatter.timePeriodUnit.days.1": "deň ",
"linkExpirationFormatter.timePeriodUnit.days.1": "deň "
};
export default messages;

View File

@ -39,7 +39,7 @@ const messages = {
"eventUpdateTotpSubject": "Uppdatera OTP",
"eventUpdateTotpBody": "OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.",
"eventUpdateTotpBodyHtml":
"<p>OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>",
"<p>OTP uppdaterades för ditt konto den {0} från {1}. Om det inte var du, vänligen kontakta i så fall en administratör.</p>"
};
export default messages;

View File

@ -55,7 +55,7 @@ const messages = {
"linkExpirationFormatter.timePeriodUnit.days": "gün",
"linkExpirationFormatter.timePeriodUnit.days.1": "gün",
"emailVerificationBodyCode": "Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.\n\n{0}\n\n.",
"emailVerificationBodyCodeHtml": "<p>Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.</p><p><b>{0}</b></p>",
"emailVerificationBodyCodeHtml": "<p>Lütfen aşağıdaki kodu girerek e-posta adresinizi doğrulayın.</p><p><b>{0}</b></p>"
};
export default messages;

View File

@ -34,7 +34,7 @@ const messages = {
"eventUpdatePasswordBodyHtml": "<p>您的密码在{0} 由 {1}更改. 如非本人操作,请联系管理员</p>",
"eventUpdateTotpSubject": "更新 OTP",
"eventUpdateTotpBody": "您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。",
"eventUpdateTotpBodyHtml": "<p>您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>",
"eventUpdateTotpBodyHtml": "<p>您账户的OTP 配置在{0} 由 {1}更改. 如非本人操作,请联系管理员。</p>"
};
export default messages;

Some files were not shown because too many files have changed in this diff Show More