Compare commits

...

26 Commits

Author SHA1 Message Date
f59ee55be5 Release candidate 2024-05-14 05:53:46 +02:00
bc549af64c Less verbose js coments (for start) #542 2024-05-14 05:53:22 +02:00
39add772f7 Fix vim motion typo 2024-05-14 05:44:39 +02:00
f19e622d39 Improve a little bit the readability of the rendered template 2024-05-14 05:30:03 +02:00
7eb13db467 Fix error in ftl templat 2024-05-14 05:05:18 +02:00
ef503e271d #545 2024-05-14 04:57:38 +02:00
7d24e2716f Update build scripts 2024-05-14 04:56:06 +02:00
9bbcc21f9c Remove debug file 2024-05-14 04:55:42 +02:00
fcfabb0c3f Update build script 2024-05-14 03:55:41 +02:00
9ca3cadd10 Converts all functions without arguments at the same place in the ftl template 2024-05-14 03:30:27 +02:00
f156fec1c3 Forget to add displayRequiredFields on some pages 2024-05-14 02:39:43 +02:00
e962b37948 Fix language menu select in templates 2024-05-14 02:32:03 +02:00
3a8f1a0ed1 Remove --feature=declarative-user-profile from testing container launch script 2024-05-14 01:47:08 +02:00
e3a7bb13f5 Fix inputs using value instead of defaultValue 2024-05-14 01:33:31 +02:00
29b45497ba Add missing mock value 2024-05-14 00:03:48 +02:00
a748e8d8ec Relase candidate 2024-05-14 00:01:06 +02:00
f2fcb553a5 Pass totp.policy.getAlgorithmKey() to the freemarker template 2024-05-14 00:00:17 +02:00
a9dc11c60d Fix path error in generate theme variant 2024-05-13 23:47:28 +02:00
ee9df31b18 Release candidate 2024-05-13 23:39:39 +02:00
69d1e86a8a Fix build jar script 2024-05-13 23:39:18 +02:00
06761807a3 Fix non closed tag 2024-05-13 23:39:09 +02:00
a6c1e9bb61 Fix several logical errors 2024-05-13 23:21:27 +02:00
b70dfe96f6 Remove debug log 2024-05-13 23:20:58 +02:00
5f2b1484b5 Update the exceptions 2024-05-13 23:20:40 +02:00
d4595c999f Better portability 2024-05-13 22:32:17 +02:00
373850e32a Fix storybook build 2024-05-13 04:00:52 +02:00
29 changed files with 249 additions and 409 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "keycloakify", "name": "keycloakify",
"version": "10.0.0-rc.0", "version": "10.0.0-rc.3",
"description": "Create Keycloak themes using React", "description": "Create Keycloak themes using React",
"repository": { "repository": {
"type": "git", "type": "git",
@ -10,10 +10,10 @@
"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 -p src/vite-plugin && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn copy-files dist/ && cp -r src dist/", "build": "tsc -p src/bin && tsc -p src && tsc -p src/vite-plugin && tsc-alias -p src/tsconfig.json && yarn grant-exec-perms && yarn run copy-files && cp -r src dist/",
"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": "ts-node --skipProject scripts/grant-exec-perms.ts",
"copy-files": "copyfiles -u 1 src/**/*.ftl src/**/*.java", "copy-files": "copyfiles -u 1 'src/**/*.ftl' dist/",
"test": "yarn test:types && vitest run", "test": "yarn test:types && vitest run",
"test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter", "test:keycloakify-starter": "ts-node scripts/test-keycloakify-starter",
"test:types": "tsc -p test/tsconfig.json --noEmit", "test:types": "tsc -p test/tsconfig.json --noEmit",
@ -23,7 +23,6 @@
"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"
@ -97,7 +96,6 @@
"properties-parser": "^0.3.1", "properties-parser": "^0.3.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rimraf": "^3.0.2",
"scripting-tools": "^0.19.13", "scripting-tools": "^0.19.13",
"storybook-dark-mode": "^1.1.2", "storybook-dark-mode": "^1.1.2",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",

View File

@ -1,10 +1,9 @@
import { getThisCodebaseRootDirPath } from "./getThisCodebaseRootDirPath";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { constants } from "fs"; import { constants } from "fs";
import { chmod, stat } from "fs/promises"; import { chmod, stat } from "fs/promises";
(async () => { (async () => {
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath(); const thisCodebaseRootDirPath = pathJoin(__dirname, "..");
const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json")); const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json"));

View File

@ -15,7 +15,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { getClassName } = useGetClassName({ doUseDefaultCss, classes }); const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, msgStr, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { locale, url, features, realm, message, referrer } = kcContext; const { locale, url, features, realm, message, referrer } = kcContext;
@ -81,10 +81,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<ul> <ul>
{locale.supported.map(({ languageTag }) => ( {locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item"> <li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} <a href={getChangeLocalUrl(languageTag)}>{labelBySupportedLanguageTag[languageTag]}</a>
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
</li> </li>
))} ))}
</ul> </ul>

View File

@ -28,11 +28,10 @@ export type GenericI18n<MessageKey extends string> = {
*/ */
currentLanguageTag: string; currentLanguageTag: string;
/** /**
* To call when the user switch language. * Redirect to this url to change the language.
* This will cause the page to be reloaded, * After reload currentLanguageTag === newLanguageTag
* on next load currentLanguageTag === newLanguageTag
*/ */
changeLocale: (newLanguageTag: string) => never; getChangeLocalUrl: (newLanguageTag: string) => string;
/** /**
* e.g. "en" => "English", "fr" => "Français", ... * e.g. "en" => "English", "fr" => "Français", ...
* *
@ -104,7 +103,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
} as any } as any
}), }),
currentLanguageTag, currentLanguageTag,
"changeLocale": newLanguageTag => { "getChangeLocalUrl": newLanguageTag => {
const { locale } = kcContext; const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled"); assert(locale !== undefined, "Internationalization not enabled");
@ -113,9 +112,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`); assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url; return targetSupportedLocale.url;
assert(false, "never");
}, },
"labelBySupportedLanguageTag": Object.fromEntries( "labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]) (kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])

View File

@ -164,21 +164,6 @@ export declare namespace KcContext {
}; };
mode?: "qr" | "manual" | undefined | null; mode?: "qr" | "manual" | undefined | null;
isAppInitiatedAction: boolean; isAppInitiatedAction: boolean;
url: {
accountUrl: string;
passwordUrl: string;
totpUrl: string;
socialUrl: string;
sessionsUrl: string;
applicationsUrl: string;
logUrl: string;
resourceUrl: string;
resourcesCommonPath: string;
resourcesPath: string;
/** @deprecated, not present in recent keycloak version apparently, use kcContext.referrer instead */
referrerURI?: string;
getLogoutUrl: () => string;
};
stateChecker: string; stateChecker: string;
}; };

