This commit is contained in:
parent
7260589136
commit
c6b52acf2f
@ -1,7 +1,7 @@
|
|||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic";
|
||||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||||
import { LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
import { LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants";
|
||||||
|
|
||||||
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
export async function copyKeycloakResourcesToStorybookStaticDir() {
|
||||||
await copyKeycloakResourcesToPublic({
|
await copyKeycloakResourcesToPublic({
|
||||||
@ -11,7 +11,7 @@ export async function copyKeycloakResourcesToStorybookStaticDir() {
|
|||||||
npmConfigGetCwd: pathJoin(__dirname, "..")
|
npmConfigGetCwd: pathJoin(__dirname, "..")
|
||||||
}),
|
}),
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
|
||||||
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,10 @@ import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloak
|
|||||||
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
|
||||||
import { deepAssign } from "../src/tools/deepAssign";
|
import { deepAssign } from "../src/tools/deepAssign";
|
||||||
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
||||||
|
import {
|
||||||
|
THEME_TYPES,
|
||||||
|
LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1
|
||||||
|
} from "../src/bin/shared/constants";
|
||||||
|
|
||||||
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
|
||||||
// update the version array for generating for newer version.
|
// update the version array for generating for newer version.
|
||||||
@ -21,12 +25,22 @@ import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions";
|
|||||||
const propertiesParser = require("properties-parser");
|
const propertiesParser = require("properties-parser");
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const keycloakVersion = "24.0.4";
|
|
||||||
|
|
||||||
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
const thisCodebaseRootDirPath = getThisCodebaseRootDirPath();
|
||||||
|
|
||||||
|
type Dictionary = { [idiomId: string]: string };
|
||||||
|
|
||||||
|
const record: { [themeType: string]: { [language: string]: Dictionary } } = {};
|
||||||
|
|
||||||
|
for (const themeType of THEME_TYPES) {
|
||||||
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({
|
||||||
keycloakVersion,
|
keycloakVersion: (() => {
|
||||||
|
switch (themeType) {
|
||||||
|
case "login":
|
||||||
|
return "25.0.4";
|
||||||
|
case "account":
|
||||||
|
return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1;
|
||||||
|
}
|
||||||
|
})(),
|
||||||
buildContext: {
|
buildContext: {
|
||||||
cacheDirPath: pathJoin(
|
cacheDirPath: pathJoin(
|
||||||
thisCodebaseRootDirPath,
|
thisCodebaseRootDirPath,
|
||||||
@ -40,10 +54,6 @@ async function main() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
type Dictionary = { [idiomId: string]: string };
|
|
||||||
|
|
||||||
const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {};
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||||
const re = new RegExp(
|
const re = new RegExp(
|
||||||
@ -60,9 +70,14 @@ async function main() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [, typeOfPage, language] = match;
|
const [, themeType_here, language] = match;
|
||||||
|
|
||||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
if (themeType_here !== themeType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
|
||||||
|
Object.fromEntries(
|
||||||
Object.entries(
|
Object.entries(
|
||||||
propertiesParser.parse(
|
propertiesParser.parse(
|
||||||
fs
|
fs
|
||||||
@ -75,16 +90,14 @@ async function main() {
|
|||||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||||
value
|
value
|
||||||
])
|
])
|
||||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
.map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
key === "termsText" ? "" : value
|
||||||
|
])
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(record).forEach(themeType => {
|
|
||||||
if (themeType !== "login" && themeType !== "account") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const recordForThemeType = record[themeType];
|
const recordForThemeType = record[themeType];
|
||||||
|
|
||||||
const languages = Object.keys(recordForThemeType);
|
const languages = Object.keys(recordForThemeType);
|
||||||
@ -99,6 +112,29 @@ async function main() {
|
|||||||
assert(false);
|
assert(false);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
/* Migration helper
|
||||||
|
|
||||||
|
console.log({ themeType });
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
const all = new Set<string>();
|
||||||
|
|
||||||
|
languages.forEach(languages => all.add(languages));
|
||||||
|
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
|
||||||
|
currentlySupportedLanguages.forEach(languages => all.add(languages));
|
||||||
|
|
||||||
|
all.forEach(language => {
|
||||||
|
console.log([
|
||||||
|
`"${language}": `,
|
||||||
|
`isInLanguages: ${languages.includes(language)}`,
|
||||||
|
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
|
||||||
|
].join(" "))
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||||
takeIntoAccountArraysOrdering: false
|
takeIntoAccountArraysOrdering: false
|
||||||
@ -180,7 +216,7 @@ async function main() {
|
|||||||
"utf8"
|
"utf8"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const keycloakifyExtraMessages_login: Record<
|
const keycloakifyExtraMessages_login: Record<
|
||||||
@ -203,6 +239,7 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
| "nl"
|
| "nl"
|
||||||
| "no"
|
| "no"
|
||||||
| "pl"
|
| "pl"
|
||||||
|
| "pt"
|
||||||
| "pt-BR"
|
| "pt-BR"
|
||||||
| "ru"
|
| "ru"
|
||||||
| "sk"
|
| "sk"
|
||||||
@ -210,7 +247,9 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
| "th"
|
| "th"
|
||||||
| "tr"
|
| "tr"
|
||||||
| "uk"
|
| "uk"
|
||||||
| "zh-CN",
|
| "ka"
|
||||||
|
| "zh-CN"
|
||||||
|
| "zh-TW",
|
||||||
Record<
|
Record<
|
||||||
| "shouldBeEqual"
|
| "shouldBeEqual"
|
||||||
| "shouldBeDifferent"
|
| "shouldBeDifferent"
|
||||||
@ -434,6 +473,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
addValue: "Dodaj wartość",
|
addValue: "Dodaj wartość",
|
||||||
languages: "Języki"
|
languages: "Języki"
|
||||||
},
|
},
|
||||||
|
pt: {
|
||||||
|
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||||
|
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||||
|
shouldMatchPattern: "O padrão deve corresponder: `/{0}/`",
|
||||||
|
mustBeAnInteger: "Deve ser um número inteiro",
|
||||||
|
notAValidOption: "Não é uma opção válida",
|
||||||
|
selectAnOption: "Selecione uma opção",
|
||||||
|
remove: "Remover",
|
||||||
|
addValue: "Adicionar valor",
|
||||||
|
languages: "Idiomas"
|
||||||
|
},
|
||||||
"pt-BR": {
|
"pt-BR": {
|
||||||
shouldBeEqual: "{0} deve ser igual a {1}",
|
shouldBeEqual: "{0} deve ser igual a {1}",
|
||||||
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
shouldBeDifferent: "{0} deve ser diferente de {1}",
|
||||||
@ -511,6 +561,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
addValue: "Додати значення",
|
addValue: "Додати значення",
|
||||||
languages: "Мови"
|
languages: "Мови"
|
||||||
},
|
},
|
||||||
|
ka: {
|
||||||
|
shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის",
|
||||||
|
shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის",
|
||||||
|
shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`",
|
||||||
|
mustBeAnInteger: "უნდა იყოს მთელი რიცხვი",
|
||||||
|
notAValidOption: "არასწორი ვარიანტი",
|
||||||
|
selectAnOption: "აირჩიეთ ვარიანტი",
|
||||||
|
remove: "წაშალეთ",
|
||||||
|
addValue: "დაამატეთ მნიშვნელობა",
|
||||||
|
languages: "ენები"
|
||||||
|
},
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
shouldBeEqual: "{0} 应该等于 {1}",
|
shouldBeEqual: "{0} 应该等于 {1}",
|
||||||
shouldBeDifferent: "{0} 应该不同于 {1}",
|
shouldBeDifferent: "{0} 应该不同于 {1}",
|
||||||
@ -521,6 +582,17 @@ const keycloakifyExtraMessages_login: Record<
|
|||||||
remove: "移除",
|
remove: "移除",
|
||||||
addValue: "添加值",
|
addValue: "添加值",
|
||||||
languages: "语言"
|
languages: "语言"
|
||||||
|
},
|
||||||
|
"zh-TW": {
|
||||||
|
shouldBeEqual: "{0} 應該等於 {1}",
|
||||||
|
shouldBeDifferent: "{0} 應該不同於 {1}",
|
||||||
|
shouldMatchPattern: "模式應匹配: `/{0}/`",
|
||||||
|
mustBeAnInteger: "必須是整數",
|
||||||
|
notAValidOption: "不是有效選項",
|
||||||
|
selectAnOption: "選擇一個選項",
|
||||||
|
remove: "移除",
|
||||||
|
addValue: "添加值",
|
||||||
|
languages: "語言"
|
||||||
}
|
}
|
||||||
/* spell-checker: enable */
|
/* spell-checker: enable */
|
||||||
};
|
};
|
||||||
@ -532,9 +604,7 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
| "cs"
|
| "cs"
|
||||||
| "da"
|
| "da"
|
||||||
| "de"
|
| "de"
|
||||||
| "el"
|
|
||||||
| "es"
|
| "es"
|
||||||
| "fa"
|
|
||||||
| "fi"
|
| "fi"
|
||||||
| "fr"
|
| "fr"
|
||||||
| "hu"
|
| "hu"
|
||||||
@ -549,9 +619,7 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
| "ru"
|
| "ru"
|
||||||
| "sk"
|
| "sk"
|
||||||
| "sv"
|
| "sv"
|
||||||
| "th"
|
|
||||||
| "tr"
|
| "tr"
|
||||||
| "uk"
|
|
||||||
| "zh-CN",
|
| "zh-CN",
|
||||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||||
> = {
|
> = {
|
||||||
@ -580,18 +648,10 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden",
|
||||||
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein"
|
||||||
},
|
},
|
||||||
el: {
|
|
||||||
newPasswordSameAsOld: "Ο νέος κωδικός πρόσβασης πρέπει να διαφέρει από τον παλιό",
|
|
||||||
passwordConfirmNotMatch: "Η επιβεβαίωση του κωδικού πρόσβασης δεν ταιριάζει"
|
|
||||||
},
|
|
||||||
es: {
|
es: {
|
||||||
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior",
|
||||||
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
passwordConfirmNotMatch: "La confirmación de la contraseña no coincide"
|
||||||
},
|
},
|
||||||
fa: {
|
|
||||||
newPasswordSameAsOld: "رمز عبور جدید باید با رمز عبور قبلی متفاوت باشد",
|
|
||||||
passwordConfirmNotMatch: "تأیید رمز عبور مطابقت ندارد"
|
|
||||||
},
|
|
||||||
fi: {
|
fi: {
|
||||||
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha",
|
||||||
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
passwordConfirmNotMatch: "Salasanan vahvistus ei täsmää"
|
||||||
@ -649,18 +709,10 @@ const keycloakifyExtraMessages_account: Record<
|
|||||||
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
newPasswordSameAsOld: "Det nya lösenordet måste skilja sig från det gamla",
|
||||||
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte"
|
||||||
},
|
},
|
||||||
th: {
|
|
||||||
newPasswordSameAsOld: "รหัสผ่านใหม่ต้องต่างจากรหัสผ่านเดิม",
|
|
||||||
passwordConfirmNotMatch: "การยืนยันรหัสผ่านไม่ตรงกัน"
|
|
||||||
},
|
|
||||||
tr: {
|
tr: {
|
||||||
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır",
|
||||||
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor"
|
||||||
},
|
},
|
||||||
uk: {
|
|
||||||
newPasswordSameAsOld: "Новий пароль повинен відрізнятися від старого",
|
|
||||||
passwordConfirmNotMatch: "Підтвердження пароля не співпадає"
|
|
||||||
},
|
|
||||||
"zh-CN": {
|
"zh-CN": {
|
||||||
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
newPasswordSameAsOld: "新密码必须与旧密码不同",
|
||||||
passwordConfirmNotMatch: "密码确认不匹配"
|
passwordConfirmNotMatch: "密码确认不匹配"
|
||||||
|
@ -15,7 +15,7 @@ import * as child_process from "child_process";
|
|||||||
import {
|
import {
|
||||||
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES,
|
||||||
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
|
BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME,
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
import type { KeycloakVersionRange } from "./KeycloakVersionRange";
|
||||||
import { exclude } from "tsafe";
|
import { exclude } from "tsafe";
|
||||||
@ -547,7 +547,7 @@ export function getBuildContext(params: {
|
|||||||
`${themeNames[0]}-keycloak-theme`,
|
`${themeNames[0]}-keycloak-theme`,
|
||||||
loginThemeResourcesFromKeycloakVersion:
|
loginThemeResourcesFromKeycloakVersion:
|
||||||
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
buildOptions.loginThemeResourcesFromKeycloakVersion ??
|
||||||
LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT,
|
LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT,
|
||||||
projectDirPath,
|
projectDirPath,
|
||||||
projectBuildDirPath,
|
projectBuildDirPath,
|
||||||
keycloakifyBuildDirPath: (() => {
|
keycloakifyBuildDirPath: (() => {
|
||||||
|
@ -50,7 +50,9 @@ export const LOGIN_THEME_PAGE_IDS = [
|
|||||||
"login-recovery-authn-code-input.ftl",
|
"login-recovery-authn-code-input.ftl",
|
||||||
"login-reset-otp.ftl",
|
"login-reset-otp.ftl",
|
||||||
"login-x509-info.ftl",
|
"login-x509-info.ftl",
|
||||||
"webauthn-error.ftl"
|
"webauthn-error.ftl",
|
||||||
|
"login-passkeys-conditional-authenticate.ftl",
|
||||||
|
"login-idp-link-confirm-override.ftl"
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const ACCOUNT_THEME_PAGE_IDS = [
|
export const ACCOUNT_THEME_PAGE_IDS = [
|
||||||
@ -70,4 +72,4 @@ export const CONTAINER_NAME = "keycloak-keycloakify";
|
|||||||
|
|
||||||
export const FALLBACK_LANGUAGE_TAG = "en";
|
export const FALLBACK_LANGUAGE_TAG = "en";
|
||||||
|
|
||||||
export const LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
export const LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { join as pathJoin, relative as pathRelative } from "path";
|
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
|
||||||
import { type BuildContext } from "./buildContext";
|
import { type BuildContext } from "../buildContext";
|
||||||
import { assert } from "tsafe/assert";
|
import { assert } from "tsafe/assert";
|
||||||
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "./constants";
|
import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants";
|
||||||
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
|
import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive";
|
||||||
|
import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath";
|
||||||
|
import * as fsPr from "fs/promises";
|
||||||
|
|
||||||
export type BuildContextLike = {
|
export type BuildContextLike = {
|
||||||
cacheDirPath: string;
|
cacheDirPath: string;
|
||||||
@ -20,6 +22,8 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
||||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||||
|
|
||||||
|
let areExtraAssetsFor24Copied = false;
|
||||||
|
|
||||||
const { extractedDirPath } = await downloadAndExtractArchive({
|
const { extractedDirPath } = await downloadAndExtractArchive({
|
||||||
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
|
||||||
cacheDirPath: buildContext.cacheDirPath,
|
cacheDirPath: buildContext.cacheDirPath,
|
||||||
@ -32,8 +36,6 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { readFile, writeFile } = params;
|
|
||||||
|
|
||||||
skip_keycloak_v2: {
|
skip_keycloak_v2: {
|
||||||
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
||||||
break skip_keycloak_v2;
|
break skip_keycloak_v2;
|
||||||
@ -42,6 +44,8 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { readFile, writeFile } = params;
|
||||||
|
|
||||||
last_account_v1_transformations: {
|
last_account_v1_transformations: {
|
||||||
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
||||||
break last_account_v1_transformations;
|
break last_account_v1_transformations;
|
||||||
@ -168,6 +172,42 @@ export async function downloadKeycloakDefaultTheme(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
copy_extra_assets: {
|
||||||
|
if (keycloakVersion !== "24.0.4") {
|
||||||
|
break copy_extra_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (areExtraAssetsFor24Copied) {
|
||||||
|
break copy_extra_assets;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extraAssetsDirPath = pathJoin(
|
||||||
|
getThisCodebaseRootDirPath(),
|
||||||
|
"src",
|
||||||
|
"bin",
|
||||||
|
__dirname.split(`${pathSep}bin${pathSep}`)[1],
|
||||||
|
"extra-assets"
|
||||||
|
);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
["webauthnAuthenticate.js", "passkeysConditionalAuth.js"].map(
|
||||||
|
async fileBasename =>
|
||||||
|
writeFile({
|
||||||
|
fileRelativePath: pathJoin(
|
||||||
|
"base",
|
||||||
|
"login",
|
||||||
|
"resources",
|
||||||
|
"js",
|
||||||
|
fileBasename
|
||||||
|
),
|
||||||
|
modifiedData: await fsPr.readFile(
|
||||||
|
pathJoin(extraAssetsDirPath, fileBasename)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
skip_unused_resources: {
|
skip_unused_resources: {
|
||||||
if (keycloakVersion !== "24.0.4") {
|
if (keycloakVersion !== "24.0.4") {
|
||||||
break skip_unused_resources;
|
break skip_unused_resources;
|
@ -0,0 +1,79 @@
|
|||||||
|
import { base64url } from "rfc4648";
|
||||||
|
import { returnSuccess, returnFailure } from "./webauthnAuthenticate.js";
|
||||||
|
|
||||||
|
export function initAuthenticate(input) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(input.errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (input.isUserIdentified || typeof PublicKeyCredential.isConditionalMediationAvailable === "undefined") {
|
||||||
|
document.getElementById("kc-form-passkey-button").style.display = 'block';
|
||||||
|
} else {
|
||||||
|
tryAutoFillUI(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAuthenticate(input) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(input.errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
rpId : input.rpId,
|
||||||
|
challenge: base64url.parse(input.challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
publicKey.allowCredentials = !input.isUserIdentified ? [] : getAllowCredentials();
|
||||||
|
|
||||||
|
if (input.createTimeout !== 0) {
|
||||||
|
publicKey.timeout = input.createTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.userVerification !== 'not specified') {
|
||||||
|
publicKey.userVerification = input.userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator.credentials.get({
|
||||||
|
publicKey: publicKey,
|
||||||
|
...input.additionalOptions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryAutoFillUI(input) {
|
||||||
|
const isConditionalMediationAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
|
||||||
|
if (isConditionalMediationAvailable) {
|
||||||
|
document.getElementById("kc-form-login").style.display = "block";
|
||||||
|
input.additionalOptions = { mediation: 'conditional'};
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate(input);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
document.getElementById("kc-form-passkey-button").style.display = 'block';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllowCredentials() {
|
||||||
|
const allowCredentials = [];
|
||||||
|
const authnUse = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authnUse !== undefined) {
|
||||||
|
if (authnUse.length === undefined) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(authnUse.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authnUse.forEach((entry) =>
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(entry.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allowCredentials;
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
import { base64url } from "rfc4648";
|
||||||
|
|
||||||
|
export async function authenticateByWebAuthn(input) {
|
||||||
|
if (!input.isUserIdentified) {
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate([], input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
checkAllowCredentials(input.challenge, input.userVerification, input.rpId, input.createTimeout, input.errmsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllowCredentials(challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||||
|
const allowCredentials = [];
|
||||||
|
const authnUse = document.forms['authn_select'].authn_use_chk;
|
||||||
|
if (authnUse !== undefined) {
|
||||||
|
if (authnUse.length === undefined) {
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(authnUse.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
authnUse.forEach((entry) =>
|
||||||
|
allowCredentials.push({
|
||||||
|
id: base64url.parse(entry.value, {loose: true}),
|
||||||
|
type: 'public-key',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg);
|
||||||
|
returnSuccess(result);
|
||||||
|
} catch (error) {
|
||||||
|
returnFailure(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function doAuthenticate(allowCredentials, challenge, userVerification, rpId, createTimeout, errmsg) {
|
||||||
|
// Check if WebAuthn is supported by this browser
|
||||||
|
if (!window.PublicKeyCredential) {
|
||||||
|
returnFailure(errmsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicKey = {
|
||||||
|
rpId : rpId,
|
||||||
|
challenge: base64url.parse(challenge, { loose: true })
|
||||||
|
};
|
||||||
|
|
||||||
|
if (createTimeout !== 0) {
|
||||||
|
publicKey.timeout = createTimeout * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowCredentials.length) {
|
||||||
|
publicKey.allowCredentials = allowCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userVerification !== 'not specified') {
|
||||||
|
publicKey.userVerification = userVerification;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigator.credentials.get({publicKey});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function returnSuccess(result) {
|
||||||
|
document.getElementById("clientDataJSON").value = base64url.stringify(new Uint8Array(result.response.clientDataJSON), { pad: false });
|
||||||
|
document.getElementById("authenticatorData").value = base64url.stringify(new Uint8Array(result.response.authenticatorData), { pad: false });
|
||||||
|
document.getElementById("signature").value = base64url.stringify(new Uint8Array(result.response.signature), { pad: false });
|
||||||
|
document.getElementById("credentialId").value = result.id;
|
||||||
|
if (result.response.userHandle) {
|
||||||
|
document.getElementById("userHandle").value = base64url.stringify(new Uint8Array(result.response.userHandle), { pad: false });
|
||||||
|
}
|
||||||
|
document.getElementById("webauth").submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function returnFailure(err) {
|
||||||
|
document.getElementById("error").value = err;
|
||||||
|
document.getElementById("webauth").submit();
|
||||||
|
}
|
1
src/bin/shared/downloadKeycloakDefaultTheme/index.ts
Normal file
1
src/bin/shared/downloadKeycloakDefaultTheme/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./downloadKeycloakDefaultTheme";
|
@ -40,6 +40,8 @@ const LoginRecoveryAuthnCodeInput = lazy(() => import("keycloakify/login/pages/L
|
|||||||
const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp"));
|
const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp"));
|
||||||
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info"));
|
||||||
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
const WebauthnError = lazy(() => import("keycloakify/login/pages/WebauthnError"));
|
||||||
|
const LoginPasskeysConditionalAuthenticate = lazy(() => import("keycloakify/login/pages/LoginPasskeysConditionalAuthenticate"));
|
||||||
|
const LoginIdpLinkConfirmOverride = lazy(() => import("keycloakify/login/pages/LoginIdpLinkConfirmOverride"));
|
||||||
|
|
||||||
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
type DefaultPageProps = PageProps<KcContext, I18n> & {
|
||||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||||
@ -121,6 +123,10 @@ export default function DefaultPage(props: DefaultPageProps) {
|
|||||||
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
||||||
case "webauthn-error.ftl":
|
case "webauthn-error.ftl":
|
||||||
return <WebauthnError kcContext={kcContext} {...rest} />;
|
return <WebauthnError kcContext={kcContext} {...rest} />;
|
||||||
|
case "login-passkeys-conditional-authenticate.ftl":
|
||||||
|
return <LoginPasskeysConditionalAuthenticate kcContext={kcContext} {...rest} />;
|
||||||
|
case "login-idp-link-confirm-override.ftl":
|
||||||
|
return <LoginIdpLinkConfirmOverride kcContext={kcContext} {...rest} />;
|
||||||
}
|
}
|
||||||
assert<Equals<typeof kcContext, never>>(false);
|
assert<Equals<typeof kcContext, never>>(false);
|
||||||
})()}
|
})()}
|
||||||
|
@ -59,7 +59,9 @@ export type KcContext =
|
|||||||
| KcContext.LoginRecoveryAuthnCodeInput
|
| KcContext.LoginRecoveryAuthnCodeInput
|
||||||
| KcContext.LoginResetOtp
|
| KcContext.LoginResetOtp
|
||||||
| KcContext.LoginX509Info
|
| KcContext.LoginX509Info
|
||||||
| KcContext.WebauthnError;
|
| KcContext.WebauthnError
|
||||||
|
| KcContext.LoginPasskeysConditionalAuthenticate
|
||||||
|
| KcContext.LoginIdpLinkConfirmOverride;
|
||||||
|
|
||||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
||||||
|
|
||||||
@ -577,6 +579,40 @@ export declare namespace KcContext {
|
|||||||
pageId: "webauthn-error.ftl";
|
pageId: "webauthn-error.ftl";
|
||||||
isAppInitiatedAction?: boolean;
|
isAppInitiatedAction?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginPasskeysConditionalAuthenticate = Common & {
|
||||||
|
pageId: "login-passkeys-conditional-authenticate.ftl";
|
||||||
|
realm: {
|
||||||
|
registrationAllowed: boolean;
|
||||||
|
password: boolean;
|
||||||
|
};
|
||||||
|
url: {
|
||||||
|
registrationUrl: string;
|
||||||
|
};
|
||||||
|
registrationDisabled: boolean;
|
||||||
|
isUserIdentified: boolean | "true" | "false";
|
||||||
|
challenge: string;
|
||||||
|
userVerification: string;
|
||||||
|
rpId: string;
|
||||||
|
createTimeout: number | string;
|
||||||
|
|
||||||
|
authenticators?: {
|
||||||
|
authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
|
||||||
|
};
|
||||||
|
shouldDisplayAuthenticators?: boolean;
|
||||||
|
usernameHidden?: boolean;
|
||||||
|
login: {
|
||||||
|
username?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginIdpLinkConfirmOverride = Common & {
|
||||||
|
pageId: "login-idp-link-confirm-override.ftl";
|
||||||
|
url: {
|
||||||
|
loginRestartFlowUrl: string;
|
||||||
|
};
|
||||||
|
idpDisplayName: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserProfile = {
|
export type UserProfile = {
|
||||||
|
@ -567,6 +567,39 @@ export const kcContextMocks = [
|
|||||||
pageId: "webauthn-error.ftl",
|
pageId: "webauthn-error.ftl",
|
||||||
...kcContextCommonMock,
|
...kcContextCommonMock,
|
||||||
isAppInitiatedAction: true
|
isAppInitiatedAction: true
|
||||||
|
}),
|
||||||
|
id<KcContext.LoginPasskeysConditionalAuthenticate>({
|
||||||
|
pageId: "login-passkeys-conditional-authenticate.ftl",
|
||||||
|
...kcContextCommonMock,
|
||||||
|
url: {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
registrationUrl: "#"
|
||||||
|
},
|
||||||
|
realm: {
|
||||||
|
...kcContextCommonMock.realm,
|
||||||
|
password: true,
|
||||||
|
registrationAllowed: true
|
||||||
|
},
|
||||||
|
registrationDisabled: false,
|
||||||
|
isUserIdentified: "false",
|
||||||
|
challenge: "",
|
||||||
|
userVerification: "not specified",
|
||||||
|
rpId: "",
|
||||||
|
createTimeout: "0",
|
||||||
|
authenticators: {
|
||||||
|
authenticators: []
|
||||||
|
},
|
||||||
|
shouldDisplayAuthenticators: false,
|
||||||
|
login: {}
|
||||||
|
}),
|
||||||
|
id<KcContext.LoginIdpLinkConfirmOverride>({
|
||||||
|
pageId: "login-idp-link-confirm-override.ftl",
|
||||||
|
...kcContextCommonMock,
|
||||||
|
url: {
|
||||||
|
...kcContextCommonMock.url,
|
||||||
|
loginRestartFlowUrl: "#"
|
||||||
|
},
|
||||||
|
idpDisplayName: "Google"
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
|
40
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
40
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
// NOTE: Added with Keycloak 25
|
||||||
|
export default function LoginIdpLinkConfirmOverride(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm-override.ftl" }>, I18n>) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { url, idpDisplayName } = kcContext;
|
||||||
|
|
||||||
|
const { msg } = i18n;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmOverrideIdpTitle")}>
|
||||||
|
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||||
|
{msg("pageExpiredMsg1")}{" "}
|
||||||
|
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||||
|
{msg("doClickHere")}
|
||||||
|
</a>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
name="submitAction"
|
||||||
|
id="confirmOverride"
|
||||||
|
value="confirmOverride"
|
||||||
|
>
|
||||||
|
{msg("confirmOverrideIdpContinue", idpDisplayName)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
252
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
252
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import { useEffect, Fragment } from "react";
|
||||||
|
import { clsx } from "keycloakify/tools/clsx";
|
||||||
|
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||||
|
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||||
|
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||||
|
import { assert } from "keycloakify/tools/assert";
|
||||||
|
import type { KcContext } from "../KcContext";
|
||||||
|
import type { I18n } from "../i18n";
|
||||||
|
|
||||||
|
// NOTE: From Keycloak 25.0.4
|
||||||
|
export default function LoginPasskeysConditionalAuthenticate(
|
||||||
|
props: PageProps<Extract<KcContext, { pageId: "login-passkeys-conditional-authenticate.ftl" }>, I18n>
|
||||||
|
) {
|
||||||
|
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
messagesPerField,
|
||||||
|
login,
|
||||||
|
url,
|
||||||
|
usernameHidden,
|
||||||
|
shouldDisplayAuthenticators,
|
||||||
|
authenticators,
|
||||||
|
registrationDisabled,
|
||||||
|
realm,
|
||||||
|
isUserIdentified,
|
||||||
|
challenge,
|
||||||
|
userVerification,
|
||||||
|
rpId,
|
||||||
|
createTimeout
|
||||||
|
} = kcContext;
|
||||||
|
|
||||||
|
const { msg, msgStr, advancedMsg } = i18n;
|
||||||
|
|
||||||
|
const { kcClsx } = getKcClsx({
|
||||||
|
doUseDefaultCss,
|
||||||
|
classes
|
||||||
|
});
|
||||||
|
|
||||||
|
const { insertScriptTags } = useInsertScriptTags({
|
||||||
|
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
|
||||||
|
scriptTags: [
|
||||||
|
{
|
||||||
|
type: "module",
|
||||||
|
textContent: `
|
||||||
|
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||||
|
import { initAuthenticate } from "${url.resourcesPath}/js/passkeysConditionalAuth.js";
|
||||||
|
|
||||||
|
const authButton = document.getElementById('authenticateWebAuthnButton');
|
||||||
|
const input = {
|
||||||
|
isUserIdentified : ${isUserIdentified},
|
||||||
|
challenge : '${challenge}',
|
||||||
|
userVerification : '${userVerification}',
|
||||||
|
rpId : '${rpId}',
|
||||||
|
createTimeout : ${createTimeout},
|
||||||
|
errmsg : "${msgStr("webauthn-unsupported-browser-text")}"
|
||||||
|
};
|
||||||
|
authButton.addEventListener("click", () => {
|
||||||
|
authenticateByWebAuthn(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
isUserIdentified : ${isUserIdentified},
|
||||||
|
challenge : '${challenge}',
|
||||||
|
userVerification : '${userVerification}',
|
||||||
|
rpId : '${rpId}',
|
||||||
|
createTimeout : ${createTimeout},
|
||||||
|
errmsg : "${msgStr("passkey-unsupported-browser-text")}"
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", (event) => initAuthenticate(args));
|
||||||
|
`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
insertScriptTags();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
kcContext={kcContext}
|
||||||
|
i18n={i18n}
|
||||||
|
doUseDefaultCss={doUseDefaultCss}
|
||||||
|
classes={classes}
|
||||||
|
headerNode={msg("passkey-login-title")}
|
||||||
|
infoNode={
|
||||||
|
realm.registrationAllowed &&
|
||||||
|
!registrationDisabled && (
|
||||||
|
<div id="kc-registration">
|
||||||
|
<span>
|
||||||
|
${msg("noAccount")}{" "}
|
||||||
|
<a tabIndex={6} href={url.registrationUrl}>
|
||||||
|
{msg("doRegister")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form id="webauth" action={url.loginAction} method="post">
|
||||||
|
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
||||||
|
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
||||||
|
<input type="hidden" id="signature" name="signature" />
|
||||||
|
<input type="hidden" id="credentialId" name="credentialId" />
|
||||||
|
<input type="hidden" id="userHandle" name="userHandle" />
|
||||||
|
<input type="hidden" id="error" name="error" />
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className={kcClsx("kcFormGroupClass")} no-bottom-margin="true" style={{ marginBottom: 0 }}>
|
||||||
|
{authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
|
||||||
|
<>
|
||||||
|
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
||||||
|
{authenticators.authenticators.map((authenticator, i) => (
|
||||||
|
<input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} />
|
||||||
|
))}
|
||||||
|
</form>
|
||||||
|
{shouldDisplayAuthenticators && (
|
||||||
|
<>
|
||||||
|
{authenticators.authenticators.length > 1 && (
|
||||||
|
<p className={kcClsx("kcSelectAuthListItemTitle")}>{msg("passkey-available-authenticators")}</p>
|
||||||
|
)}
|
||||||
|
<div className={kcClsx("kcFormClass")}>
|
||||||
|
{authenticators.authenticators.map((authenticator, i) => (
|
||||||
|
<div key={i} id={`kc-webauthn-authenticator-item-${i}`} className={kcClsx("kcSelectAuthListItemClass")}>
|
||||||
|
<i
|
||||||
|
className={clsx(
|
||||||
|
(() => {
|
||||||
|
const className = kcClsx(authenticator.transports.iconClass as any);
|
||||||
|
if (className === authenticator.transports.iconClass) {
|
||||||
|
return kcClsx("kcWebAuthnDefaultIcon");
|
||||||
|
}
|
||||||
|
return className;
|
||||||
|
})(),
|
||||||
|
kcClsx("kcSelectAuthListItemIconPropertyClass")
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemBodyClass")}>
|
||||||
|
<div
|
||||||
|
id={`kc-webauthn-authenticator-label-${i}`}
|
||||||
|
className={kcClsx("kcSelectAuthListItemHeadingClass")}
|
||||||
|
>
|
||||||
|
{advancedMsg(authenticator.label)}
|
||||||
|
</div>
|
||||||
|
{authenticator.transports !== undefined &&
|
||||||
|
authenticator.transports.displayNameProperties !== undefined &&
|
||||||
|
authenticator.transports.displayNameProperties.length !== 0 && (
|
||||||
|
<div
|
||||||
|
id={`kc-webauthn-authenticator-transport-${i}`}
|
||||||
|
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
|
||||||
|
>
|
||||||
|
{authenticator.transports.displayNameProperties.map((nameProperty, i, arr) => (
|
||||||
|
<Fragment key={i}>
|
||||||
|
<span key={i}> {advancedMsg(nameProperty)} </span>
|
||||||
|
{i !== arr.length - 1 && <span>, </span>}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>
|
||||||
|
<span id={`kc-webauthn-authenticator-createdlabel-${i}`}>{msg("passkey-createdAt-label")}</span>
|
||||||
|
<span id={`kc-webauthn-authenticator-created-${i}`}>{authenticator.createdAt}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div id="kc-form">
|
||||||
|
<div id="kc-form-wrapper">
|
||||||
|
{realm.password && (
|
||||||
|
<form
|
||||||
|
id="kc-form-passkey"
|
||||||
|
action={url.loginAction}
|
||||||
|
method="post"
|
||||||
|
style={{ display: "none" }}
|
||||||
|
onSubmit={event => {
|
||||||
|
try {
|
||||||
|
// @ts-expect-error
|
||||||
|
event.target.login.disabled = true;
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{!usernameHidden && (
|
||||||
|
<div className={kcClsx("kcFormGroupClass")}>
|
||||||
|
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
|
||||||
|
{msg("passkey-autofill-select")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
tabIndex={1}
|
||||||
|
id="username"
|
||||||
|
aria-invalid={messagesPerField.existsError("username")}
|
||||||
|
className={kcClsx("kcInputClass")}
|
||||||
|
name="username"
|
||||||
|
defaultValue={login.username ?? ""}
|
||||||
|
//autoComplete="username webauthn"
|
||||||
|
type="text"
|
||||||
|
autoFocus
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
{messagesPerField.existsError("username") && (
|
||||||
|
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||||
|
{messagesPerField.get("username")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
|
||||||
|
<input
|
||||||
|
id="authenticateWebAuthnButton"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
assert("doAuthenticate" in window);
|
||||||
|
assert(typeof window.doAuthenticate === "function");
|
||||||
|
window.doAuthenticate(
|
||||||
|
[],
|
||||||
|
rpId,
|
||||||
|
challenge,
|
||||||
|
typeof isUserIdentified === "boolean" ? isUserIdentified : isUserIdentified === "true",
|
||||||
|
createTimeout,
|
||||||
|
userVerification,
|
||||||
|
msgStr("passkey-unsupported-browser-text")
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
value={msgStr("passkey-doAuthenticate")}
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
|
||||||
|
<input
|
||||||
|
id="authenticateWebAuthnButton"
|
||||||
|
type="button"
|
||||||
|
autoFocus
|
||||||
|
value={msgStr("passkey-doAuthenticate")}
|
||||||
|
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Template>
|
||||||
|
);
|
||||||
|
}
|
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { createKcPageStory } from "../KcPageStory";
|
||||||
|
|
||||||
|
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm-override.ftl" });
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "login/login-idp-link-confirm-override.ftl",
|
||||||
|
component: KcPageStory
|
||||||
|
} satisfies Meta<typeof KcPageStory>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <KcPageStory />
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { createKcPageStory } from "../KcPageStory";
|
||||||
|
|
||||||
|
const { KcPageStory } = createKcPageStory({ pageId: "login-passkeys-conditional-authenticate.ftl" });
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
title: "login/login-passkeys-conditional-authenticate.ftl",
|
||||||
|
component: KcPageStory
|
||||||
|
} satisfies Meta<typeof KcPageStory>;
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof meta>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => <KcPageStory />
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user