Compare commits

..

19 Commits

Author SHA1 Message Date
a7c0e5bdaa Bump version 2023-06-19 01:37:20 +02:00
3b051cbbea Fix build 2023-06-19 01:37:03 +02:00
f7edfd1c29 Add release note 2023-06-19 01:37:03 +02:00
b182c43965 Be more lax on the detection of field name. 2023-06-19 01:37:03 +02:00
4639e7ad2e Better exception message 2023-06-19 01:37:03 +02:00
56241203a0 Merge branch 'main' of https://github.com/keycloakify/keycloakify 2023-06-19 00:09:33 +02:00
8c8540de5d Analyze the code to see what field names are acutally used. Deprecates the customUserAttributes option, it's no longer needed 2023-06-19 00:09:21 +02:00
b45af78322 Bump version 2023-06-18 16:13:36 +02:00
98bcf3bf7e #362: otpCredentials is an array! 2023-06-18 16:13:18 +02:00
e28bcfced3 Bump version 2023-06-18 00:27:36 +02:00
a5bd990245 #362 2023-06-18 00:27:20 +02:00
58301e0844 Bump version 2023-06-17 00:52:35 +02:00
c9213fb6cd Bump version 2023-06-16 23:44:21 +02:00
641819a364 #359: fix: 'Local variable assigned outside a macro.' 2023-06-16 23:44:04 +02:00
3ee3a8b41d #359: Remove comment 2023-06-16 23:39:49 +02:00
5600403088 Bump version 2023-06-16 23:30:58 +02:00
3b00bace23 #359: Wrongely named FTL variable name fix 2023-06-16 23:30:36 +02:00
fcba470aad Release candidate 2023-06-16 11:31:40 +02:00
206e602d73 Accomodate #218 and #359 2023-06-16 11:29:04 +02:00
15 changed files with 580 additions and 142 deletions

View File