View File

@ -29,13 +29,12 @@ export async function buildJar(params: {
const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", jarFileBasename.replace(".jar", "")); const keycloakifyBuildTmpDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, "..", jarFileBasename.replace(".jar", ""));
{ if (existsSync(keycloakifyBuildTmpDirPath)) {
if (!existsSync(buildOptions.keycloakifyBuildDirPath)) { await fs.rm(keycloakifyBuildTmpDirPath, { "recursive": true });
await fs.mkdir(buildOptions.keycloakifyBuildDirPath, { "recursive": true });
} }
await fs.writeFile(pathJoin(buildOptions.keycloakifyBuildDirPath, ".gitignore"), Buffer.from("*", "utf8")); await fs.mkdir(keycloakifyBuildTmpDirPath, { "recursive": true });
} await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, ".gitignore"), Buffer.from("*", "utf8"));
const srcMainResourcesRelativeDirPath = pathJoin("src", "main", "resources"); const srcMainResourcesRelativeDirPath = pathJoin("src", "main", "resources");
@ -43,10 +42,10 @@ export async function buildJar(params: {
const keycloakThemesJsonFilePath = pathJoin(srcMainResourcesRelativeDirPath, "META-INF", "keycloak-themes.json"); const keycloakThemesJsonFilePath = pathJoin(srcMainResourcesRelativeDirPath, "META-INF", "keycloak-themes.json");
const themePropertiesFilePathSet = new Set( const themePropertiesFilePathSet = new Set(
...buildOptions.themeNames.map(themeName => pathJoin(srcMainResourcesRelativeDirPath, "themes", themeName, "account", "theme.properties")) ...buildOptions.themeNames.map(themeName => pathJoin(srcMainResourcesRelativeDirPath, "theme", themeName, "account", "theme.properties"))
); );
const accountV1RelativeDirPath = pathJoin(srcMainResourcesRelativeDirPath, "themes", accountV1ThemeName); const accountV1RelativeDirPath = pathJoin(srcMainResourcesRelativeDirPath, "theme", accountV1ThemeName);
transformCodebase({ transformCodebase({
"srcDirPath": buildOptions.keycloakifyBuildDirPath, "srcDirPath": buildOptions.keycloakifyBuildDirPath,
@ -108,7 +107,7 @@ export async function buildJar(params: {
(["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId => (["register.ftl", "login-update-profile.ftl"] as const).forEach(pageId =>
buildOptions.themeNames.map(themeName => { buildOptions.themeNames.map(themeName => {
const ftlFilePath = pathJoin(srcMainResourcesRelativeDirPath, "themes", themeName, "login", pageId); const ftlFilePath = pathJoin(keycloakifyBuildTmpDirPath, srcMainResourcesRelativeDirPath, "theme", themeName, "login", pageId);
const ftlFileContent = readFileSync(ftlFilePath).toString("utf8"); const ftlFileContent = readFileSync(ftlFilePath).toString("utf8");
@ -123,7 +122,7 @@ export async function buildJar(params: {
})(); })();
const modifiedFtlFileContent = ftlFileContent.replace( const modifiedFtlFileContent = ftlFileContent.replace(
`out["pageId"] = "${pageId}";`, `out["pageId"] = "\${pageId}";`,
`out["pageId"] = "${pageId}"; out["realPageId"] = "${realPageId}";` `out["pageId"] = "${pageId}"; out["realPageId"] = "${realPageId}";`
); );
@ -141,12 +140,24 @@ export async function buildJar(params: {
keycloakThemeAdditionalInfoExtensionVersion keycloakThemeAdditionalInfoExtensionVersion
}); });
await fs.writeFile(pathJoin(buildOptions.keycloakifyBuildDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8")); await fs.writeFile(pathJoin(keycloakifyBuildTmpDirPath, "pom.xml"), Buffer.from(pomFileCode, "utf8"));
} }
await new Promise<void>((resolve, reject) => await new Promise<void>((resolve, reject) =>
child_process.exec("mvn clean install", { "cwd": keycloakifyBuildTmpDirPath }, error => { child_process.exec("mvn clean install", { "cwd": keycloakifyBuildTmpDirPath }, error => {
if (error !== null) { if (error !== null) {
console.error(
`Build jar failed: ${JSON.stringify(
{
jarFileBasename,
keycloakAccountV1Version,
keycloakThemeAdditionalInfoExtensionVersion
},
null,
2
)}`
);
reject(error); reject(error);
return; return;
} }

View File

@ -73,10 +73,10 @@ export function generatePom(params: {
` </dependency>` ` </dependency>`
] ]
: []), : []),
` </dependencies>`, ` </dependencies>`
`</project>`
] ]
: []) : []),
`</project>`
].join("\n"); ].join("\n");
return { pomFileCode }; return { pomFileCode };

View File

@ -1,15 +1,11 @@
<script>const _= <script>const _=
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
(()=>{ (()=>{
<#assign pageId="PAGE_ID_xIgLsPgGId9D8e">
const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc}; const out = ${ftl_object_to_js_code_declaring_an_object(.data_model, [])?no_esc};
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["msg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }; out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); };
out["advancedMsg"]= function(){ throw new Error("use import { useKcMessage } from 'keycloakify'"); }; out["messagesPerField"]= {
out["messagesPerField"]= {
<#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]> <#assign fieldNames = [ FIELD_NAMES_eKsIY4ZsZ4xeM ]>
<#attempt> <#attempt>
<#if profile?? && profile.attributes?? && profile.attributes?is_enumerable> <#if profile?? && profile.attributes?? && profile.attributes?is_enumerable>
<#list profile.attributes as attribute> <#list profile.attributes as attribute>
@ -21,260 +17,173 @@
</#if> </#if>
<#recover> <#recover>
</#attempt> </#attempt>
"printIfExists": function (fieldName, text) { "printIfExists": function (fieldName, text) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)> <#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page"); throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else> <#else>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 --> <#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'> <#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = ""> <#assign doExistErrorOnUsernameOrPassword = "">
<#attempt> <#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')> <#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover> <#recover>
<#assign doExistErrorOnUsernameOrPassword = true> <#assign doExistErrorOnUsernameOrPassword = true>
</#attempt> </#attempt>
<#if doExistErrorOnUsernameOrPassword> <#if doExistErrorOnUsernameOrPassword>
return text; return text;
<#else> <#else>
<#assign doExistMessageForField = ""> <#assign doExistMessageForField = "">
<#attempt> <#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')> <#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover> <#recover>
<#assign doExistMessageForField = true> <#assign doExistMessageForField = true>
</#attempt> </#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>; return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if> </#if>
<#else> <#else>
<#assign doExistMessageForField = ""> <#assign doExistMessageForField = "">
<#attempt> <#attempt>
<#assign doExistMessageForField = messagesPerField.exists('${fieldName}')> <#assign doExistMessageForField = messagesPerField.exists('${fieldName}')>
<#recover> <#recover>
<#assign doExistMessageForField = true> <#assign doExistMessageForField = true>
</#attempt> </#attempt>
return <#if doExistMessageForField>text<#else>undefined</#if>; return <#if doExistMessageForField>text<#else>undefined</#if>;
</#if> </#if>
} }
</#list> </#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated"); throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if> </#if>
}, },
"existsError": function (){ "existsError": function (){
function existsError_singleFieldName(fieldName) { function existsError_singleFieldName(fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)> <#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.printIfExists in this page"); throw new Error("You're not supposed to use messagesPerField.printIfExists in this page");
<#else> <#else>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 --> <#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'> <#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = ""> <#assign doExistErrorOnUsernameOrPassword = "">
<#attempt> <#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')> <#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover> <#recover>
<#assign doExistErrorOnUsernameOrPassword = true> <#assign doExistErrorOnUsernameOrPassword = true>
</#attempt> </#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>; return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else> <#else>
<#assign doExistErrorMessageForField = ""> <#assign doExistErrorMessageForField = "">
<#attempt> <#attempt>
<#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')> <#assign doExistErrorMessageForField = messagesPerField.existsError('${fieldName}')>
<#recover> <#recover>
<#assign doExistErrorMessageForField = true> <#assign doExistErrorMessageForField = true>
</#attempt> </#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>; return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if> </#if>
} }
</#list> </#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated"); throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if> </#if>
} }
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
if( existsError_singleFieldName(arguments[i]) ){ if( existsError_singleFieldName(arguments[i]) ){
return true; return true;
} }
} }
return false; return false;
}, },
"get": function (fieldName) { "get": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)> <#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.get in this page"); throw new Error("You're not supposed to use messagesPerField.get in this page");
<#else> <#else>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 --> <#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'> <#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = ""> <#assign doExistErrorOnUsernameOrPassword = "">
<#attempt> <#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')> <#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover> <#recover>
<#assign doExistErrorOnUsernameOrPassword = true> <#assign doExistErrorOnUsernameOrPassword = true>
</#attempt> </#attempt>
<#if doExistErrorOnUsernameOrPassword> <#if doExistErrorOnUsernameOrPassword>
<#attempt> <#attempt>
return "${kcSanitize(msg('invalidUserMessage'))?no_esc}"; return "${kcSanitize(msg('invalidUserMessage'))?no_esc}";
<#recover> <#recover>
return "Invalid username or password."; return "Invalid username or password.";
</#attempt> </#attempt>
<#else> <#else>
<#attempt> <#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}"; return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover> <#recover>
return ""; return "";
</#attempt> </#attempt>
</#if> </#if>
<#else> <#else>
<#attempt> <#attempt>
return "${messagesPerField.get('${fieldName}')?no_esc}"; return "${messagesPerField.get('${fieldName}')?no_esc}";
<#recover> <#recover>
return "invalid field"; return "invalid field";
</#attempt> </#attempt>
</#if> </#if>
} }
</#list> </#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated"); throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if> </#if>
}, },
"exists": function (fieldName) { "exists": function (fieldName) {
<#if !messagesPerField?? || !(messagesPerField?is_hash)> <#if !messagesPerField?? || !(messagesPerField?is_hash)>
throw new Error("You're not supposed to use messagesPerField.exists in this page"); throw new Error("You're not supposed to use messagesPerField.exists in this page");
<#else> <#else>
<#list fieldNames as fieldName> <#list fieldNames as fieldName>
if(fieldName === "${fieldName}" ){ if(fieldName === "${fieldName}" ){
<#-- https://github.com/keycloakify/keycloakify/pull/218 --> <#-- https://github.com/keycloakify/keycloakify/pull/218 -->
<#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'> <#if ('${fieldName}' == 'username' || '${fieldName}' == 'password') && pageId != 'register.ftl' && pageId != 'register-user-profile.ftl'>
<#assign doExistErrorOnUsernameOrPassword = ""> <#assign doExistErrorOnUsernameOrPassword = "">
<#attempt> <#attempt>
<#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')> <#assign doExistErrorOnUsernameOrPassword = messagesPerField.existsError('username', 'password')>
<#recover> <#recover>
<#assign doExistErrorOnUsernameOrPassword = true> <#assign doExistErrorOnUsernameOrPassword = true>
</#attempt> </#attempt>
return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>; return <#if doExistErrorOnUsernameOrPassword>true<#else>false</#if>;
<#else> <#else>
<#assign doExistErrorMessageForField = ""> <#assign doExistErrorMessageForField = "">
<#attempt> <#attempt>
<#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')> <#assign doExistErrorMessageForField = messagesPerField.exists('${fieldName}')>
<#recover> <#recover>
<#assign doExistErrorMessageForField = true> <#assign doExistErrorMessageForField = true>
</#attempt> </#attempt>
return <#if doExistErrorMessageForField>true<#else>false</#if>; return <#if doExistErrorMessageForField>true<#else>false</#if>;
</#if> </#if>
} }
</#list> </#list>
throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated"); throw new Error(fieldName + "is probably runtime generated, see: https://docs.keycloakify.dev/limitations#field-names-cant-be-runtime-generated");
</#if> </#if>
}, },
"getFirstError": function () { "getFirstError": function () {
for( let i = 0; i < arguments.length; i++ ){ for( let i = 0; i < arguments.length; i++ ){
const fieldName = arguments[i]; const fieldName = arguments[i];
if( out.messagesPerField.existsError(fieldName) ){ if( out.messagesPerField.existsError(fieldName) ){
return out.messagesPerField.get(fieldName); return out.messagesPerField.get(fieldName);
} }
} }
} }
}; };
<#if account??> out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["url"]["getLogoutUrl"] = function () { out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
<#attempt> out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
return "${url.getLogoutUrl()}"; out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
<#recover> out["pageId"] = "${pageId}";
</#attempt>
};
</#if>
out["keycloakifyVersion"] = "KEYCLOAKIFY_VERSION_xEdKd3xEdr";
out["themeVersion"] = "KEYCLOAKIFY_THEME_VERSION_sIgKd3xEdr3dx";
out["themeType"] = "KEYCLOAKIFY_THEME_TYPE_dExKd3xEdr";
out["themeName"] = "KEYCLOAKIFY_THEME_NAME_cXxKd3xEer";
out["pageId"] = "${pageId}";
try {
try {
out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv"; out["url"]["resourcesCommonPath"] = out["url"]["resourcesPath"] + "/" + "RESOURCES_COMMON_cLsLsMrtDkpVv";
} catch(error) { }
} catch(error) { return out;
} })();
return out;
})()
<#function ftl_object_to_js_code_declaring_an_object object path> <#function ftl_object_to_js_code_declaring_an_object object path>
<#local isHash = ""> <#local isHash = "">
@ -298,7 +207,6 @@
<#return "ABORT: We can't list keys on this object"> <#return "ABORT: We can't list keys on this object">
</#attempt> </#attempt>
<#local out_seq = []> <#local out_seq = []>
<#list keys as key> <#list keys as key>
@ -367,23 +275,23 @@
key == "realmAttributes" key == "realmAttributes"
) )
> >
<#local out_seq += ["/*If you need '" + path?join(".") + "." + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]> <#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
<#continue> <#continue>
</#if> </#if>
<#-- https://github.com/keycloakify/keycloakify/discussions/406 --> <#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if ( <#if (
["register.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) && ["register.ftl", "register-user-profile.ftl", "info.ftl", "login.ftl", "login-update-password.ftl", "login-oauth2-device-verify-user-code.ftl"]?seq_contains(pageId) &&
key == "attemptedUsername" && are_same_path(path, ["auth"]) key == "attemptedUsername" && are_same_path(path, ["auth"])
)> )>
<#attempt> <#attempt>
<#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 --> <#-- https://github.com/keycloak/keycloak/blob/3a2bf0c04bcde185e497aaa32d0bb7ab7520cf4a/themes/src/main/resources/theme/base/login/template.ftl#L63 -->
<#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())> <#if !(auth?has_content && auth.showUsername() && !auth.showResetCredentials())>
<#local out_seq += ["/*If you need '" + key + "' on " + pageId + ", please submit an issue to the Keycloakify repo*/"]> <#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
<#continue> <#continue>
</#if> </#if>
<#recover> <#recover>
<#local out_seq += ["/*Testing if attemptedUsername should be skipped throwed an exception */"]> <#local out_seq += ["/*Accessing attemptedUsername throwed an exception */"]>
</#attempt> </#attempt>
</#if> </#if>
@ -459,6 +367,26 @@
</#attempt> </#attempt>
</#if> </#if>
<#if are_same_path(path, ["url", "getLogoutUrl"])>
<#local returnValue = "">
<#attempt>
<#local returnValue = url.getLogoutUrl()>
<#recover>
<#return "ABORT: Couldn't evaluate url.getLogoutUrl()">
</#attempt>
<#return 'function(){ return "' + returnValue + '"; }'>
</#if>
<#if are_same_path(path, ["totp", "policy", "getAlgorithmKey"])>
<#local returnValue = "">
<#attempt>
<#local returnValue = totp.policy.getAlgorithmKey()>
<#recover>
<#return "ABORT: Couldn't evaluate totp.policy.getAlgorithmKey()">
</#attempt>
<#return 'function(){ return "' + returnValue + '"; }'>
</#if>
<#return "ABORT: It's a method"> <#return "ABORT: It's a method">
</#if> </#if>

View File

@ -108,7 +108,7 @@ export function generateFtlFilesCodeFactory(params: {
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }'; const ftlObjectToJsCodeDeclaringAnObjectPlaceholder = '{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';
$("head").prepend(["<script>", ` window.${nameOfTheGlobal}= ${ftlObjectToJsCodeDeclaringAnObjectPlaceholder};`, "</script>"].join("\n")); $("head").prepend(`<script>\nwindow.${nameOfTheGlobal}=${ftlObjectToJsCodeDeclaringAnObjectPlaceholder}</script>`);
// Remove part of the document marked as ignored. // Remove part of the document marked as ignored.
{ {

View File

@ -2,9 +2,11 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path"; import { join as pathJoin, relative as pathRelative, basename as pathBasename } from "path";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { BuildOptions } from "./buildOptions"; import type { BuildOptions } from "./buildOptions";
import { accountV1ThemeName } from "../constants";
export type BuildOptionsLike = { export type BuildOptionsLike = {
keycloakifyBuildDirPath: string; keycloakifyBuildDirPath: string;
themeNames: string[];
}; };
assert<BuildOptions extends BuildOptionsLike ? true : false>(); assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -15,11 +17,14 @@ const containerName = "keycloak-testing-container";
const keycloakVersion = "24.0.4"; const keycloakVersion = "24.0.4";
/** Files for being able to run a hot reload keycloak container */ /** Files for being able to run a hot reload keycloak container */
export function generateStartKeycloakTestingContainer(params: { jarFilePath: string; buildOptions: BuildOptionsLike }) { export function generateStartKeycloakTestingContainer(params: {
const { jarFilePath, buildOptions } = params; jarFilePath: string;
doesImplementAccountTheme: boolean;
buildOptions: BuildOptionsLike;
}) {
const { jarFilePath, doesImplementAccountTheme, buildOptions } = params;
const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme"); const themeRelativeDirPath = pathJoin("src", "main", "resources", "theme");
const themeDirPath = pathJoin(buildOptions.keycloakifyBuildDirPath, themeRelativeDirPath);
fs.writeFileSync( fs.writeFileSync(
pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename), pathJoin(buildOptions.keycloakifyBuildDirPath, generateStartKeycloakTestingContainer.basename),
@ -40,18 +45,12 @@ export function generateStartKeycloakTestingContainer(params: { jarFilePath: str
"$(pwd)", "$(pwd)",
pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath) pathRelative(buildOptions.keycloakifyBuildDirPath, jarFilePath)
)}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`, )}":"/opt/keycloak/providers/${pathBasename(jarFilePath)}" \\`,
...fs [...(doesImplementAccountTheme ? [accountV1ThemeName] : []), ...buildOptions.themeNames].map(
.readdirSync(themeDirPath)
.filter(name => fs.lstatSync(pathJoin(themeDirPath, name)).isDirectory())
.map(
themeName => themeName =>
` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace( ` -v "${pathJoin("$(pwd)", themeRelativeDirPath, themeName).replace(/\\/g, "/")}":"/opt/keycloak/themes/${themeName}":rw \\`
/\\/g,
"/"
)}":"/opt/keycloak/themes/${themeName}":rw \\`
), ),
` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`, ` -it quay.io/keycloak/keycloak:${keycloakVersion} \\`,
` start-dev --features=declarative-user-profile`, ` start-dev`,
"" ""
].join("\n"), ].join("\n"),
"utf8" "utf8"

View File

@ -10,7 +10,7 @@ export function generateThemeVariations(params: { themeName: string; themeVarian
transformCodebase({ transformCodebase({
"srcDirPath": mainThemeDirPath, "srcDirPath": mainThemeDirPath,
"destDirPath": pathJoin(mainThemeDirPath, themeVariantName), "destDirPath": pathJoin(mainThemeDirPath, "..", themeVariantName),
"transformSourceCode": ({ fileRelativePath, sourceCode }) => { "transformSourceCode": ({ fileRelativePath, sourceCode }) => {
if (pathExtname(fileRelativePath) === ".ftl" && fileRelativePath.split(pathSep).length === 2) { if (pathExtname(fileRelativePath) === ".ftl" && fileRelativePath.split(pathSep).length === 2) {
const modifiedSourceCode = Buffer.from( const modifiedSourceCode = Buffer.from(

View File

@ -36,8 +36,6 @@ export function readFieldNameUsage(params: { keycloakifySrcDirPath: string; them
return p1; return p1;
}) })
.map(part => { .map(part => {
console.log(part);
return part return part
.split(",") .split(",")
.map(a => a.trim()) .map(a => a.trim())

View File

@ -57,6 +57,7 @@ export async function main() {
generateStartKeycloakTestingContainer({ generateStartKeycloakTestingContainer({
"jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, lastJarFileBasename), "jarFilePath": pathJoin(buildOptions.keycloakifyBuildDirPath, lastJarFileBasename),
doesImplementAccountTheme,
buildOptions buildOptions
}); });

View File

@ -32,7 +32,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
const { getClassName } = useGetClassName({ doUseDefaultCss, classes }); const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, msgStr, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, msgStr, getChangeLocalUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext;
@ -126,31 +126,17 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div className={getClassName("kcFormCardClass")}> <div className={getClassName("kcFormCardClass")}>
<header className={getClassName("kcFormHeaderClass")}> <header className={getClassName("kcFormHeaderClass")}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && ( {realm.internationalizationEnabled && (assert(locale !== undefined), locale.supported.length > 1) && (
<div className={getClassName("kcLocaleMainClass")} id="kc-locale"> <div className={getClassName("kcLocaleMainClass")} id="kc-locale">
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}> <div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div id="kc-locale-dropdown" className={clsx("menu-button-links", getClassName("kcLocaleDropDownClass"))}> <div id="kc-locale-dropdown" className={clsx("menu-button-links", getClassName("kcLocaleDropDownClass"))}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
</a>
<ul>
{locale.supported.map(({ languageTag }) => (
<li key={languageTag} className="kc-dropdown-item">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" onClick={() => changeLocale(languageTag)}>
{labelBySupportedLanguageTag[languageTag]}
</a>
</li>
))}
</ul>
<button <button
tabIndex={1} tabIndex={1}
id="kc-current-locale-link" id="kc-current-locale-link"
aria-label={msgStr("languages" as any)} aria-label={msgStr("languages" as any)}
aria-haspopup={true} aria-haspopup="true"
aria-expanded={false} aria-expanded="false"
aria-controls="language-switch1" aria-controls="language-switch1"
> >
{labelBySupportedLanguageTag[currentLanguageTag]} {labelBySupportedLanguageTag[currentLanguageTag]}
@ -165,13 +151,11 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
> >
{locale.supported.map(({ languageTag }, i) => ( {locale.supported.map(({ languageTag }, i) => (
<li key={languageTag} className={getClassName("kcLocaleListItemClass")} role="none"> <li key={languageTag} className={getClassName("kcLocaleListItemClass")} role="none">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a <a
role="menuitem" role="menuitem"
id={`language-${i}`} id={`language-${i + 1}`}
className={getClassName("kcLocaleItemClass")} className={getClassName("kcLocaleItemClass")}
href="#" href={getChangeLocalUrl(languageTag)}
onClick={() => changeLocale(languageTag)}
> >
{labelBySupportedLanguageTag[languageTag]} {labelBySupportedLanguageTag[languageTag]}
</a> </a>

View File

@ -28,11 +28,10 @@ export type GenericI18n<MessageKey extends string> = {
*/ */
currentLanguageTag: string; currentLanguageTag: string;
/** /**
* To call when the user switch language. * Redirect to this url to change the language.
* This will cause the page to be reloaded, * After reload currentLanguageTag === newLanguageTag
* on next load currentLanguageTag === newLanguageTag
*/ */
changeLocale: (newLanguageTag: string) => never; getChangeLocalUrl: (newLanguageTag: string) => string;
/** /**
* e.g. "en" => "English", "fr" => "Français", ... * e.g. "en" => "English", "fr" => "Français", ...
* *
@ -104,7 +103,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
} as any } as any
}), }),
currentLanguageTag, currentLanguageTag,
"changeLocale": newLanguageTag => { "getChangeLocalUrl": newLanguageTag => {
const { locale } = kcContext; const { locale } = kcContext;
assert(locale !== undefined, "Internationalization not enabled"); assert(locale !== undefined, "Internationalization not enabled");
@ -113,9 +112,7 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`); assert(targetSupportedLocale !== undefined, `${newLanguageTag} need to be enabled in Keycloak admin`);
window.location.href = targetSupportedLocale.url; return targetSupportedLocale.url;
assert(false, "never");
}, },
"labelBySupportedLanguageTag": Object.fromEntries( "labelBySupportedLanguageTag": Object.fromEntries(
(kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label]) (kcContext.locale?.supported ?? []).map(({ languageTag, label }) => [languageTag, label])

View File

@ -419,6 +419,7 @@ export declare namespace KcContext {
algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512"; algorithm: "HmacSHA1" | "HmacSHA256" | "HmacSHA512";
digits: number; digits: number;
lookAheadWindow: number; lookAheadWindow: number;
getAlgorithmKey: () => string;
} & ( } & (
| { | {
type: "totp"; type: "totp";

View File

@ -443,7 +443,8 @@ export const kcContextMocks = [
"digits": 6, "digits": 6,
"lookAheadWindow": 1, "lookAheadWindow": 1,
"type": "totp", "type": "totp",
"period": 30 "period": 30,
"getAlgorithmKey": () => "SHA1"
} }
} }
}), }),

View File

@ -24,7 +24,7 @@ export default function Code(props: PageProps<Extract<KcContext, { pageId: "code
{code.success ? ( {code.success ? (
<> <>
<p>{msg("copyCodeInstruction")}</p> <p>{msg("copyCodeInstruction")}</p>
<input id="code" className={getClassName("kcTextareaClass")} value={code.code} /> <input id="code" className={getClassName("kcTextareaClass")} defaultValue={code.code} />
</> </>
) : ( ) : (
<p id="error">{code.error}</p> <p id="error">{code.error}</p>

View File

@ -29,7 +29,7 @@ export default function IdpReviewUserProfile(props: IdpReviewUserProfileProps) {
<Template <Template
{...{ kcContext, i18n, doUseDefaultCss, classes }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true} displayRequiredFields
headerNode={msg("loginIdpReviewProfileTitle")} headerNode={msg("loginIdpReviewProfileTitle")}
> >
<form id="kc-idp-review-profile-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post"> <form id="kc-idp-review-profile-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">

View File

@ -104,7 +104,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
id="username" id="username"
className={getClassName("kcInputClass")} className={getClassName("kcInputClass")}
name="username" name="username"
value={login.username ?? ""} defaultValue={login.username ?? ""}
type="text" type="text"
autoFocus autoFocus
autoComplete="username" autoComplete="username"

View File

@ -17,14 +17,6 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
const { msg, msgStr } = i18n; const { msg, msgStr } = i18n;
// NOTE: We should edit the ftl_object_to_js_code_declaring_an_object.ftl
// so that we have access to getAlgorithmKey()
const algToKeyUriAlg: Record<(typeof kcContext)["totp"]["policy"]["algorithm"], string> = {
"HmacSHA1": "SHA1",
"HmacSHA256": "SHA256",
"HmacSHA512": "SHA512"
};
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("loginTotpTitle")}> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("loginTotpTitle")}>
<> <>
@ -60,7 +52,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
{msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)} {msg("loginTotpType")}: {msg(`loginTotp.${totp.policy.type}`)}
</li> </li>
<li id="kc-totp-algorithm"> <li id="kc-totp-algorithm">
{msg("loginTotpAlgorithm")}: {algToKeyUriAlg?.[totp.policy.algorithm] ?? totp.policy.algorithm} {msg("loginTotpAlgorithm")}: {totp.policy.getAlgorithmKey()}
</li> </li>
<li id="kc-totp-digits"> <li id="kc-totp-digits">
{msg("loginTotpDigits")}: {totp.policy.digits} {msg("loginTotpDigits")}: {totp.policy.digits}

View File

@ -26,7 +26,7 @@ export default function LoginUpdateProfile(props: LoginUpdateProfileProps) {
const [isFormSubmittable, setIsFormSubmittable] = useState(false); const [isFormSubmittable, setIsFormSubmittable] = useState(false);
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("loginProfileTitle")}> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} displayRequiredFields headerNode={msg("loginProfileTitle")}>
<form id="kc-update-profile-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post"> <form id="kc-update-profile-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">
<UserProfileFormFields <UserProfileFormFields
{...{ {...{

View File

@ -101,7 +101,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
id="username" id="username"
className={getClassName("kcInputClass")} className={getClassName("kcInputClass")}
name="username" name="username"
value={login.username ?? ""} defaultValue={login.username ?? ""}
type="text" type="text"
autoFocus autoFocus
autoComplete="off" autoComplete="off"

View File

@ -28,7 +28,7 @@ export default function Register(props: RegisterProps) {
const [isFormSubmittable, setIsFormSubmittable] = useState(false); const [isFormSubmittable, setIsFormSubmittable] = useState(false);
return ( return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}> <Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")} displayRequiredFields>
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post"> <form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<UserProfileFormFields <UserProfileFormFields
{...{ {...{
@ -85,7 +85,7 @@ export default function Register(props: RegisterProps) {
function TermsAcceptance(props: { function TermsAcceptance(props: {
i18n: I18n; i18n: I18n;
getClassName: ReturnType<typeof useGetClassName>["getClassName"]; getClassName: ReturnType<typeof useGetClassName>["getClassName"];
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">; messagesPerField: Pick<KcContext["messagesPerField"], "existsError" | "get">;
}) { }) {
const { i18n, getClassName, messagesPerField } = props; const { i18n, getClassName, messagesPerField } = props;

View File

@ -29,7 +29,7 @@ export default function UpdateEmail(props: UpdateEmailProps) {
<Template <Template
{...{ kcContext, i18n, doUseDefaultCss, classes }} {...{ kcContext, i18n, doUseDefaultCss, classes }}
displayMessage={messagesPerField.exists("global")} displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true} displayRequiredFields
headerNode={msg("updateEmailTitle")} headerNode={msg("updateEmailTitle")}
> >
<form id="kc-update-email-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post"> <form id="kc-update-email-form" className={getClassName("kcFormClass")} action={url.loginAction} method="post">

View File

@ -275,7 +275,7 @@ function LogoutOtherSessions(props: { i18n: I18n; getClassName: ReturnType<typeo
<div className={getClassName("kcFormOptionsWrapperClass")}> <div className={getClassName("kcFormOptionsWrapperClass")}>
<div className="checkbox"> <div className="checkbox">
<label> <label>
<input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked /> <input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" defaultChecked={true} />
{msg("logoutOtherSessions")} {msg("logoutOtherSessions")}
</label> </label>
</div> </div>

View File

@ -5,8 +5,8 @@ import { useI18n } from "./i18n";
import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms"; import { useDownloadTerms } from "../../dist/login/lib/useDownloadTerms";
import tos_en_url from "./tos_en.md"; import tos_en_url from "./tos_en.md";
import tos_fr_url from "./tos_fr.md"; import tos_fr_url from "./tos_fr.md";
import Template from "../../dist/login/Template";
const DefaultTemplate = lazy(() => import("../../dist/login/Template")); import UserProfileFormFields from "../../dist/login/UserProfileFormFields";
export default function KcApp(props: { kcContext: KcContext }) { export default function KcApp(props: { kcContext: KcContext }) {
const { kcContext } = props; const { kcContext } = props;
@ -42,7 +42,7 @@ export default function KcApp(props: { kcContext: KcContext }) {
{(() => { {(() => {
switch (kcContext.pageId) { switch (kcContext.pageId) {
default: default:
return <Fallback {...{ kcContext, i18n }} Template={DefaultTemplate} doUseDefaultCss={true} />; return <Fallback {...{ kcContext, i18n, Template, UserProfileFormFields }} doUseDefaultCss={true} />;
} }
})()} })()}
</Suspense> </Suspense>

View File

@ -1,24 +0,0 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "register-user-profile.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;

View File

@ -1,24 +0,0 @@
import React from "react";
import type { ComponentMeta } from "@storybook/react";
import { createPageStory } from "../createPageStory";
const pageId = "update-user-profile.ftl";
const { PageStory } = createPageStory({ pageId });
const meta: ComponentMeta<any> = {
title: `login/${pageId}`,
component: PageStory,
parameters: {
viewMode: "story",
previewTabs: {
"storybook/docs/panel": {
"hidden": true
}
}
}
};
export default meta;
export const Default = () => <PageStory />;