@ -117,6 +117,10 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
# Changelog highlights # Changelog highlights
## 7.13
- Deprecate `customUserAttribute`, Keycloakify now analyze your code to predict field name usage. [See doc](https://docs.keycloakify.dev/build-options#customuserattributes).
## 7.12 ## 7.12
- You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme. - You can now pack multiple themes variant in a single `.jar` bundle. In vanilla Keycloak themes you have the ability to extend a base theme.

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "7.12.2", "version": "7.13.0",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -10,8 +10,7 @@
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"scripts": { "scripts": {
"prepare": "yarn generate-i18n-messages", "prepare": "yarn generate-i18n-messages",
"build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/", "build": "rimraf dist/ && tsc -p src/bin && tsc -p src && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"generate:json-schema": "ts-node scripts/generate-json-schema.ts", "generate:json-schema": "ts-node scripts/generate-json-schema.ts",
"grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js", "grant-exec-perms": "node dist/bin/tools/grant-exec-perms.js",
"copy-files": "copyfiles -u 1 src/**/*.ftl", "copy-files": "copyfiles -u 1 src/**/*.ftl",
@ -24,6 +23,7 @@
"generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts", "generate-i18n-messages": "ts-node --skipProject scripts/generate-i18n-messages.ts",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts", "link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"link-in-starter": "yarn link-in-app keycloakify-starter", "link-in-starter": "yarn link-in-app keycloakify-starter",
"watch-in-starter": "yarn build && yarn link-in-starter && (concurrently \"tsc -p src -w\" \"tsc-alias -p src/tsconfig.json\" \"tsc -p src/bin -w\")",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js", "copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/copy-keycloak-resources-to-public.js",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006", "storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook" "build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook"

View File

@ -52,9 +52,34 @@ export declare namespace KcContext {
name: string; // Client id name: string; // Client id
}; };
messagesPerField: { messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined; /**
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
*
* @param fieldName to check for
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
/**
* Check if exists error message for given fields
*
* @param fields
* @return boolean
*/
existsError: (fieldName: string) => boolean; existsError: (fieldName: string) => boolean;
/**
* Get message for given field.
*
* @param fieldName
* @return message text or empty string
*/
get: (fieldName: string) => string; get: (fieldName: string) => string;
/**
* Check if message for given field exists
*
* @param field
* @return boolean
*/
exists: (fieldName: string) => boolean; exists: (fieldName: string) => boolean;
}; };
account: { account: {

View File

@ -31,13 +31,3 @@ export function getThemeSrcDirPath(params: { projectDirPath: string }) {
return { themeSrcDirPath }; return { themeSrcDirPath };
} }
export function getEmailThemeSrcDirPath(params: { projectDirPath: string }) {
const { projectDirPath } = params;
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
const emailThemeSrcDirPath = themeSrcDirPath === undefined ? undefined : pathJoin(themeSrcDirPath, "email");
return { emailThemeSrcDirPath };
}

View File

@ -7,7 +7,7 @@ import { promptKeycloakVersion } from "./promptKeycloakVersion";
import { readBuildOptions } from "./keycloakify/BuildOptions"; import { readBuildOptions } from "./keycloakify/BuildOptions";
import * as fs from "fs"; import * as fs from "fs";
import { getLogger } from "./tools/logger"; import { getLogger } from "./tools/logger";
import { getEmailThemeSrcDirPath } from "./getSrcDirPath"; import { getThemeSrcDirPath } from "./getSrcDirPath";
export async function main() { export async function main() {
const { isSilent } = readBuildOptions({ const { isSilent } = readBuildOptions({
@ -17,16 +17,18 @@ export async function main() {
const logger = getLogger({ isSilent }); const logger = getLogger({ isSilent });
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ const { themeSrcDirPath } = getThemeSrcDirPath({
"projectDirPath": process.cwd() "projectDirPath": process.cwd()
}); });
if (emailThemeSrcDirPath === undefined) { if (themeSrcDirPath === undefined) {
logger.warn("Couldn't locate your theme source directory"); logger.warn("Couldn't locate your theme source directory");
process.exit(-1); process.exit(-1);
} }
const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (fs.existsSync(emailThemeSrcDirPath)) { if (fs.existsSync(emailThemeSrcDirPath)) {
logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`); logger.warn(`There is already a ${pathRelative(process.cwd(), emailThemeSrcDirPath)} directory in your project. Aborting.`);

View File

@ -28,7 +28,6 @@ export namespace BuildOptions {
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
/** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */ /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
customUserAttributes: string[];
}; };
export type Standalone = Common & { export type Standalone = Common & {
@ -199,8 +198,7 @@ export function readBuildOptions(params: { projectDirPath: string; processArgv:
} }
return keycloakifyBuildDirPath; return keycloakifyBuildDirPath;
})(), })()
"customUserAttributes": keycloakify.customUserAttributes ?? []
}; };
})(); })();

View File

@ -8,13 +8,7 @@
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }; out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["messagesPerField"]= { out["messagesPerField"]= {
<#assign fieldNames = [ <#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
"global", "userLabel", "username", "email", "firstName", "lastName", "password", "password-confirm",
"totp", "totpSecret", "SAMLRequest", "SAMLResponse", "relayState", "device_user_code", "code",
"password-new", "rememberMe", "login", "authenticationExecution", "cancel-aia", "clientDataJSON",
"authenticatorData", "signature", "credentialId", "userHandle", "error", "authn_use_chk", "authenticationExecution",
"isSetRetry", "try-again", "attestationObject", "publicKeyCredentialId", "authenticatorLabel"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM
]>
<#attempt> <#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable> <#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
@ -28,85 +22,371 @@
<#recover> <#recover>
</#attempt> </#attempt>
"printIfExists": function (fieldName, x) { "printIfExists": function (fieldName, text) {
<#if !messagesPerField?? >
return undefined; <#if !messagesPerField?? || !(messagesPerField?is_hash)>
<#else> throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'> <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>x<#else>undefined</#if>;
<#else> <#assign doExistMessageForUsernameOrPassword = "">
return <#if messagesPerField.existsError('${fieldName}')>x<#else>undefined</#if>;
</#if> <#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover> <#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt> </#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>text<#else>undefined</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
return text;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if>
</#if>
} }
</#list> </#list>
throw new Error("There is no " + fieldName + " field");
</#if> throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
}, },
"existsError": function (fieldName) { "existsError": function (fieldName) {
<#if !messagesPerField?? >
return false; <#if !messagesPerField?? || !(messagesPerField?is_hash)>
<#else> throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
</#if>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'> <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.existsError('username', 'password')>true<#else>false</#if>;
<#else> <#assign doExistMessageForUsernameOrPassword = "">
return <#if messagesPerField.existsError('${fieldName}')>true<#else>false</#if>;
</#if> <#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover> <#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt> </#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
} }
</#list> </#list>
throw new Error("There is no " + fieldName + " field");
</#if> throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
}, },
"get": function (fieldName) { "get": function (fieldName) {
<#if !messagesPerField?? >
return '';
<#else> <#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.get in this page");
</#if>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'> <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#if messagesPerField.existsError('username', 'password')>
return 'Invalid username or password.'; <#assign doExistMessageForUsernameOrPassword = "">
</#if>
<#else> <#attempt>
<#if messagesPerField.existsError('${fieldName}')> <#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
return "${messagesPerField.get('${fieldName}')?no_esc}";
</#if>
</#if>
<#recover> <#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt> </#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
<#if !doExistMessageForUsernameOrPassword>
return "";
<#else>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
<#if doExistErrorOnUsernameOrPassword>
<#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover>
return "Invalid username or password.";
</#attempt>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "";
</#attempt>
</#if>
<#else>
<#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover>
return "invalid field";
</#attempt>
</#if>
</#if>
} }
</#list> </#list>
throw new Error("There is no " + fieldName + " field");
</#if> throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
}, },
"exists": function (fieldName) { "exists": function (fieldName) {
<#if !messagesPerField?? >
return false; <#if !messagesPerField?? || !(messagesPerField?is_hash)>
<#else> throw new Error("You're not supposed to use messagesPerField.exists in this page");
</#if>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#attempt>
<#-- https://github.com/keycloakify/keycloakify/pull/359 Compat with Keycloak prior v12 -->
<#if !messagesPerField.existsError??>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'> <#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
return <#if messagesPerField.exists('username') || messagesPerField.exists('password')>true<#else>false</#if>;
<#else> <#assign doExistMessageForUsernameOrPassword = "">
return <#if messagesPerField.exists('${fieldName}')>true<#else>false</#if>;
</#if> <#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('username')>
<#recover> <#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt> </#attempt>
<#if !doExistMessageForUsernameOrPassword>
<#attempt>
<#assign doExistMessageForUsernameOrPassword = messagesPerField.exists('password')>
<#recover>
<#assign doExistMessageForUsernameOrPassword = true>
</#attempt>
</#if>
return <#if doExistMessageForUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistMessageForField = "">
<#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistMessageForField = true>
</#attempt>
return <#if doExistMessageForField>true<#else>false</#if>;
</#if>
<#else>
<#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if '${fieldName}' == 'username' || '${fieldName}' == 'password'>
<#assign doExistErrorOnUsernameOrPassword = "">
<#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover>
<#assign doExistErrorOnUsernameOrPassword = true>
</#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else>
<#assign doExistErrorMessageForField = "">
<#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover>
<#assign doExistErrorMessageForField = true>
</#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if>
</#if>
} }
</#list> </#list>
throw new Error("There is no " + fieldName + " field");
</#if> throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
} }
}; };
@ -176,6 +456,10 @@
are_same_path(path, ["url"]) && are_same_path(path, ["url"]) &&
["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl"]?seq_contains(pageId) && ["saml-post-form.ftl", "error.ftl", "info.ftl", "login-oauth-grant.ftl"]?seq_contains(pageId) &&
!(auth?has_content && auth.showTryAnotherWayLink()) !(auth?has_content && auth.showTryAnotherWayLink())
) || (
<#-- https://github.com/keycloakify/keycloakify/issues/362 -->
["secretData", "value"]?seq_contains(key) &&
are_same_path(path, [ "totp", "otpCredentials", "*" ])
) || ( ) || (
["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) && ["contextData", "idpConfig", "idp", "authenticationSession"]?seq_contains(key) &&
are_same_path(path, ["brokerContext"]) && are_same_path(path, ["brokerContext"]) &&

View File

@ -18,7 +18,6 @@ export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.Ex
export namespace BuildOptionsLike { export namespace BuildOptionsLike {
export type Common = { export type Common = {
themeName: string; themeName: string;
customUserAttributes: string[];
themeVersion: string; themeVersion: string;
}; };
@ -57,8 +56,9 @@ export function generateFtlFilesCodeFactory(params: {
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
keycloakifyVersion: string; keycloakifyVersion: string;
themeType: ThemeType; themeType: ThemeType;
fieldNames: string[];
}) { }) {
const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType } = params; const { cssGlobalsToDefine, indexHtmlCode, buildOptions, keycloakifyVersion, themeType, fieldNames } = params;
const $ = cheerio.load(indexHtmlCode); const $ = cheerio.load(indexHtmlCode);
@ -129,10 +129,7 @@ export function generateFtlFilesCodeFactory(params: {
.readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl")) .readFileSync(pathJoin(__dirname, "ftl_object_to_js_code_declaring_an_object.ftl"))
.toString("utf8") .toString("utf8")
.match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1] .match(/^<script>const _=((?:.|\n)+)<\/script>[\n]?$/)![1]
.replace( .replace("FIELD_NAMES_eKsIY4ZsZ4xeM", fieldNames.map(name => `"${name}"`).join(", "))
"CUSTOM_USER_ATTRIBUTES_eKsIY4ZsZ4xeM",
buildOptions.customUserAttributes.length === 0 ? "" : ", " + buildOptions.customUserAttributes.map(name => `"${name}"`).join(", ")
)
.replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion) .replace("KEYCLOAKIFY_VERSION_xEdKd3xEdr", keycloakifyVersion)
.replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion) .replace("KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx", buildOptions.themeVersion)
.replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType) .replace("KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr", themeType)

View File

@ -1,9 +1,9 @@
import * as fs from "fs"; import * as fs from "fs";
import { join as pathJoin, dirname as pathDirname } from "path"; import { join as pathJoin, dirname as pathDirname } from "path";
import { themeTypes } from "./generateFtl/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Reflect } from "tsafe/Reflect"; import { Reflect } from "tsafe/Reflect";
import type { BuildOptions } from "./BuildOptions"; import type { BuildOptions } from "./BuildOptions";
import type { ThemeType } from "./generateFtl";
export type BuildOptionsLike = { export type BuildOptionsLike = {
themeName: string; themeName: string;
@ -21,7 +21,7 @@ export type BuildOptionsLike = {
export function generateJavaStackFiles(params: { export function generateJavaStackFiles(params: {
keycloakThemeBuildingDirPath: string; keycloakThemeBuildingDirPath: string;
doBundlesEmailTemplate: boolean; implementedThemeTypes: Record<ThemeType | "email", boolean>;
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
}): { }): {
jarFilePath: string; jarFilePath: string;
@ -29,7 +29,7 @@ export function generateJavaStackFiles(params: {
const { const {
buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId }, buildOptions: { groupId, themeName, extraThemeNames, themeVersion, artifactId },
keycloakThemeBuildingDirPath, keycloakThemeBuildingDirPath,
doBundlesEmailTemplate implementedThemeTypes
} = params; } = params;
{ {
@ -70,7 +70,9 @@ export function generateJavaStackFiles(params: {
{ {
"themes": [themeName, ...extraThemeNames].map(themeName => ({ "themes": [themeName, ...extraThemeNames].map(themeName => ({
"name": themeName, "name": themeName,
"types": [...themeTypes, ...(doBundlesEmailTemplate ? ["email"] : [])] "types": Object.entries(implementedThemeTypes)
.filter(([, isImplemented]) => isImplemented)
.map(([themeType]) => themeType)
})) }))
}, },
null, null,

View File

@ -33,7 +33,6 @@ export function generateStartKeycloakTestingContainer(params: {
fs.writeFileSync( fs.writeFileSync(
pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename), pathJoin(keycloakThemeBuildingDirPath, generateStartKeycloakTestingContainer.basename),
Buffer.from( Buffer.from(
[ [
"#!/usr/bin/env bash", "#!/usr/bin/env bash",

View File

@ -9,6 +9,7 @@ import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../BuildOptions"; import type { BuildOptions } from "../BuildOptions";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources"; import { downloadKeycloakStaticResources } from "./downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets; export type BuildOptionsLike = BuildOptionsLike.Standalone | BuildOptionsLike.ExternalAssets;
@ -19,7 +20,6 @@ export namespace BuildOptionsLike {
extraAccountPages?: string[]; extraAccountPages?: string[];
extraThemeProperties?: string[]; extraThemeProperties?: string[];
isSilent: boolean; isSilent: boolean;
customUserAttributes: string[];
themeVersion: string; themeVersion: string;
keycloakVersionDefaultAssets: string; keycloakVersionDefaultAssets: string;
}; };
@ -53,11 +53,12 @@ assert<BuildOptions extends BuildOptionsLike ? true : false>();
export async function generateTheme(params: { export async function generateTheme(params: {
reactAppBuildDirPath: string; reactAppBuildDirPath: string;
keycloakThemeBuildingDirPath: string; keycloakThemeBuildingDirPath: string;
emailThemeSrcDirPath: string | undefined; themeSrcDirPath: string | undefined;
keycloakifySrcDirPath: string;
buildOptions: BuildOptionsLike; buildOptions: BuildOptionsLike;
keycloakifyVersion: string; keycloakifyVersion: string;
}): Promise<{ doBundlesEmailTemplate: boolean }> { }): Promise<void> {
const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, emailThemeSrcDirPath, buildOptions, keycloakifyVersion } = params; const { reactAppBuildDirPath, keycloakThemeBuildingDirPath, themeSrcDirPath, keycloakifySrcDirPath, buildOptions, keycloakifyVersion } = params;
const getThemeDirPath = (themeType: ThemeType | "email") => const getThemeDirPath = (themeType: ThemeType | "email") =>
pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType); pathJoin(keycloakThemeBuildingDirPath, "src", "main", "resources", "theme", buildOptions.themeName, themeType);
@ -142,7 +143,12 @@ export async function generateTheme(params: {
"cssGlobalsToDefine": allCssGlobalsToDefine, "cssGlobalsToDefine": allCssGlobalsToDefine,
buildOptions, buildOptions,
keycloakifyVersion, keycloakifyVersion,
themeType,
"fieldNames": readFieldNameUsage({
keycloakifySrcDirPath,
themeSrcDirPath,
themeType themeType
})
}); });
return generateFtlFilesCode; return generateFtlFilesCode;
@ -220,21 +226,20 @@ export async function generateTheme(params: {
); );
} }
let doBundlesEmailTemplate: boolean;
email: { email: {
if (emailThemeSrcDirPath === undefined) { if (themeSrcDirPath === undefined) {
doBundlesEmailTemplate = false;
break email; break email;
} }
doBundlesEmailTemplate = true; const emailThemeSrcDirPath = pathJoin(themeSrcDirPath, "email");
if (!fs.existsSync(emailThemeSrcDirPath)) {
break email;
}
transformCodebase({ transformCodebase({
"srcDirPath": emailThemeSrcDirPath, "srcDirPath": emailThemeSrcDirPath,
"destDirPath": getThemeDirPath("email") "destDirPath": getThemeDirPath("email")
}); });
} }
return { doBundlesEmailTemplate };
} }

View File

@ -0,0 +1,96 @@
import { crawl } from "../../tools/crawl";
import { removeDuplicates } from "evt/tools/reducers/removeDuplicates";
import { join as pathJoin } from "path";
import * as fs from "fs";
import type { ThemeType } from "../generateFtl";
import { exclude } from "tsafe/exclude";
export function readFieldNameUsage(params: {
keycloakifySrcDirPath: string;
themeSrcDirPath: string | undefined;
themeType: ThemeType | "email";
}): string[] {
const { keycloakifySrcDirPath, themeSrcDirPath, themeType } = params;
const fieldNames: string[] = [];
if (themeSrcDirPath === undefined) {
//If we can't detect the user theme directory we restore the fieldNames we had previously to prevent errors.
fieldNames.push(
...[
"global",
"userLabel",
"username",
"email",
"firstName",
"lastName",
"password",
"password-confirm",
"totp",
"totpSecret",
"SAMLRequest",
"SAMLResponse",
"relayState",
"device_user_code",
"code",
"password-new",
"rememberMe",
"login",
"authenticationExecution",
"cancel-aia",
"clientDataJSON",
"authenticatorData",
"signature",
"credentialId",
"userHandle",
"error",
"authn_use_chk",
"authenticationExecution",
"isSetRetry",
"try-again",
"attestationObject",
"publicKeyCredentialId",
"authenticatorLabel"
]
);
}
for (const srcDirPath of (
[
pathJoin(keycloakifySrcDirPath, themeType),
(() => {
if (themeSrcDirPath === undefined) {
return undefined;
}
const srcDirPath = pathJoin(themeSrcDirPath, themeType);
if (!fs.existsSync(srcDirPath)) {
return undefined;
}
return srcDirPath;
})()
] as const
).filter(exclude(undefined))) {
const filePaths = crawl(srcDirPath)
.filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath))
.map(filePath => pathJoin(srcDirPath, filePath));
for (const filePath of filePaths) {
const rawSourceFile = fs.readFileSync(filePath).toString("utf8");
if (!rawSourceFile.includes("messagesPerField")) {
continue;
}
fieldNames.push(
...Array.from(rawSourceFile.matchAll(/(?:(?:printIfExists)|(?:existsError)|(?:get)|(?:exists))\(["']([^"']+)["']/g), m => m[1])
);
}
}
const out = fieldNames.reduce(...removeDuplicates<string>());
return out;
}

View File

@ -9,8 +9,9 @@ import { getLogger } from "../tools/logger";
import jar from "../tools/jar"; import jar from "../tools/jar";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import { Equals } from "tsafe"; import { Equals } from "tsafe";
import { getEmailThemeSrcDirPath } from "../getSrcDirPath"; import { getThemeSrcDirPath } from "../getSrcDirPath";
import { getProjectRoot } from "../tools/getProjectRoot"; import { getProjectRoot } from "../tools/getProjectRoot";
import { objectKeys } from "tsafe/objectKeys";
export async function main() { export async function main() {
const projectDirPath = process.cwd(); const projectDirPath = process.cwd();
@ -23,42 +24,54 @@ export async function main() {
const logger = getLogger({ "isSilent": buildOptions.isSilent }); const logger = getLogger({ "isSilent": buildOptions.isSilent });
logger.log("🔏 Building the keycloak theme...⌚"); logger.log("🔏 Building the keycloak theme...⌚");
let doBundlesEmailTemplate: boolean | undefined; const keycloakifyDirPath = getProjectRoot();
const { themeSrcDirPath } = getThemeSrcDirPath({ projectDirPath });
for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) { for (const themeName of [buildOptions.themeName, ...buildOptions.extraThemeNames]) {
const { doBundlesEmailTemplate: doBundlesEmailTemplate_ } = await generateTheme({ await generateTheme({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath, "keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
"emailThemeSrcDirPath": (() => { themeSrcDirPath,
const { emailThemeSrcDirPath } = getEmailThemeSrcDirPath({ projectDirPath }); "keycloakifySrcDirPath": pathJoin(keycloakifyDirPath, "src"),
if (emailThemeSrcDirPath === undefined || !fs.existsSync(emailThemeSrcDirPath)) {
return;
}
return emailThemeSrcDirPath;
})(),
"reactAppBuildDirPath": buildOptions.reactAppBuildDirPath, "reactAppBuildDirPath": buildOptions.reactAppBuildDirPath,
"buildOptions": { "buildOptions": {
...buildOptions, ...buildOptions,
"themeName": themeName "themeName": themeName
}, },
"keycloakifyVersion": (() => { "keycloakifyVersion": (() => {
const version = JSON.parse(fs.readFileSync(pathJoin(getProjectRoot(), "package.json")).toString("utf8"))["version"]; const version = JSON.parse(fs.readFileSync(pathJoin(keycloakifyDirPath, "package.json")).toString("utf8"))["version"];
assert(typeof version === "string"); assert(typeof version === "string");
return version; return version;
})() })()
}); });
doBundlesEmailTemplate ??= doBundlesEmailTemplate_;
} }
assert(doBundlesEmailTemplate !== undefined);
const { jarFilePath } = generateJavaStackFiles({ const { jarFilePath } = generateJavaStackFiles({
keycloakThemeBuildingDirPath: buildOptions.keycloakifyBuildDirPath, "keycloakThemeBuildingDirPath": buildOptions.keycloakifyBuildDirPath,
doBundlesEmailTemplate, "implementedThemeTypes": (() => {
const implementedThemeTypes = {
"login": false,
"account": false,
"email": false
};
if (themeSrcDirPath === undefined) {
implementedThemeTypes["login"] = true;
implementedThemeTypes["account"] = true;
return implementedThemeTypes;
}
for (const themeType of objectKeys(implementedThemeTypes)) {
if (!fs.existsSync(pathJoin(themeSrcDirPath, themeType))) {
continue;
}
implementedThemeTypes[themeType] = true;
}
return implementedThemeTypes;
})(),
buildOptions buildOptions
}); });

View File

@ -23,7 +23,6 @@ export type ParsedPackageJson = {
keycloakVersionDefaultAssets?: string; keycloakVersionDefaultAssets?: string;
reactAppBuildDirPath?: string; reactAppBuildDirPath?: string;
keycloakifyBuildDirPath?: string; keycloakifyBuildDirPath?: string;
customUserAttributes?: string[];
themeName?: string; themeName?: string;
extraThemeNames?: string[]; extraThemeNames?: string[];
}; };
@ -46,7 +45,6 @@ export const zParsedPackageJson = z.object({
"keycloakVersionDefaultAssets": z.string().optional(), "keycloakVersionDefaultAssets": z.string().optional(),
"reactAppBuildDirPath": z.string().optional(), "reactAppBuildDirPath": z.string().optional(),
"keycloakifyBuildDirPath": z.string().optional(), "keycloakifyBuildDirPath": z.string().optional(),
"customUserAttributes": z.array(z.string()).optional(),
"themeName": z.string().optional(), "themeName": z.string().optional(),
"extraThemeNames": z.array(z.string()).optional() "extraThemeNames": z.array(z.string()).optional()
}) })

View File

@ -81,9 +81,34 @@ export declare namespace KcContext {
}; };
isAppInitiatedAction: boolean; isAppInitiatedAction: boolean;
messagesPerField: { messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined; /**
* Return text if message for given field exists. Useful eg. to add css styles for fields with message.
*
* @param fieldName to check for
* @param text to return
* @return text if message exists for given field, else undefined
*/
printIfExists: <T extends string>(fieldName: string, text: T) => T | undefined;
/**
* Check if exists error message for given fields
*
* @param fields
* @return boolean
*/
existsError: (fieldName: string) => boolean; existsError: (fieldName: string) => boolean;
/**
* Get message for given field.
*
* @param fieldName
* @return message text or empty string
*/
get: (fieldName: string) => string; get: (fieldName: string) => string;
/**
* Check if message for given field exists
*
* @param field
* @return boolean
*/
exists: (fieldName: string) => boolean; exists: (fieldName: string) => boolean;
}; };
}; };