From c6b52acf2f968ed61d55c8c7ad2db4b910f5b025 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 2 Sep 2024 03:26:39 +0200 Subject: [PATCH 01/20] #631 --- ...pyKeycloakResourcesToStorybookStaticDir.ts | 4 +- scripts/generate-i18n-messages.ts | 204 ++++++++------ src/bin/shared/buildContext.ts | 4 +- src/bin/shared/constants.ts | 6 +- .../downloadKeycloakDefaultTheme.ts | 52 +++- .../extra-assets/passkeysConditionalAuth.js | 79 ++++++ .../extra-assets/webauthnAuthenticate.js | 82 ++++++ .../downloadKeycloakDefaultTheme/index.ts | 1 + src/login/DefaultPage.tsx | 6 + src/login/KcContext/KcContext.ts | 38 ++- src/login/KcContext/kcContextMocks.ts | 33 +++ .../pages/LoginIdpLinkConfirmOverride.tsx | 40 +++ .../LoginPasskeysConditionalAuthenticate.tsx | 252 ++++++++++++++++++ .../LoginIdpLinkConfirmOverride.stories.tsx | 18 ++ ...asskeysConditionalAuthenticate.stories.tsx | 18 ++ 15 files changed, 748 insertions(+), 89 deletions(-) rename src/bin/shared/{ => downloadKeycloakDefaultTheme}/downloadKeycloakDefaultTheme.ts (86%) create mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js create mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js create mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/index.ts create mode 100644 src/login/pages/LoginIdpLinkConfirmOverride.tsx create mode 100644 src/login/pages/LoginPasskeysConditionalAuthenticate.tsx create mode 100644 stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx create mode 100644 stories/login/pages/LoginPasskeysConditionalAuthenticate.stories.tsx diff --git a/scripts/copyKeycloakResourcesToStorybookStaticDir.ts b/scripts/copyKeycloakResourcesToStorybookStaticDir.ts index b666c27b..fd6b4368 100644 --- a/scripts/copyKeycloakResourcesToStorybookStaticDir.ts +++ b/scripts/copyKeycloakResourcesToStorybookStaticDir.ts @@ -1,7 +1,7 @@ import { join as pathJoin } from "path"; import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic"; 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() { await copyKeycloakResourcesToPublic({ @@ -11,7 +11,7 @@ export async function copyKeycloakResourcesToStorybookStaticDir() { npmConfigGetCwd: pathJoin(__dirname, "..") }), loginThemeResourcesFromKeycloakVersion: - LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT, + LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT, publicDirPath: pathJoin(__dirname, "..", ".storybook", "static") } }); diff --git a/scripts/generate-i18n-messages.ts b/scripts/generate-i18n-messages.ts index 89affa4f..8aa3cc9c 100644 --- a/scripts/generate-i18n-messages.ts +++ b/scripts/generate-i18n-messages.ts @@ -13,6 +13,10 @@ import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloak import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath"; import { deepAssign } from "../src/tools/deepAssign"; 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, // update the version array for generating for newer version. @@ -21,68 +25,77 @@ import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions"; const propertiesParser = require("properties-parser"); async function main() { - const keycloakVersion = "24.0.4"; - const thisCodebaseRootDirPath = getThisCodebaseRootDirPath(); - const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion, - buildContext: { - cacheDirPath: pathJoin( - thisCodebaseRootDirPath, - "node_modules", - ".cache", - "keycloakify" - ), - fetchOptions: getProxyFetchOptions({ - npmConfigGetCwd: thisCodebaseRootDirPath - }) - } - }); - type Dictionary = { [idiomId: string]: string }; - const record: { [typeOfPage: string]: { [language: string]: Dictionary } } = {}; + const record: { [themeType: string]: { [language: string]: Dictionary } } = {}; - { - const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base"); - const re = new RegExp( - `^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$` - ); - - crawl({ - dirPath: baseThemeDirPath, - returnedPathsType: "relative to dirPath" - }).forEach(filePath => { - const match = filePath.match(re); - - if (match === null) { - return; + for (const themeType of THEME_TYPES) { + const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ + keycloakVersion: (() => { + switch (themeType) { + case "login": + return "25.0.4"; + case "account": + return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1; + } + })(), + buildContext: { + cacheDirPath: pathJoin( + thisCodebaseRootDirPath, + "node_modules", + ".cache", + "keycloakify" + ), + fetchOptions: getProxyFetchOptions({ + npmConfigGetCwd: thisCodebaseRootDirPath + }) } - - const [, typeOfPage, language] = match; - - (record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries( - Object.entries( - propertiesParser.parse( - fs - .readFileSync(pathJoin(baseThemeDirPath, filePath)) - .toString("utf8") - ) as Record - ) - .map(([key, value]) => [key, value.replace(/''/g, "'")]) - .map(([key, value]) => [ - key === "locale_pt_BR" ? "locale_pt-BR" : key, - value - ]) - .map(([key, value]) => [key, key === "termsText" ? "" : value]) - ); }); - } - Object.keys(record).forEach(themeType => { - if (themeType !== "login" && themeType !== "account") { - return; + { + const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base"); + const re = new RegExp( + `^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$` + ); + + crawl({ + dirPath: baseThemeDirPath, + returnedPathsType: "relative to dirPath" + }).forEach(filePath => { + const match = filePath.match(re); + + if (match === null) { + return; + } + + const [, themeType_here, language] = match; + + if (themeType_here !== themeType) { + return; + } + + (record[themeType] ??= {})[language.replace(/_/g, "-")] = + Object.fromEntries( + Object.entries( + propertiesParser.parse( + fs + .readFileSync(pathJoin(baseThemeDirPath, filePath)) + .toString("utf8") + ) as Record + ) + .map(([key, value]) => [key, value.replace(/''/g, "'")]) + .map(([key, value]) => [ + key === "locale_pt_BR" ? "locale_pt-BR" : key, + value + ]) + .map(([key, value]) => [ + key, + key === "termsText" ? "" : value + ]) + ); + }); } const recordForThemeType = record[themeType]; @@ -99,6 +112,29 @@ async function main() { assert(false); })(); + /* Migration helper + + console.log({ themeType }); + + { + + const all = new Set(); + + 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( same(languages, Object.keys(keycloakifyExtraMessages), { takeIntoAccountArraysOrdering: false @@ -180,7 +216,7 @@ async function main() { "utf8" ) ); - }); + } } const keycloakifyExtraMessages_login: Record< @@ -203,6 +239,7 @@ const keycloakifyExtraMessages_login: Record< | "nl" | "no" | "pl" + | "pt" | "pt-BR" | "ru" | "sk" @@ -210,7 +247,9 @@ const keycloakifyExtraMessages_login: Record< | "th" | "tr" | "uk" - | "zh-CN", + | "ka" + | "zh-CN" + | "zh-TW", Record< | "shouldBeEqual" | "shouldBeDifferent" @@ -434,6 +473,17 @@ const keycloakifyExtraMessages_login: Record< addValue: "Dodaj wartość", 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": { shouldBeEqual: "{0} deve ser igual a {1}", shouldBeDifferent: "{0} deve ser diferente de {1}", @@ -511,6 +561,17 @@ const keycloakifyExtraMessages_login: Record< addValue: "Додати значення", languages: "Мови" }, + ka: { + shouldBeEqual: "{0} უნდა იყოს ტოლი {1}-სთვის", + shouldBeDifferent: "{0} უნდა იყოს სხვა {1}-სთვის", + shouldMatchPattern: "შაბლონს უნდა ემთხვევა: `/{0}/`", + mustBeAnInteger: "უნდა იყოს მთელი რიცხვი", + notAValidOption: "არასწორი ვარიანტი", + selectAnOption: "აირჩიეთ ვარიანტი", + remove: "წაშალეთ", + addValue: "დაამატეთ მნიშვნელობა", + languages: "ენები" + }, "zh-CN": { shouldBeEqual: "{0} 应该等于 {1}", shouldBeDifferent: "{0} 应该不同于 {1}", @@ -521,6 +582,17 @@ const keycloakifyExtraMessages_login: Record< remove: "移除", addValue: "添加值", languages: "语言" + }, + "zh-TW": { + shouldBeEqual: "{0} 應該等於 {1}", + shouldBeDifferent: "{0} 應該不同於 {1}", + shouldMatchPattern: "模式應匹配: `/{0}/`", + mustBeAnInteger: "必須是整數", + notAValidOption: "不是有效選項", + selectAnOption: "選擇一個選項", + remove: "移除", + addValue: "添加值", + languages: "語言" } /* spell-checker: enable */ }; @@ -532,9 +604,7 @@ const keycloakifyExtraMessages_account: Record< | "cs" | "da" | "de" - | "el" | "es" - | "fa" | "fi" | "fr" | "hu" @@ -549,9 +619,7 @@ const keycloakifyExtraMessages_account: Record< | "ru" | "sk" | "sv" - | "th" | "tr" - | "uk" | "zh-CN", Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string> > = { @@ -580,18 +648,10 @@ const keycloakifyExtraMessages_account: Record< newPasswordSameAsOld: "Das neue Passwort muss sich vom alten unterscheiden", passwordConfirmNotMatch: "Passwortbestätigung stimmt nicht überein" }, - el: { - newPasswordSameAsOld: "Ο νέος κωδικός πρόσβασης πρέπει να διαφέρει από τον παλιό", - passwordConfirmNotMatch: "Η επιβεβαίωση του κωδικού πρόσβασης δεν ταιριάζει" - }, es: { newPasswordSameAsOld: "La nueva contraseña debe ser diferente de la anterior", passwordConfirmNotMatch: "La confirmación de la contraseña no coincide" }, - fa: { - newPasswordSameAsOld: "رمز عبور جدید باید با رمز عبور قبلی متفاوت باشد", - passwordConfirmNotMatch: "تأیید رمز عبور مطابقت ندارد" - }, fi: { newPasswordSameAsOld: "Uusi salasana on oltava erilainen kuin vanha", 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", passwordConfirmNotMatch: "Lösenordsbekräftelsen matchar inte" }, - th: { - newPasswordSameAsOld: "รหัสผ่านใหม่ต้องต่างจากรหัสผ่านเดิม", - passwordConfirmNotMatch: "การยืนยันรหัสผ่านไม่ตรงกัน" - }, tr: { newPasswordSameAsOld: "Yeni şifre eskisinden farklı olmalıdır", passwordConfirmNotMatch: "Şifre doğrulama eşleşmiyor" }, - uk: { - newPasswordSameAsOld: "Новий пароль повинен відрізнятися від старого", - passwordConfirmNotMatch: "Підтвердження пароля не співпадає" - }, "zh-CN": { newPasswordSameAsOld: "新密码必须与旧密码不同", passwordConfirmNotMatch: "密码确认不匹配" diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index b0c5439d..4c45d449 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -15,7 +15,7 @@ import * as child_process from "child_process"; import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES, BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME, - LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT + LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT } from "./constants"; import type { KeycloakVersionRange } from "./KeycloakVersionRange"; import { exclude } from "tsafe"; @@ -547,7 +547,7 @@ export function getBuildContext(params: { `${themeNames[0]}-keycloak-theme`, loginThemeResourcesFromKeycloakVersion: buildOptions.loginThemeResourcesFromKeycloakVersion ?? - LOGIN_THEME_RESOURCES_FROMkEYCLOAK_VERSION_DEFAULT, + LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT, projectDirPath, projectBuildDirPath, keycloakifyBuildDirPath: (() => { diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index 255a1fdf..ad015654 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -50,7 +50,9 @@ export const LOGIN_THEME_PAGE_IDS = [ "login-recovery-authn-code-input.ftl", "login-reset-otp.ftl", "login-x509-info.ftl", - "webauthn-error.ftl" + "webauthn-error.ftl", + "login-passkeys-conditional-authenticate.ftl", + "login-idp-link-confirm-override.ftl" ] as const; export const ACCOUNT_THEME_PAGE_IDS = [ @@ -70,4 +72,4 @@ export const CONTAINER_NAME = "keycloak-keycloakify"; 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"; diff --git a/src/bin/shared/downloadKeycloakDefaultTheme.ts b/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts similarity index 86% rename from src/bin/shared/downloadKeycloakDefaultTheme.ts rename to src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts index c72e251a..f03bfaf0 100644 --- a/src/bin/shared/downloadKeycloakDefaultTheme.ts +++ b/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts @@ -1,8 +1,10 @@ -import { join as pathJoin, relative as pathRelative } from "path"; -import { type BuildContext } from "./buildContext"; +import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; +import { type BuildContext } from "../buildContext"; import { assert } from "tsafe/assert"; -import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "./constants"; -import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive"; +import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants"; +import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive"; +import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; +import * as fsPr from "fs/promises"; export type BuildContextLike = { cacheDirPath: string; @@ -20,6 +22,8 @@ export async function downloadKeycloakDefaultTheme(params: { let kcNodeModulesKeepFilePaths: Set | undefined = undefined; let kcNodeModulesKeepFilePaths_lastAccountV1: Set | undefined = undefined; + let areExtraAssetsFor24Copied = false; + const { extractedDirPath } = await downloadAndExtractArchive({ url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, cacheDirPath: buildContext.cacheDirPath, @@ -32,8 +36,6 @@ export async function downloadKeycloakDefaultTheme(params: { return; } - const { readFile, writeFile } = params; - skip_keycloak_v2: { if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) { break skip_keycloak_v2; @@ -42,6 +44,8 @@ export async function downloadKeycloakDefaultTheme(params: { return; } + const { readFile, writeFile } = params; + last_account_v1_transformations: { if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) { 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: { if (keycloakVersion !== "24.0.4") { break skip_unused_resources; diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js new file mode 100644 index 00000000..5fcbb4ed --- /dev/null +++ b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js @@ -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; +} \ No newline at end of file diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js new file mode 100644 index 00000000..eaa05b69 --- /dev/null +++ b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js @@ -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(); +} \ No newline at end of file diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/index.ts b/src/bin/shared/downloadKeycloakDefaultTheme/index.ts new file mode 100644 index 00000000..c8acd724 --- /dev/null +++ b/src/bin/shared/downloadKeycloakDefaultTheme/index.ts @@ -0,0 +1 @@ +export * from "./downloadKeycloakDefaultTheme"; diff --git a/src/login/DefaultPage.tsx b/src/login/DefaultPage.tsx index 278f2013..9ddfbe09 100644 --- a/src/login/DefaultPage.tsx +++ b/src/login/DefaultPage.tsx @@ -40,6 +40,8 @@ const LoginRecoveryAuthnCodeInput = lazy(() => import("keycloakify/login/pages/L const LoginResetOtp = lazy(() => import("keycloakify/login/pages/LoginResetOtp")); const LoginX509Info = lazy(() => import("keycloakify/login/pages/LoginX509Info")); 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 & { UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>; @@ -121,6 +123,10 @@ export default function DefaultPage(props: DefaultPageProps) { return ; case "webauthn-error.ftl": return ; + case "login-passkeys-conditional-authenticate.ftl": + return ; + case "login-idp-link-confirm-override.ftl": + return ; } assert>(false); })()} diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts index a5db6561..a091225e 100644 --- a/src/login/KcContext/KcContext.ts +++ b/src/login/KcContext/KcContext.ts @@ -59,7 +59,9 @@ export type KcContext = | KcContext.LoginRecoveryAuthnCodeInput | KcContext.LoginResetOtp | KcContext.LoginX509Info - | KcContext.WebauthnError; + | KcContext.WebauthnError + | KcContext.LoginPasskeysConditionalAuthenticate + | KcContext.LoginIdpLinkConfirmOverride; assert(); @@ -577,6 +579,40 @@ export declare namespace KcContext { pageId: "webauthn-error.ftl"; 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 = { diff --git a/src/login/KcContext/kcContextMocks.ts b/src/login/KcContext/kcContextMocks.ts index 030fb089..99343466 100644 --- a/src/login/KcContext/kcContextMocks.ts +++ b/src/login/KcContext/kcContextMocks.ts @@ -567,6 +567,39 @@ export const kcContextMocks = [ pageId: "webauthn-error.ftl", ...kcContextCommonMock, isAppInitiatedAction: true + }), + id({ + 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({ + pageId: "login-idp-link-confirm-override.ftl", + ...kcContextCommonMock, + url: { + ...kcContextCommonMock.url, + loginRestartFlowUrl: "#" + }, + idpDisplayName: "Google" }) ]; diff --git a/src/login/pages/LoginIdpLinkConfirmOverride.tsx b/src/login/pages/LoginIdpLinkConfirmOverride.tsx new file mode 100644 index 00000000..dbad8a32 --- /dev/null +++ b/src/login/pages/LoginIdpLinkConfirmOverride.tsx @@ -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, I18n>) { + const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; + + const { kcClsx } = getKcClsx({ + doUseDefaultCss, + classes + }); + + const { url, idpDisplayName } = kcContext; + + const { msg } = i18n; + + return ( + + ); +} diff --git a/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx b/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx new file mode 100644 index 00000000..bb3f8b70 --- /dev/null +++ b/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx @@ -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, 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 ( + + ); +} diff --git a/stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx b/stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx new file mode 100644 index 00000000..88d5fdaf --- /dev/null +++ b/stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; diff --git a/stories/login/pages/LoginPasskeysConditionalAuthenticate.stories.tsx b/stories/login/pages/LoginPasskeysConditionalAuthenticate.stories.tsx new file mode 100644 index 00000000..c1e3ab64 --- /dev/null +++ b/stories/login/pages/LoginPasskeysConditionalAuthenticate.stories.tsx @@ -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; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => +}; From f3602219f3fe73e8df06331a186ddb0c7a4bf77e Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 2 Sep 2024 03:27:09 +0200 Subject: [PATCH 02/20] Release candidate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df108e96..4e2db040 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keycloakify", - "version": "10.0.5", + "version": "10.1.0-rc.0", "description": "Create Keycloak themes using React", "repository": { "type": "git", From 46c40d713a298304866c4577441a2af29b5edccf Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 2 Sep 2024 03:37:16 +0200 Subject: [PATCH 03/20] Fix broken path --- .../downloadKeycloakDefaultTheme.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts b/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts index f03bfaf0..6988f121 100644 --- a/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts +++ b/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts @@ -1,4 +1,4 @@ -import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; +import { join as pathJoin, relative as pathRelative } from "path"; import { type BuildContext } from "../buildContext"; import { assert } from "tsafe/assert"; import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants"; @@ -185,7 +185,8 @@ export async function downloadKeycloakDefaultTheme(params: { getThisCodebaseRootDirPath(), "src", "bin", - __dirname.split(`${pathSep}bin${pathSep}`)[1], + "shared", + "downloadKeycloakDefaultTheme", "extra-assets" ); From 569e933f02a1f9bd3e38b879c01172727d5bb1ac Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 2 Sep 2024 03:37:36 +0200 Subject: [PATCH 04/20] Release candidate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e2db040..6af71d74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keycloakify", - "version": "10.1.0-rc.0", + "version": "10.1.0-rc.1", "description": "Create Keycloak themes using React", "repository": { "type": "git", From 93c1c56279d4cb5303d5f273c454ff1a95d4a75d Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 00:06:47 +0200 Subject: [PATCH 05/20] Refactor checkpoint --- .gitignore | 2 + package.json | 2 +- scripts/build.ts | 13 +- scripts/prepare/constants.ts | 4 + scripts/prepare/createAccountV1Dir.ts | 77 ++++ scripts/prepare/createResourcesDir.ts | 70 ++++ .../prepare/downloadKeycloakDefaultTheme.ts | 34 ++ .../generateI18nMessages.ts} | 92 ++--- scripts/prepare/main.ts | 13 + src/account/KcContext/kcContextMocks.ts | 6 +- src/bin/copy-keycloak-resources-to-public.ts | 2 +- .../keycloakify/generateFtl/generateFtl.ts | 10 +- .../generateResources/bringInAccountV1.ts | 89 ----- .../generateResourcesForMainTheme.ts | 148 ++++---- .../replacers/replaceImportsInCssCode.ts | 4 +- .../replacers/replaceImportsInJsCode/vite.ts | 6 +- .../replaceImportsInJsCode/webpack.ts | 6 +- src/bin/shared/buildContext.ts | 9 +- src/bin/shared/constants.ts | 14 +- .../shared/copyKeycloakResourcesToPublic.ts | 68 ++-- .../downloadKeycloakDefaultTheme.ts | 338 ------------------ .../extra-assets/passkeysConditionalAuth.js | 79 ---- .../extra-assets/webauthnAuthenticate.js | 82 ----- .../downloadKeycloakDefaultTheme/index.ts | 1 - .../shared/downloadKeycloakStaticResources.ts | 53 --- src/login/KcContext/kcContextMocks.ts | 7 +- src/vite-plugin/vite-plugin.ts | 26 +- test/bin/replacers.spec.ts | 44 +-- 28 files changed, 402 insertions(+), 897 deletions(-) create mode 100644 scripts/prepare/constants.ts create mode 100644 scripts/prepare/createAccountV1Dir.ts create mode 100644 scripts/prepare/createResourcesDir.ts create mode 100644 scripts/prepare/downloadKeycloakDefaultTheme.ts rename scripts/{generate-i18n-messages.ts => prepare/generateI18nMessages.ts} (94%) create mode 100644 scripts/prepare/main.ts delete mode 100644 src/bin/keycloakify/generateResources/bringInAccountV1.ts delete mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts delete mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js delete mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js delete mode 100644 src/bin/shared/downloadKeycloakDefaultTheme/index.ts delete mode 100644 src/bin/shared/downloadKeycloakStaticResources.ts diff --git a/.gitignore b/.gitignore index 687df547..dc5f3c97 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,8 @@ jspm_packages /src/login/i18n/messages_defaultSet/ /src/account/i18n/messages_defaultSet/ +/resources/ +/account-v1/ # VS Code devcontainers .devcontainer diff --git a/package.json b/package.json index 6af71d74..3c5b32fa 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "url": "git://github.com/keycloakify/keycloakify.git" }, "scripts": { - "prepare": "tsx scripts/generate-i18n-messages.ts", + "prepare": "tsx scripts/prepare/main.ts", "build": "tsx scripts/build.ts", "storybook": "tsx scripts/start-storybook.ts", "link-in-starter": "tsx scripts/link-in-starter.ts", diff --git a/scripts/build.ts b/scripts/build.ts index c32f2087..583d135f 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -4,6 +4,7 @@ import { join } from "path"; import { assert } from "tsafe/assert"; import { transformCodebase } from "../src/bin/tools/transformCodebase"; import chalk from "chalk"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../src/bin/shared/constants"; console.log(chalk.cyan("Building Keycloakify...")); @@ -136,9 +137,17 @@ fs.rmSync(join("dist", "ncc_out"), { recursive: true }); assert(hasBeenPatched); } -fs.rmSync(join("dist", "src"), { recursive: true, force: true }); +for (const dirBasename of [ + "src", + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, + WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 +]) { + const destDirPath = join("dist", dirBasename); -fs.cpSync("src", join("dist", "src"), { recursive: true }); + fs.rmSync(destDirPath, { recursive: true, force: true }); + + fs.cpSync(dirBasename, destDirPath, { recursive: true }); +} transformCodebase({ srcDirPath: join("stories"), diff --git a/scripts/prepare/constants.ts b/scripts/prepare/constants.ts new file mode 100644 index 00000000..fffddb47 --- /dev/null +++ b/scripts/prepare/constants.ts @@ -0,0 +1,4 @@ +export const KEYCLOAK_VERSION = { + FOR_LOGIN_THEME: "25.0.4", + FOR_ACCOUNT_MULTI_PAGE: "21.1.2" +}; diff --git a/scripts/prepare/createAccountV1Dir.ts b/scripts/prepare/createAccountV1Dir.ts new file mode 100644 index 00000000..19a7e91f --- /dev/null +++ b/scripts/prepare/createAccountV1Dir.ts @@ -0,0 +1,77 @@ +import * as fs from "fs"; +import { join as pathJoin } from "path"; +import { KEYCLOAK_VERSION } from "./constants"; +import { transformCodebase } from "../../src/bin/tools/transformCodebase"; +import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants"; +import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; +import { accountMultiPageSupportedLanguages } from "./generateI18nMessages"; + +export async function createAccountV1Dir() { + const { extractedDirPath } = await downloadKeycloakDefaultTheme({ + keycloakVersion: KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE + }); + + // TODO: Exclude unused resources. + + const destDirPath = pathJoin( + getThisCodebaseRootDirPath(), + WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 + ); + + transformCodebase({ + srcDirPath: pathJoin(extractedDirPath, "base", "account"), + destDirPath + }); + + transformCodebase({ + srcDirPath: pathJoin(extractedDirPath, "keycloak", "account", "resources"), + destDirPath: pathJoin(destDirPath, "resources") + }); + + transformCodebase({ + srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"), + destDirPath: pathJoin( + destDirPath, + "resources", + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON + ) + }); + + fs.writeFileSync( + pathJoin(destDirPath, "theme.properties"), + Buffer.from( + [ + "accountResourceProvider=account-v1", + "", + `locales=${accountMultiPageSupportedLanguages.join(",")}`, + "", + "styles=" + + [ + "css/account.css", + "img/icon-sidebar-active.png", + "img/logo.png", + ...[ + "patternfly.min.css", + "patternfly-additions.min.css", + "patternfly-additions.min.css" + ].map( + fileBasename => + `${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}` + ) + ].join(" "), + "", + "##### css classes for form buttons", + "# main class used for all buttons", + "kcButtonClass=btn", + "# classes defining priority of the button - primary or default (there is typically only one priority button for the form)", + "kcButtonPrimaryClass=btn-primary", + "kcButtonDefaultClass=btn-default", + "# classes defining size of the button", + "kcButtonLargeClass=btn-lg", + "" + ].join("\n"), + "utf8" + ) + ); +} diff --git a/scripts/prepare/createResourcesDir.ts b/scripts/prepare/createResourcesDir.ts new file mode 100644 index 00000000..1fb13f54 --- /dev/null +++ b/scripts/prepare/createResourcesDir.ts @@ -0,0 +1,70 @@ +import { join as pathJoin } from "path"; +import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; +import { KEYCLOAK_VERSION } from "./constants"; +import { transformCodebase } from "../../src/bin/tools/transformCodebase"; +import { existsAsync } from "../../src/bin/tools/fs.existsAsync"; +import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants"; +import { assert, type Equals } from "tsafe/assert"; + +export async function createResourcesDir() { + await Promise.all( + (["login", "account"] as const).map(async themeType => { + const keycloakVersion = (() => { + switch (themeType) { + case "login": + return KEYCLOAK_VERSION.FOR_LOGIN_THEME; + case "account": + return KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE; + } + assert>(); + })(); + + const { extractedDirPath } = await downloadKeycloakDefaultTheme({ + keycloakVersion + }); + + const destDirPath = pathJoin( + getThisCodebaseRootDirPath(), + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, + themeType + ); + + base_resources: { + const srcDirPath = pathJoin( + extractedDirPath, + "base", + themeType, + "resources" + ); + + if (!(await existsAsync(srcDirPath))) { + break base_resources; + } + + transformCodebase({ + srcDirPath, + destDirPath + }); + } + + transformCodebase({ + srcDirPath: pathJoin( + extractedDirPath, + "keycloak", + themeType, + "resources" + ), + destDirPath + }); + + transformCodebase({ + srcDirPath: pathJoin(extractedDirPath, "keycloak", "common", "resources"), + destDirPath: pathJoin( + destDirPath, + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON + ) + }); + }) + ); +} diff --git a/scripts/prepare/downloadKeycloakDefaultTheme.ts b/scripts/prepare/downloadKeycloakDefaultTheme.ts new file mode 100644 index 00000000..7848e969 --- /dev/null +++ b/scripts/prepare/downloadKeycloakDefaultTheme.ts @@ -0,0 +1,34 @@ +import { relative as pathRelative } from "path"; +import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtractArchive"; +import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions"; +import { join as pathJoin } from "path"; +import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; + +export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: string }) { + const { keycloakVersion } = params; + + const { extractedDirPath } = await downloadAndExtractArchive({ + url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, + cacheDirPath: pathJoin( + getThisCodebaseRootDirPath(), + "node_modules", + ".cache", + "scripts" + ), + fetchOptions: getProxyFetchOptions({ + npmConfigGetCwd: getThisCodebaseRootDirPath() + }), + uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme", + onArchiveFile: async ({ fileRelativePath, writeFile }) => { + const fileRelativePath_target = pathRelative("theme", fileRelativePath); + + if (fileRelativePath_target.startsWith("..")) { + return; + } + + await writeFile({ fileRelativePath: fileRelativePath_target }); + } + }); + + return { extractedDirPath }; +} diff --git a/scripts/generate-i18n-messages.ts b/scripts/prepare/generateI18nMessages.ts similarity index 94% rename from scripts/generate-i18n-messages.ts rename to scripts/prepare/generateI18nMessages.ts index 8aa3cc9c..a127002e 100644 --- a/scripts/generate-i18n-messages.ts +++ b/scripts/prepare/generateI18nMessages.ts @@ -8,15 +8,12 @@ import { } from "path"; import { assert } from "tsafe/assert"; import { same } from "evt/tools/inDepth"; -import { crawl } from "../src/bin/tools/crawl"; -import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme"; -import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath"; -import { deepAssign } from "../src/tools/deepAssign"; -import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions"; -import { - THEME_TYPES, - LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 -} from "../src/bin/shared/constants"; +import { crawl } from "../../src/bin/tools/crawl"; +import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; +import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; +import { deepAssign } from "../../src/tools/deepAssign"; +import { THEME_TYPES } from "../../src/bin/shared/constants"; +import { KEYCLOAK_VERSION } from "./constants"; // NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files, // update the version array for generating for newer version. @@ -24,7 +21,7 @@ import { //@ts-ignore const propertiesParser = require("properties-parser"); -async function main() { +export async function generateI18nMessages() { const thisCodebaseRootDirPath = getThisCodebaseRootDirPath(); type Dictionary = { [idiomId: string]: string }; @@ -32,30 +29,19 @@ async function main() { const record: { [themeType: string]: { [language: string]: Dictionary } } = {}; for (const themeType of THEME_TYPES) { - const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ + const { extractedDirPath } = await downloadKeycloakDefaultTheme({ keycloakVersion: (() => { switch (themeType) { case "login": - return "25.0.4"; + return KEYCLOAK_VERSION.FOR_LOGIN_THEME; case "account": - return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1; + return KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE; } - })(), - buildContext: { - cacheDirPath: pathJoin( - thisCodebaseRootDirPath, - "node_modules", - ".cache", - "keycloakify" - ), - fetchOptions: getProxyFetchOptions({ - npmConfigGetCwd: thisCodebaseRootDirPath - }) - } + })() }); { - const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base"); + const baseThemeDirPath = pathJoin(extractedDirPath, "base"); const re = new RegExp( `^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$` ); @@ -597,30 +583,34 @@ const keycloakifyExtraMessages_login: Record< /* spell-checker: enable */ }; +export const accountMultiPageSupportedLanguages = [ + "en", + "ar", + "ca", + "cs", + "da", + "de", + "es", + "fi", + "fr", + "hu", + "it", + "ja", + "lt", + "lv", + "nl", + "no", + "pl", + "pt-BR", + "ru", + "sk", + "sv", + "tr", + "zh-CN" +] as const; + const keycloakifyExtraMessages_account: Record< - | "en" - | "ar" - | "ca" - | "cs" - | "da" - | "de" - | "es" - | "fi" - | "fr" - | "hu" - | "it" - | "ja" - | "lt" - | "lv" - | "nl" - | "no" - | "pl" - | "pt-BR" - | "ru" - | "sk" - | "sv" - | "tr" - | "zh-CN", + (typeof accountMultiPageSupportedLanguages)[number], Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string> > = { en: { @@ -719,7 +709,3 @@ const keycloakifyExtraMessages_account: Record< } /* spell-checker: enable */ }; - -if (require.main === module) { - main(); -} diff --git a/scripts/prepare/main.ts b/scripts/prepare/main.ts new file mode 100644 index 00000000..4b35dcf5 --- /dev/null +++ b/scripts/prepare/main.ts @@ -0,0 +1,13 @@ +import { generateI18nMessages } from "./generateI18nMessages"; +import { createAccountV1Dir } from "./createAccountV1Dir"; +import { createResourcesDir } from "./createResourcesDir"; + +(async () => { + console.log("Pulling i18n messages..."); + await generateI18nMessages(); + console.log("Creating account-v1 dir..."); + await createAccountV1Dir(); + console.log("Creating resources dir..."); + await createResourcesDir(); + console.log("Done!"); +})(); diff --git a/src/account/KcContext/kcContextMocks.ts b/src/account/KcContext/kcContextMocks.ts index 3f79faef..a80ed058 100644 --- a/src/account/KcContext/kcContextMocks.ts +++ b/src/account/KcContext/kcContextMocks.ts @@ -1,10 +1,10 @@ import "keycloakify/tools/Object.fromEntries"; -import { RESOURCES_COMMON, KEYCLOAK_RESOURCES } from "keycloakify/bin/shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants"; import { id } from "tsafe/id"; import type { KcContext } from "./KcContext"; import { BASE_URL } from "keycloakify/lib/BASE_URL"; -const resourcesPath = `${BASE_URL}${KEYCLOAK_RESOURCES}/account/resources`; +const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY}/account`; export const kcContextCommonMock: KcContext.Common = { themeVersion: "0.0.0", @@ -13,7 +13,7 @@ export const kcContextCommonMock: KcContext.Common = { themeName: "my-theme-name", url: { resourcesPath, - resourcesCommonPath: `${resourcesPath}/${RESOURCES_COMMON}`, + resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`, resourceUrl: "#", accountUrl: "#", applicationsUrl: "#", diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts index ff23cc61..b245076a 100644 --- a/src/bin/copy-keycloak-resources-to-public.ts +++ b/src/bin/copy-keycloak-resources-to-public.ts @@ -7,7 +7,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) const buildContext = getBuildContext({ cliCommandOptions }); - await copyKeycloakResourcesToPublic({ + copyKeycloakResourcesToPublic({ buildContext }); } diff --git a/src/bin/keycloakify/generateFtl/generateFtl.ts b/src/bin/keycloakify/generateFtl/generateFtl.ts index b47af42f..71331b7a 100644 --- a/src/bin/keycloakify/generateFtl/generateFtl.ts +++ b/src/bin/keycloakify/generateFtl/generateFtl.ts @@ -11,11 +11,7 @@ import * as fs from "fs"; import { join as pathJoin } from "path"; import type { BuildContext } from "../../shared/buildContext"; import { assert } from "tsafe/assert"; -import { - type ThemeType, - BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR, - RESOURCES_COMMON -} from "../../shared/constants"; +import { type ThemeType, WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants"; import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; export type BuildContextLike = BuildContextLike_replaceImportsInJsCode & @@ -94,7 +90,7 @@ export function generateFtlFilesCodeFactory(params: { new RegExp( `^${(buildContext.urlPathname ?? "/").replace(/\//g, "\\/")}` ), - `\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/` + `\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/` ) ); }) @@ -119,7 +115,7 @@ export function generateFtlFilesCodeFactory(params: { .replace("{{keycloakifyVersion}}", keycloakifyVersion) .replace("{{themeVersion}}", buildContext.themeVersion) .replace("{{fieldNames}}", fieldNames.map(name => `"${name}"`).join(", ")) - .replace("{{RESOURCES_COMMON}}", RESOURCES_COMMON) + .replace("{{RESOURCES_COMMON}}", WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON) .replace( "{{userDefinedExclusions}}", buildContext.kcContextExclusionsFtlCode ?? "" diff --git a/src/bin/keycloakify/generateResources/bringInAccountV1.ts b/src/bin/keycloakify/generateResources/bringInAccountV1.ts deleted file mode 100644 index eb34dfc3..00000000 --- a/src/bin/keycloakify/generateResources/bringInAccountV1.ts +++ /dev/null @@ -1,89 +0,0 @@ -import * as fs from "fs"; -import { join as pathJoin } from "path"; -import { assert } from "tsafe/assert"; -import type { BuildContext } from "../../shared/buildContext"; -import { - RESOURCES_COMMON, - LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1, - ACCOUNT_V1_THEME_NAME -} from "../../shared/constants"; -import { - downloadKeycloakDefaultTheme, - BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme -} from "../../shared/downloadKeycloakDefaultTheme"; -import { transformCodebase } from "../../tools/transformCodebase"; - -export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme; - -assert(); - -export async function bringInAccountV1(params: { - resourcesDirPath: string; - buildContext: BuildContextLike; -}) { - const { resourcesDirPath, buildContext } = params; - - const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion: LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1, - buildContext - }); - - const accountV1DirPath = pathJoin( - resourcesDirPath, - "theme", - ACCOUNT_V1_THEME_NAME, - "account" - ); - - transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "base", "account"), - destDirPath: accountV1DirPath - }); - - transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "account", "resources"), - destDirPath: pathJoin(accountV1DirPath, "resources") - }); - - transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"), - destDirPath: pathJoin(accountV1DirPath, "resources", RESOURCES_COMMON) - }); - - fs.writeFileSync( - pathJoin(accountV1DirPath, "theme.properties"), - Buffer.from( - [ - "accountResourceProvider=account-v1", - "", - "locales=ar,ca,cs,da,de,en,es,fr,fi,hu,it,ja,lt,nl,no,pl,pt-BR,ru,sk,sv,tr,zh-CN", - "", - "styles=" + - [ - "css/account.css", - "img/icon-sidebar-active.png", - "img/logo.png", - ...[ - "patternfly.min.css", - "patternfly-additions.min.css", - "patternfly-additions.min.css" - ].map( - fileBasename => - `${RESOURCES_COMMON}/node_modules/patternfly/dist/css/${fileBasename}` - ) - ].join(" "), - "", - "##### css classes for form buttons", - "# main class used for all buttons", - "kcButtonClass=btn", - "# classes defining priority of the button - primary or default (there is typically only one priority button for the form)", - "kcButtonPrimaryClass=btn-primary", - "kcButtonDefaultClass=btn-default", - "# classes defining size of the button", - "kcButtonLargeClass=btn-lg", - "" - ].join("\n"), - "utf8" - ) - ); -} diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index a9ca89d0..fc36cd57 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -1,11 +1,6 @@ import { transformCodebase } from "../../tools/transformCodebase"; import * as fs from "fs"; -import { - join as pathJoin, - resolve as pathResolve, - relative as pathRelative, - dirname as pathDirname -} from "path"; +import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path"; import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode"; import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode"; import { @@ -14,26 +9,15 @@ import { } from "../generateFtl"; import { type ThemeType, - LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1, - KEYCLOAK_RESOURCES, - ACCOUNT_V1_THEME_NAME, - BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR, LOGIN_THEME_PAGE_IDS, - ACCOUNT_THEME_PAGE_IDS + ACCOUNT_THEME_PAGE_IDS, + WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants"; import type { BuildContext } from "../../shared/buildContext"; import { assert, type Equals } from "tsafe/assert"; -import { - downloadKeycloakStaticResources, - type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources -} from "../../shared/downloadKeycloakStaticResources"; import { readFieldNameUsage } from "./readFieldNameUsage"; import { readExtraPagesNames } from "./readExtraPageNames"; import { generateMessageProperties } from "./generateMessageProperties"; -import { - bringInAccountV1, - type BuildContextLike as BuildContextLike_bringInAccountV1 -} from "./bringInAccountV1"; import { rmSync } from "../../tools/fs.rmSync"; import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion"; import { @@ -43,20 +27,18 @@ import { import { objectEntries } from "tsafe/objectEntries"; import { escapeStringForPropertiesFile } from "../../tools/escapeStringForPropertiesFile"; import * as child_process from "child_process"; +import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; -export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & - BuildContextLike_downloadKeycloakStaticResources & - BuildContextLike_bringInAccountV1 & { - extraThemeProperties: string[] | undefined; - loginThemeResourcesFromKeycloakVersion: string; - projectDirPath: string; - projectBuildDirPath: string; - environmentVariables: { name: string; default: string }[]; - implementedThemeTypes: BuildContext["implementedThemeTypes"]; - themeSrcDirPath: string; - bundler: "vite" | "webpack"; - packageJsonFilePath: string; - }; +export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & { + extraThemeProperties: string[] | undefined; + projectDirPath: string; + projectBuildDirPath: string; + environmentVariables: { name: string; default: string }[]; + implementedThemeTypes: BuildContext["implementedThemeTypes"]; + themeSrcDirPath: string; + bundler: "vite" | "webpack"; + packageJsonFilePath: string; +}; assert(); @@ -88,7 +70,7 @@ export async function generateResourcesForMainTheme(params: { const destDirPath = pathJoin( themeTypeDirPath, "resources", - BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR + WELL_KNOWN_DIRECTORY_BASE_NAME.DIST ); // NOTE: Prevent accumulation of files in the assets dir, as names are hashed they pile up. @@ -106,7 +88,7 @@ export async function generateResourcesForMainTheme(params: { themeType: "login" }), "resources", - BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR + WELL_KNOWN_DIRECTORY_BASE_NAME.DIST ), destDirPath }); @@ -117,7 +99,7 @@ export async function generateResourcesForMainTheme(params: { { const dirPath = pathJoin( buildContext.projectBuildDirPath, - KEYCLOAK_RESOURCES + WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY ); if (fs.existsSync(dirPath)) { @@ -125,7 +107,7 @@ export async function generateResourcesForMainTheme(params: { throw new Error( [ - `Keycloakify build error: The ${KEYCLOAK_RESOURCES} directory shouldn't exist in your build directory.`, + `Keycloakify build error: The ${WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY} directory shouldn't exist in your build directory.`, `(${pathRelative(process.cwd(), dirPath)}).\n`, `Theses assets are only required for local development with Storybook.", "Please remove this directory as an additional step of your command.\n`, @@ -232,23 +214,50 @@ export async function generateResourcesForMainTheme(params: { }); } + bring_in_account_v3_i18n_messages: { + if (!buildContext.implementedThemeTypes.account.isImplemented) { + break bring_in_account_v3_i18n_messages; + } + if (buildContext.implementedThemeTypes.account.type !== "Single-Page") { + break bring_in_account_v3_i18n_messages; + } + + const accountUiDirPath = child_process + .execSync("npm list @keycloakify/keycloak-account-ui --parseable", { + cwd: pathDirname(buildContext.packageJsonFilePath) + }) + .toString("utf8") + .trim(); + + const messagesDirPath = pathJoin(accountUiDirPath, "messages"); + + if (!fs.existsSync(messagesDirPath)) { + throw new Error( + `Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.` + ); + } + + transformCodebase({ + srcDirPath: messagesDirPath, + destDirPath: pathJoin( + getThemeTypeDirPath({ themeType: "account" }), + "messages" + ) + }); + } + keycloak_static_resources: { if (isForAccountSpa) { break keycloak_static_resources; } - await downloadKeycloakStaticResources({ - keycloakVersion: (() => { - switch (themeType) { - case "account": - return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1; - case "login": - return buildContext.loginThemeResourcesFromKeycloakVersion; - } - })(), - themeDirPath: pathResolve(pathJoin(themeTypeDirPath, "..")), - themeType, - buildContext + transformCodebase({ + srcDirPath: pathJoin( + getThisCodebaseRootDirPath(), + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, + themeType + ), + destDirPath: pathJoin(themeTypeDirPath, "resources") }); } @@ -259,7 +268,7 @@ export async function generateResourcesForMainTheme(params: { `parent=${(() => { switch (themeType) { case "account": - return isForAccountSpa ? "base" : ACCOUNT_V1_THEME_NAME; + return isForAccountSpa ? "base" : "account-v1"; case "login": return "keycloak"; } @@ -299,41 +308,12 @@ export async function generateResourcesForMainTheme(params: { break bring_in_account_v1; } - await bringInAccountV1({ - resourcesDirPath, - buildContext - }); - } - - bring_in_account_v3_i18n_messages: { - if (!buildContext.implementedThemeTypes.account.isImplemented) { - break bring_in_account_v3_i18n_messages; - } - if (buildContext.implementedThemeTypes.account.type !== "Single-Page") { - break bring_in_account_v3_i18n_messages; - } - - const accountUiDirPath = child_process - .execSync("npm list @keycloakify/keycloak-account-ui --parseable", { - cwd: pathDirname(buildContext.packageJsonFilePath) - }) - .toString("utf8") - .trim(); - - const messagesDirPath = pathJoin(accountUiDirPath, "messages"); - - if (!fs.existsSync(messagesDirPath)) { - throw new Error( - `Please update @keycloakify/keycloak-account-ui to 25.0.4-rc.5 or later.` - ); - } - transformCodebase({ - srcDirPath: messagesDirPath, - destDirPath: pathJoin( - getThemeTypeDirPath({ themeType: "account" }), - "messages" - ) + srcDirPath: pathJoin( + getThisCodebaseRootDirPath(), + WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 + ), + destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account") }); } @@ -349,7 +329,7 @@ export async function generateResourcesForMainTheme(params: { if (buildContext.implementedThemeTypes.account.isImplemented) { metaInfKeycloakThemes.themes.push({ - name: ACCOUNT_V1_THEME_NAME, + name: "account-v1", types: ["account"] }); } diff --git a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts index 707f6e5a..f15b078b 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInCssCode.ts @@ -1,5 +1,5 @@ import type { BuildContext } from "../../shared/buildContext"; -import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../shared/constants"; import { assert } from "tsafe/assert"; import { posix } from "path"; @@ -50,7 +50,7 @@ export function replaceImportsInCssCode(params: { break inline_style_in_html; } - return `url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}${assetFileAbsoluteUrlPathname}")`; + return `url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}${assetFileAbsoluteUrlPathname}")`; } const assetFileRelativeUrlPathname = posix.relative( diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts index 20048cf3..3830eb72 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/vite.ts @@ -1,4 +1,4 @@ -import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants"; import { assert } from "tsafe/assert"; import type { BuildContext } from "../../../shared/buildContext"; import * as nodePath from "path"; @@ -85,13 +85,13 @@ export function replaceImportsInJsCode_vite(params: { fixedJsCode = replaceAll( fixedJsCode, `"${relativePathOfAssetFile}"`, - `(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")` + `(window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")` ); fixedJsCode = replaceAll( fixedJsCode, `"${buildContext.urlPathname ?? "/"}${relativePathOfAssetFile}"`, - `(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${relativePathOfAssetFile}")` + `(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${relativePathOfAssetFile}")` ); }); } diff --git a/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts index 7c03125f..466822f0 100644 --- a/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts +++ b/src/bin/keycloakify/replacers/replaceImportsInJsCode/webpack.ts @@ -1,4 +1,4 @@ -import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "../../../shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../../shared/constants"; import { assert } from "tsafe/assert"; import type { BuildContext } from "../../../shared/buildContext"; import * as nodePath from "path"; @@ -90,7 +90,7 @@ export function replaceImportsInJsCode_webpack(params: { return "${u}"; })()] = ${ isArrowFunction ? `${e} =>` : `function(${e}) { return ` - } "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}${language}/"` + } "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}${language}/"` .replace(/\s+/g, " ") .trim(); } @@ -104,7 +104,7 @@ export function replaceImportsInJsCode_webpack(params: { `[a-zA-Z]+\\.[a-zA-Z]+\\+"${staticDir.replace(/\//g, "\\/")}`, "g" ), - `window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/${staticDir}` + `window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/${staticDir}` ); return { fixedJsCode }; diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts index 4c45d449..15d9773a 100644 --- a/src/bin/shared/buildContext.ts +++ b/src/bin/shared/buildContext.ts @@ -14,8 +14,7 @@ import { assert, type Equals } from "tsafe/assert"; import * as child_process from "child_process"; import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES, - BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME, - LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT + BUILD_FOR_KEYCLOAK_MAJOR_VERSION_ENV_NAME } from "./constants"; import type { KeycloakVersionRange } from "./KeycloakVersionRange"; import { exclude } from "tsafe"; @@ -33,7 +32,6 @@ export type BuildContext = { extraThemeProperties: string[] | undefined; groupId: string; artifactId: string; - loginThemeResourcesFromKeycloakVersion: string; projectDirPath: string; projectBuildDirPath: string; /** Directory that keycloakify outputs to. Defaults to {cwd}/build_keycloak */ @@ -85,7 +83,6 @@ export type BuildOptions = { extraThemeProperties?: string[]; artifactId?: string; groupId?: string; - loginThemeResourcesFromKeycloakVersion?: string; keycloakifyBuildDirPath?: string; kcContextExclusionsFtl?: string; startKeycloakOptions?: { @@ -357,7 +354,6 @@ export function getBuildContext(params: { extraThemeProperties: z.array(z.string()).optional(), artifactId: z.string().optional(), groupId: z.string().optional(), - loginThemeResourcesFromKeycloakVersion: z.string().optional(), keycloakifyBuildDirPath: z.string().optional(), kcContextExclusionsFtl: z.string().optional(), startKeycloakOptions: zStartKeycloakOptions.optional() @@ -545,9 +541,6 @@ export function getBuildContext(params: { process.env.KEYCLOAKIFY_ARTIFACT_ID ?? buildOptions.artifactId ?? `${themeNames[0]}-keycloak-theme`, - loginThemeResourcesFromKeycloakVersion: - buildOptions.loginThemeResourcesFromKeycloakVersion ?? - LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT, projectDirPath, projectBuildDirPath, keycloakifyBuildDirPath: (() => { diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index ad015654..1f3f2f84 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -1,10 +1,12 @@ -export const KEYCLOAK_RESOURCES = "keycloak-resources"; -export const RESOURCES_COMMON = "resources-common"; -export const LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 = "21.1.2"; -export const BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR = "dist"; +export const WELL_KNOWN_DIRECTORY_BASE_NAME = { + DOT_KEYCLOAKIFY: ".keycloakify", + RESOURCES_COMMON: "resources-common", + DIST: "dist", + RESOURCES: "resources", + ACCOUNT_V1: "account-v1" +}; export const THEME_TYPES = ["login", "account"] as const; -export const ACCOUNT_V1_THEME_NAME = "account-v1"; export type ThemeType = (typeof THEME_TYPES)[number]; @@ -71,5 +73,3 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number]; export const CONTAINER_NAME = "keycloak-keycloakify"; export const FALLBACK_LANGUAGE_TAG = "en"; - -export const LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT = "24.0.4"; diff --git a/src/bin/shared/copyKeycloakResourcesToPublic.ts b/src/bin/shared/copyKeycloakResourcesToPublic.ts index 5f4778ab..ff6b6241 100644 --- a/src/bin/shared/copyKeycloakResourcesToPublic.ts +++ b/src/bin/shared/copyKeycloakResourcesToPublic.ts @@ -1,44 +1,34 @@ -import { - downloadKeycloakStaticResources, - type BuildContextLike as BuildContextLike_downloadKeycloakStaticResources -} from "./downloadKeycloakStaticResources"; -import { join as pathJoin, relative as pathRelative } from "path"; -import { - THEME_TYPES, - KEYCLOAK_RESOURCES, - LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 -} from "../shared/constants"; +import { join as pathJoin, dirname as pathDirname } from "path"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants"; import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion"; import { assert } from "tsafe/assert"; import * as fs from "fs"; import { rmSync } from "../tools/fs.rmSync"; import type { BuildContext } from "./buildContext"; +import { transformCodebase } from "../tools/transformCodebase"; +import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath"; -export type BuildContextLike = BuildContextLike_downloadKeycloakStaticResources & { - loginThemeResourcesFromKeycloakVersion: string; +export type BuildContextLike = { publicDirPath: string; }; assert(); -export async function copyKeycloakResourcesToPublic(params: { +export function copyKeycloakResourcesToPublic(params: { buildContext: BuildContextLike; }) { const { buildContext } = params; - const destDirPath = pathJoin(buildContext.publicDirPath, KEYCLOAK_RESOURCES); + const destDirPath = pathJoin( + buildContext.publicDirPath, + WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY + ); const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo"); const keycloakifyBuildinfoRaw = JSON.stringify( { - destDirPath, - keycloakifyVersion: readThisNpmPackageVersion(), - buildContext: { - loginThemeResourcesFromKeycloakVersion: readThisNpmPackageVersion(), - cacheDirPath: pathRelative(destDirPath, buildContext.cacheDirPath), - fetchOptions: buildContext.fetchOptions - } + keycloakifyVersion: readThisNpmPackageVersion() }, null, 2 @@ -62,35 +52,33 @@ export async function copyKeycloakResourcesToPublic(params: { rmSync(destDirPath, { force: true, recursive: true }); + // NOTE: To remove in a while, remove the legacy keycloak-resources directory + rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), { + force: true, + recursive: true + }); + fs.mkdirSync(destDirPath, { recursive: true }); fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8")); - for (const themeType of THEME_TYPES) { - await downloadKeycloakStaticResources({ - keycloakVersion: (() => { - switch (themeType) { - case "login": - return buildContext.loginThemeResourcesFromKeycloakVersion; - case "account": - return LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1; - } - })(), - themeType, - themeDirPath: destDirPath, - buildContext - }); - } + transformCodebase({ + srcDirPath: pathJoin( + getThisCodebaseRootDirPath(), + WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES + ), + destDirPath + }); fs.writeFileSync( pathJoin(destDirPath, "README.txt"), Buffer.from( // prettier-ignore [ - "This is just a test folder that helps develop", - "the login and register page without having to run a Keycloak container\n", - "This directory will be automatically excluded from the final build." - ].join(" ") + "This directory is only used in dev mode by Keycloakify", + "It won't be included in your final build.", + "Do not modify anything in this directory.", + ].join("\n") ) ); diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts b/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts deleted file mode 100644 index 6988f121..00000000 --- a/src/bin/shared/downloadKeycloakDefaultTheme/downloadKeycloakDefaultTheme.ts +++ /dev/null @@ -1,338 +0,0 @@ -import { join as pathJoin, relative as pathRelative } from "path"; -import { type BuildContext } from "../buildContext"; -import { assert } from "tsafe/assert"; -import { LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 } from "../constants"; -import { downloadAndExtractArchive } from "../../tools/downloadAndExtractArchive"; -import { getThisCodebaseRootDirPath } from "../../tools/getThisCodebaseRootDirPath"; -import * as fsPr from "fs/promises"; - -export type BuildContextLike = { - cacheDirPath: string; - fetchOptions: BuildContext["fetchOptions"]; -}; - -assert(); - -export async function downloadKeycloakDefaultTheme(params: { - keycloakVersion: string; - buildContext: BuildContextLike; -}): Promise<{ defaultThemeDirPath: string }> { - const { keycloakVersion, buildContext } = params; - - let kcNodeModulesKeepFilePaths: Set | undefined = undefined; - let kcNodeModulesKeepFilePaths_lastAccountV1: Set | undefined = undefined; - - let areExtraAssetsFor24Copied = false; - - const { extractedDirPath } = await downloadAndExtractArchive({ - url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, - cacheDirPath: buildContext.cacheDirPath, - fetchOptions: buildContext.fetchOptions, - uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme", - onArchiveFile: async params => { - const fileRelativePath = pathRelative("theme", params.fileRelativePath); - - if (fileRelativePath.startsWith("..")) { - return; - } - - skip_keycloak_v2: { - if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) { - break skip_keycloak_v2; - } - - return; - } - - const { readFile, writeFile } = params; - - last_account_v1_transformations: { - if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) { - break last_account_v1_transformations; - } - - skip_web_modules: { - if ( - !fileRelativePath.startsWith( - pathJoin("keycloak", "common", "resources", "web_modules") - ) - ) { - break skip_web_modules; - } - - return; - } - - skip_lib: { - if ( - !fileRelativePath.startsWith( - pathJoin("keycloak", "common", "resources", "lib") - ) - ) { - break skip_lib; - } - - return; - } - - skip_node_modules: { - const nodeModulesRelativeDirPath = pathJoin( - "keycloak", - "common", - "resources", - "node_modules" - ); - - if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) { - break skip_node_modules; - } - - if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) { - kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([ - pathJoin("patternfly", "dist", "css", "patternfly.min.css"), - pathJoin( - "patternfly", - "dist", - "css", - "patternfly-additions.min.css" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Regular-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Bold-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Light-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Semibold-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "PatternFlyIcons-webfont.ttf" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "PatternFlyIcons-webfont.woff" - ) - ]); - } - - const fileRelativeToNodeModulesPath = fileRelativePath.substring( - nodeModulesRelativeDirPath.length + 1 - ); - - if ( - kcNodeModulesKeepFilePaths_lastAccountV1.has( - fileRelativeToNodeModulesPath - ) - ) { - break skip_node_modules; - } - - return; - } - - patch_account_css: { - if ( - fileRelativePath !== - pathJoin("keycloak", "account", "resources", "css", "account.css") - ) { - break patch_account_css; - } - - await writeFile({ - fileRelativePath, - modifiedData: Buffer.from( - (await readFile()) - .toString("utf8") - .replace("top: -34px;", "top: -34px !important;"), - "utf8" - ) - }); - - return; - } - } - - copy_extra_assets: { - if (keycloakVersion !== "24.0.4") { - break copy_extra_assets; - } - - if (areExtraAssetsFor24Copied) { - break copy_extra_assets; - } - - const extraAssetsDirPath = pathJoin( - getThisCodebaseRootDirPath(), - "src", - "bin", - "shared", - "downloadKeycloakDefaultTheme", - "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: { - if (keycloakVersion !== "24.0.4") { - break skip_unused_resources; - } - - skip_node_modules: { - const nodeModulesRelativeDirPath = pathJoin( - "keycloak", - "common", - "resources", - "node_modules" - ); - - if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) { - break skip_node_modules; - } - - if (kcNodeModulesKeepFilePaths === undefined) { - kcNodeModulesKeepFilePaths = new Set([ - pathJoin("@patternfly", "patternfly", "patternfly.min.css"), - pathJoin("patternfly", "dist", "css", "patternfly.min.css"), - pathJoin( - "patternfly", - "dist", - "css", - "patternfly-additions.min.css" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Regular-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Light-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Bold-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Bold-webfont.woff" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Bold-webfont.ttf" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "fontawesome-webfont.woff2" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "PatternFlyIcons-webfont.ttf" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "PatternFlyIcons-webfont.woff" - ), - pathJoin( - "patternfly", - "dist", - "fonts", - "OpenSans-Semibold-webfont.woff2" - ), - pathJoin("patternfly", "dist", "img", "bg-login.jpg"), - pathJoin("jquery", "dist", "jquery.min.js") - ]); - } - - const fileRelativeToNodeModulesPath = fileRelativePath.substring( - nodeModulesRelativeDirPath.length + 1 - ); - - if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) { - break skip_node_modules; - } - - return; - } - - skip_vendor: { - if ( - !fileRelativePath.startsWith( - pathJoin("keycloak", "common", "resources", "vendor") - ) - ) { - break skip_vendor; - } - - return; - } - - skip_rollup_config: { - if ( - fileRelativePath !== - pathJoin("keycloak", "common", "resources", "rollup.config.js") - ) { - break skip_rollup_config; - } - - return; - } - } - - await writeFile({ fileRelativePath }); - } - }); - - return { defaultThemeDirPath: extractedDirPath }; -} diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js deleted file mode 100644 index 5fcbb4ed..00000000 --- a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/passkeysConditionalAuth.js +++ /dev/null @@ -1,79 +0,0 @@ -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; -} \ No newline at end of file diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js b/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js deleted file mode 100644 index eaa05b69..00000000 --- a/src/bin/shared/downloadKeycloakDefaultTheme/extra-assets/webauthnAuthenticate.js +++ /dev/null @@ -1,82 +0,0 @@ -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(); -} \ No newline at end of file diff --git a/src/bin/shared/downloadKeycloakDefaultTheme/index.ts b/src/bin/shared/downloadKeycloakDefaultTheme/index.ts deleted file mode 100644 index c8acd724..00000000 --- a/src/bin/shared/downloadKeycloakDefaultTheme/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./downloadKeycloakDefaultTheme"; diff --git a/src/bin/shared/downloadKeycloakStaticResources.ts b/src/bin/shared/downloadKeycloakStaticResources.ts deleted file mode 100644 index a66b6272..00000000 --- a/src/bin/shared/downloadKeycloakStaticResources.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { transformCodebase } from "../tools/transformCodebase"; -import { join as pathJoin } from "path"; -import { - downloadKeycloakDefaultTheme, - type BuildContextLike as BuildContextLike_downloadKeycloakDefaultTheme -} from "./downloadKeycloakDefaultTheme"; -import { RESOURCES_COMMON, type ThemeType } from "./constants"; -import type { BuildContext } from "./buildContext"; -import { assert } from "tsafe/assert"; -import { existsAsync } from "../tools/fs.existsAsync"; - -export type BuildContextLike = BuildContextLike_downloadKeycloakDefaultTheme & {}; - -assert(); - -export async function downloadKeycloakStaticResources(params: { - themeType: ThemeType; - themeDirPath: string; - keycloakVersion: string; - buildContext: BuildContextLike; -}) { - const { themeType, themeDirPath, keycloakVersion, buildContext } = params; - - const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion, - buildContext - }); - - const resourcesDirPath = pathJoin(themeDirPath, themeType, "resources"); - - repatriate_base_resources: { - const srcDirPath = pathJoin(defaultThemeDirPath, "base", themeType, "resources"); - - if (!(await existsAsync(srcDirPath))) { - break repatriate_base_resources; - } - - transformCodebase({ - srcDirPath, - destDirPath: resourcesDirPath - }); - } - - transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", themeType, "resources"), - destDirPath: resourcesDirPath - }); - - transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "keycloak", "common", "resources"), - destDirPath: pathJoin(resourcesDirPath, RESOURCES_COMMON) - }); -} diff --git a/src/login/KcContext/kcContextMocks.ts b/src/login/KcContext/kcContextMocks.ts index 99343466..52122cf8 100644 --- a/src/login/KcContext/kcContextMocks.ts +++ b/src/login/KcContext/kcContextMocks.ts @@ -1,8 +1,7 @@ import "keycloakify/tools/Object.fromEntries"; import type { KcContext, Attribute } from "./KcContext"; import { - RESOURCES_COMMON, - KEYCLOAK_RESOURCES, + WELL_KNOWN_DIRECTORY_BASE_NAME, type LoginThemePageId } from "keycloakify/bin/shared/constants"; import { id } from "tsafe/id"; @@ -76,7 +75,7 @@ const attributesByName = Object.fromEntries( ]).map(attribute => [attribute.name, attribute]) ); -const resourcesPath = `${BASE_URL}${KEYCLOAK_RESOURCES}/login/resources`; +const resourcesPath = `${BASE_URL}${WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY}/login`; export const kcContextCommonMock: KcContext.Common = { themeVersion: "0.0.0", @@ -86,7 +85,7 @@ export const kcContextCommonMock: KcContext.Common = { url: { loginAction: "#", resourcesPath, - resourcesCommonPath: `${resourcesPath}/${RESOURCES_COMMON}`, + resourcesCommonPath: `${resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON}`, loginRestartFlowUrl: "#", loginUrl: "#", ssoLoginInOtherTabsUrl: "#" diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts index 3c5214b2..d773be8b 100644 --- a/src/vite-plugin/vite-plugin.ts +++ b/src/vite-plugin/vite-plugin.ts @@ -1,8 +1,7 @@ import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path"; import type { Plugin } from "vite"; import { - BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR, - KEYCLOAK_RESOURCES, + WELL_KNOWN_DIRECTORY_BASE_NAME, VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../bin/shared/constants"; import { id } from "tsafe/id"; @@ -128,14 +127,8 @@ export function keycloakify(params: keycloakify.Params) { } }); - await Promise.all([ - copyKeycloakResourcesToPublic({ - buildContext - }), - generateKcGenTs({ - buildContext - }) - ]); + copyKeycloakResourcesToPublic({ buildContext }), + await generateKcGenTs({ buildContext }); }, transform: (code, id) => { assert(command !== undefined); @@ -174,7 +167,7 @@ export function keycloakify(params: keycloakify.Params) { `(`, `(window.kcContext === undefined || import.meta.env.MODE === "development")?`, `"${urlPathname ?? "/"}":`, - `(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/")`, + `(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/")`, `)` ].join("") ); @@ -207,10 +200,13 @@ export function keycloakify(params: keycloakify.Params) { assert(buildDirPath !== undefined); - await rm(pathJoin(buildDirPath, KEYCLOAK_RESOURCES), { - recursive: true, - force: true - }); + await rm( + pathJoin(buildDirPath, WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY), + { + recursive: true, + force: true + } + ); } } satisfies Plugin; diff --git a/test/bin/replacers.spec.ts b/test/bin/replacers.spec.ts index 7732074e..cb36d686 100644 --- a/test/bin/replacers.spec.ts +++ b/test/bin/replacers.spec.ts @@ -2,7 +2,7 @@ import { replaceImportsInJsCode_vite } from "keycloakify/bin/keycloakify/replace import { replaceImportsInJsCode_webpack } from "keycloakify/bin/keycloakify/replacers/replaceImportsInJsCode/webpack"; import { replaceImportsInCssCode } from "keycloakify/bin/keycloakify/replacers/replaceImportsInCssCode"; import { expect, it, describe } from "vitest"; -import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "keycloakify/bin/shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants"; describe("js replacer - vite", () => { it("replaceImportsInJsCode_vite - 1", () => { @@ -87,13 +87,13 @@ describe("js replacer - vite", () => { }); const fixedJsCodeExpected = ` - S=(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ + S=(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ function __vite__mapDeps(indexes) { if (!__vite__mapDeps.viteFileDeps) { __vite__mapDeps.viteFileDeps = [ - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/Login-dJpPRzM4.js"), - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/index-XwzrZ5Gu.js") + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/Login-dJpPRzM4.js"), + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/index-XwzrZ5Gu.js") ] } return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) @@ -146,13 +146,13 @@ describe("js replacer - vite", () => { }); const fixedJsCodeExpected = ` - S=(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ + S=(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/foo/bar/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ function __vite__mapDeps(indexes) { if (!__vite__mapDeps.viteFileDeps) { __vite__mapDeps.viteFileDeps = [ - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/foo/bar/Login-dJpPRzM4.js"), - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/foo/bar/index-XwzrZ5Gu.js") + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/foo/bar/Login-dJpPRzM4.js"), + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/foo/bar/index-XwzrZ5Gu.js") ] } return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) @@ -205,13 +205,13 @@ describe("js replacer - vite", () => { }); const fixedJsCodeExpected = ` - S=(window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ + S=(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/keycloakify-logo-mqjydaoZ.png"),H=(()=>{ function __vite__mapDeps(indexes) { if (!__vite__mapDeps.viteFileDeps) { __vite__mapDeps.viteFileDeps = [ - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/Login-dJpPRzM4.js"), - (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/index-XwzrZ5Gu.js") + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/Login-dJpPRzM4.js"), + (window.kcContext["x-keycloakify"].resourcesPath.substring(1) + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/index-XwzrZ5Gu.js") ] } return indexes.map((i) => __vite__mapDeps.viteFileDeps[i]) @@ -267,13 +267,13 @@ describe("js replacer - webpack", () => { const fixedJsCodeExpected = ` function f() { - return window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/js/" + ({}[e] || e) + "." + { + return window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/js/" + ({}[e] || e) + "." + { 3: "0664cdc0" }[e] + ".chunk.js" } function sameAsF() { - return window.kcContext["x-keycloakify"].resourcesPath + "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/js/" + ({}[e] || e) + "." + { + return window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/js/" + ({}[e] || e) + "." + { 3: "0664cdc0" }[e] + ".chunk.js" } @@ -288,7 +288,7 @@ describe("js replacer - webpack", () => { } return "u"; })()] = function(e) { - return "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/js/" + e + "." + { + return "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/js/" + e + "." + { 147: "6c5cee76", 787: "8da10fcf", 922: "be170a73" @@ -305,7 +305,7 @@ describe("js replacer - webpack", () => { } return "miniCssF"; })()] = function(e) { - return "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/css/" + e + "." + { + return "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/css/" + e + "." + { 164:"dcfd7749", 908:"67c9ed2c" } [e] + ".chunk.css" @@ -320,7 +320,7 @@ describe("js replacer - webpack", () => { }); } return "u"; - })()] = e => "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js" + })()] = e => "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/js/"+e+"."+{69:"4f205f87",128:"49264537",453:"b2fed72e",482:"f0106901"}[e]+".chunk.js" t[(function(){ var pd = Object.getOwnPropertyDescriptor(t, "p"); @@ -331,7 +331,7 @@ describe("js replacer - webpack", () => { }); } return "miniCssF"; - })()] = e => "/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css" + })()] = e => "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/static/css/"+e+"."+{164:"dcfd7749",908:"67c9ed2c"}[e]+".chunk.css" `; expect(isSameCode(fixedJsCode, fixedJsCodeExpected)).toBe(true); @@ -495,15 +495,15 @@ describe("css replacer", () => { const fixedCssCodeExpected = ` .my-div { - background: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/background.png") no-repeat center center; + background: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/background.png") no-repeat center center; } .my-div2 { - background: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/background.png") repeat center center; + background: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/background.png") repeat center center; } .my-div3 { - background-image: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/media/something.svg"); + background-image: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/media/something.svg"); } `; @@ -533,15 +533,15 @@ describe("css replacer", () => { const fixedCssCodeExpected = ` .my-div { - background: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/background.png") no-repeat center center; + background: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/background.png") no-repeat center center; } .my-div2 { - background: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/background.png") repeat center center; + background: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/background.png") repeat center center; } .my-div3 { - background-image: url("\${xKeycloakify.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}/assets/media/something.svg"); + background-image: url("\${xKeycloakify.resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/assets/media/something.svg"); } `; From 77d3a5190d00d0398dbf1eb200ebcbe0ebc563a7 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 12:00:07 +0200 Subject: [PATCH 06/20] Refactor checkpoint --- .gitignore | 2 - .storybook/main.js | 2 +- .storybook/static/terms/en.md | 49 ----- .storybook/static/terms/es.md | 49 ----- .storybook/static/terms/fr.md | 49 ----- package.json | 4 +- scripts/build-storybook.ts | 4 - scripts/build.ts | 185 ----------------- .../{prepare => build}/createAccountV1Dir.ts | 14 +- .../createPublicDotKeycloakifyDir.ts} | 13 +- scripts/build/main.ts | 186 ++++++++++++++++++ ...pyKeycloakResourcesToStorybookStaticDir.ts | 18 -- ...nMessages.ts => generate-i18n-messages.ts} | 23 ++- scripts/grant-exec-perms.ts | 19 -- scripts/link-in-starter.ts | 2 +- scripts/prepare/main.ts | 13 -- scripts/{prepare => shared}/constants.ts | 0 .../downloadKeycloakDefaultTheme.ts | 2 +- .../{ => shared}/startRebuildOnSrcChange.ts | 0 src/PUBLIC_URL.ts | 8 +- src/bin/initialize-email-theme.ts | 31 ++- src/bin/keycloakify/buildJars/buildJar.ts | 10 +- .../generateResourcesForMainTheme.ts | 8 +- src/bin/shared/constants.ts | 6 +- .../shared/copyKeycloakResourcesToPublic.ts | 3 +- src/bin/start-keycloak/start-keycloak.ts | 10 +- 26 files changed, 259 insertions(+), 451 deletions(-) delete mode 100644 .storybook/static/terms/en.md delete mode 100644 .storybook/static/terms/es.md delete mode 100644 .storybook/static/terms/fr.md delete mode 100644 scripts/build.ts rename scripts/{prepare => build}/createAccountV1Dir.ts (86%) rename scripts/{prepare/createResourcesDir.ts => build/createPublicDotKeycloakifyDir.ts} (83%) create mode 100644 scripts/build/main.ts delete mode 100644 scripts/copyKeycloakResourcesToStorybookStaticDir.ts rename scripts/{prepare/generateI18nMessages.ts => generate-i18n-messages.ts} (97%) delete mode 100644 scripts/grant-exec-perms.ts delete mode 100644 scripts/prepare/main.ts rename scripts/{prepare => shared}/constants.ts (100%) rename scripts/{prepare => shared}/downloadKeycloakDefaultTheme.ts (95%) rename scripts/{ => shared}/startRebuildOnSrcChange.ts (100%) diff --git a/.gitignore b/.gitignore index dc5f3c97..687df547 100644 --- a/.gitignore +++ b/.gitignore @@ -50,8 +50,6 @@ jspm_packages /src/login/i18n/messages_defaultSet/ /src/account/i18n/messages_defaultSet/ -/resources/ -/account-v1/ # VS Code devcontainers .devcontainer diff --git a/.storybook/main.js b/.storybook/main.js index 876e6396..d33953e7 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -9,5 +9,5 @@ module.exports = { core: { builder: "webpack5" }, - staticDirs: ["./static"] + staticDirs: ["./static", "../dist/public"] }; diff --git a/.storybook/static/terms/en.md b/.storybook/static/terms/en.md deleted file mode 100644 index 1027b85c..00000000 --- a/.storybook/static/terms/en.md +++ /dev/null @@ -1,49 +0,0 @@ -## Overview - -This Terms of Service document outlines the rules and regulations for the use of **Example Company's** Services. - -## Acceptance of Terms - -By accessing and using our services, you acknowledge that you have read, understood, and agree to be bound by these terms. If you do not accept these terms, you are not authorized to use our services. - -## Description of Service - -**Example Service** (hereinafter referred to as "the Service") is a web-based solution offered by **Example Company** (hereinafter referred to as "the Company"). Our service provides users with access to [documentation](https://example.com/docs) and support for managing their projects effectively. - -## Modifications to the Terms of Service - -The Company reserves the right to modify these terms at any time. Such modifications will be effective immediately upon posting the updated terms on our website. Your continued use of the Service after any such changes shall constitute your consent to such changes. - -## Account Registration - -You may be required to register with the Service to access certain features. When registering, you agree to provide accurate, current, and complete information about yourself as requested. - -## User Responsibilities - -- **Data Security**: Users are responsible for safeguarding their login credentials and should not disclose their passwords to any third party. -- **Acceptable Use**: Users are expected to use the Service in a responsible manner that does not infringe upon the rights of others. -- **Content Ownership**: Users retain all rights to the content they upload to the Service but grant the Company a license to use and distribute this content as part of the Service. - -## Intellectual Property - -All intellectual property rights related to the Service and its original content, features, and functionality are owned by the Company. - -## Termination - -The Company may terminate or suspend access to our Service immediately, without prior notice or liability, for any reason whatsoever, including, without limitation, breach of these Terms. - -## Governing Law - -These Terms shall be governed and construed in accordance with the laws of [Your Country], without regard to its conflict of law provisions. - -## Contact Information - -For any questions about these Terms, please contact us at [support@example.com](mailto:support@example.com) or visit our [FAQ page](https://example.com/faq). - -## Changes to Terms of Service - -We reserve the right, at our sole discretion, to modify or replace these Terms at any time. If a revision is material, we will provide at least 30 days' notice prior to any new terms taking effect. - -## Effective Date - -These terms are effective as of **[Insert Date]**. diff --git a/.storybook/static/terms/es.md b/.storybook/static/terms/es.md deleted file mode 100644 index 588342a5..00000000 --- a/.storybook/static/terms/es.md +++ /dev/null @@ -1,49 +0,0 @@ -## Resumen - -Este documento de Términos de Servicio detalla las reglas y regulaciones para el uso de los servicios de **Empresa Ejemplo**. - -## Aceptación de Términos - -Al acceder y utilizar nuestros servicios, usted reconoce que ha leído, entendido y acepta estar vinculado por estos términos. Si no acepta estos términos, no está autorizado para usar nuestros servicios. - -## Descripción del Servicio - -**Servicio Ejemplo** (en adelante denominado "el Servicio") es una solución basada en la web ofrecida por **Empresa Ejemplo** (en adelante denominada "la Empresa"). Nuestro servicio proporciona a los usuarios acceso a [documentación](https://ejemplo.com/docs) y soporte para gestionar sus proyectos de manera efectiva. - -## Modificaciones a los Términos de Servicio - -La Empresa se reserva el derecho de modificar estos términos en cualquier momento. Dichas modificaciones entrarán en vigor inmediatamente después de la publicación de los términos actualizados en nuestro sitio web. Su uso continuado del Servicio después de tales cambios constituirá su consentimiento a dichos cambios. - -## Registro de Cuenta - -Puede ser necesario que se registre en el Servicio para acceder a ciertas características. Al registrarse, usted acepta proporcionar información precisa, actual y completa sobre sí mismo como se solicita. - -## Responsabilidades del Usuario - -- **Seguridad de Datos**: Los usuarios son responsables de salvaguardar sus credenciales de inicio de sesión y no deben divulgar sus contraseñas a terceros. -- **Uso Aceptable**: Se espera que los usuarios utilicen el Servicio de manera responsable que no infrinja los derechos de otros. -- **Propiedad del Contenido**: Los usuarios retienen todos los derechos sobre el contenido que cargan en el Servicio, pero otorgan a la Empresa una licencia para usar y distribuir este contenido como parte del Servicio. - -## Propiedad Intelectual - -Todos los derechos de propiedad intelectual relacionados con el Servicio y su contenido original, características y funcionalidad son propiedad de la Empresa. - -## Terminación - -La Empresa puede terminar o suspender su acceso a nuestro Servicio de inmediato, sin previo aviso ni responsabilidad, por cualquier motivo, incluido, entre otros, una violación de estos Términos. - -## Ley Aplicable - -Estos Términos se regirán e interpretarán de acuerdo con las leyes de [Su País], sin tener en cuenta sus disposiciones de conflicto de leyes. - -## Información de Contacto - -Para cualquier pregunta sobre estos Términos, contáctenos en [support@ejemplo.com](mailto:support@ejemplo.com) o visite nuestra [página de FAQ](https://ejemplo.com/faq). - -## Cambios a los Términos de Servicio - -Nos reservamos el derecho, a nuestra única discreción, de modificar o reemplazar estos Términos en cualquier momento. Si una revisión es material, proporcionaremos al menos 30 días de aviso antes de que los nuevos términos entren en vigor. - -## Fecha de Efectividad - -Estos términos son efectivos a partir del **[Insertar Fecha]**. diff --git a/.storybook/static/terms/fr.md b/.storybook/static/terms/fr.md deleted file mode 100644 index fe52ecc8..00000000 --- a/.storybook/static/terms/fr.md +++ /dev/null @@ -1,49 +0,0 @@ -## Vue d'ensemble - -Ce document des Conditions Générales d'Utilisation détaille les règles et réglementations pour l'utilisation des services de **l'Entreprise Exemple**. - -## Acceptation des Conditions - -En accédant et en utilisant nos services, vous reconnaissez avoir lu, compris et accepté d'être lié par ces conditions. Si vous n'acceptez pas ces termes, vous n'êtes pas autorisé à utiliser nos services. - -## Description du Service - -**Service Exemple** (ci-après dénommé "le Service") est une solution basée sur le web offerte par **l'Entreprise Exemple** (ci-après dénommée "l'Entreprise"). Notre service offre aux utilisateurs un accès à la [documentation](https://exemple.com/docs) et un support pour gérer efficacement leurs projets. - -## Modifications des Conditions de Service - -L'Entreprise se réserve le droit de modifier ces conditions à tout moment. De telles modifications entreront en vigueur immédiatement après la publication des termes mis à jour sur notre site web. Votre utilisation continue du Service après de tels changements constitue votre consentement à ces modifications. - -## Inscription au Compte - -Vous devrez peut-être vous inscrire au Service pour accéder à certaines fonctionnalités. Lors de l'inscription, vous acceptez de fournir des informations précises, actuelles et complètes vous concernant, comme demandé. - -## Responsabilités des Utilisateurs - -- **Sécurité des Données** : Les utilisateurs sont responsables de la sauvegarde de leurs identifiants de connexion et ne doivent divulguer leurs mots de passe à aucun tiers. -- **Utilisation Acceptable** : Les utilisateurs sont censés utiliser le Service de manière responsable qui ne porte pas atteinte aux droits d'autrui. -- **Propriété du Contenu** : Les utilisateurs conservent tous les droits sur le contenu qu'ils téléchargent sur le Service mais accordent à l'Entreprise une licence pour utiliser et distribuer ce contenu dans le cadre du Service. - -## Propriété Intellectuelle - -Tous les droits de propriété intellectuelle relatifs au Service et à son contenu original, fonctionnalités et fonctionnement sont détenus par l'Entreprise. - -## Résiliation - -L'Entreprise peut résilier ou suspendre votre accès à notre Service immédiatement, sans préavis ni responsabilité, pour quelque raison que ce soit, y compris, sans limitation, en cas de violation de ces Conditions. - -## Loi Applicable - -Ces Conditions seront régies et interprétées conformément aux lois de [Votre Pays], sans égard à ses dispositions de conflit de lois. - -## Informations de Contact - -Pour toute question concernant ces Conditions, veuillez nous contacter à [support@exemple.com](mailto:support@exemple.com) ou visitez notre [page FAQ](https://exemple.com/faq). - -## Modifications des Conditions de Service - -Nous nous réservons le droit, à notre seule discrétion, de modifier ou de remplacer ces Conditions à tout moment. Si une révision est importante, nous vous fournirons un préavis d'au moins 30 jours avant que les nouveaux termes prennent effet. - -## Date d'Effet - -Ces conditions sont effectives à partir du **[Insérer la Date]**. diff --git a/package.json b/package.json index 3c5b32fa..52d23717 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,8 @@ "url": "git://github.com/keycloakify/keycloakify.git" }, "scripts": { - "prepare": "tsx scripts/prepare/main.ts", - "build": "tsx scripts/build.ts", + "prepare": "tsx scripts/generate-i18n-messages.ts", + "build": "tsx scripts/build/main.ts", "storybook": "tsx scripts/start-storybook.ts", "link-in-starter": "tsx scripts/link-in-starter.ts", "test": "yarn test:types && vitest run", diff --git a/scripts/build-storybook.ts b/scripts/build-storybook.ts index 7f617736..00f4167d 100644 --- a/scripts/build-storybook.ts +++ b/scripts/build-storybook.ts @@ -1,11 +1,7 @@ import * as child_process from "child_process"; -import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir"; (async () => { run("yarn build"); - - await copyKeycloakResourcesToStorybookStaticDir(); - run("npx build-storybook"); })(); diff --git a/scripts/build.ts b/scripts/build.ts deleted file mode 100644 index 583d135f..00000000 --- a/scripts/build.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as child_process from "child_process"; -import * as fs from "fs"; -import { join } from "path"; -import { assert } from "tsafe/assert"; -import { transformCodebase } from "../src/bin/tools/transformCodebase"; -import chalk from "chalk"; -import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../src/bin/shared/constants"; - -console.log(chalk.cyan("Building Keycloakify...")); - -const startTime = Date.now(); - -if (fs.existsSync(join("dist", "bin", "main.original.js"))) { - fs.renameSync( - join("dist", "bin", "main.original.js"), - join("dist", "bin", "main.js") - ); - - fs.readdirSync(join("dist", "bin")).forEach(fileBasename => { - if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) { - fs.rmSync(join("dist", "bin", fileBasename)); - } - }); -} - -run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`); - -if ( - !fs - .readFileSync(join("dist", "bin", "main.js")) - .toString("utf8") - .includes("__nccwpck_require__") -) { - fs.cpSync(join("dist", "bin", "main.js"), join("dist", "bin", "main.original.js")); -} - -run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`); - -transformCodebase({ - srcDirPath: join("dist", "ncc_out"), - destDirPath: join("dist", "bin"), - transformSourceCode: ({ fileRelativePath, sourceCode }) => { - if (fileRelativePath === "index.js") { - return { - newFileName: "main.js", - modifiedSourceCode: sourceCode - }; - } - - return { modifiedSourceCode: sourceCode }; - } -}); - -fs.rmSync(join("dist", "ncc_out"), { recursive: true }); - -{ - let hasBeenPatched = false; - - fs.readdirSync(join("dist", "bin")).forEach(fileBasename => { - if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) { - return; - } - - const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage( - join("dist", "bin", fileBasename) - ); - - if (hasBeenPatched_i) { - hasBeenPatched = true; - } - }); - - assert(hasBeenPatched); -} - -fs.chmodSync( - join("dist", "bin", "main.js"), - fs.statSync(join("dist", "bin", "main.js")).mode | - fs.constants.S_IXUSR | - fs.constants.S_IXGRP | - fs.constants.S_IXOTH -); - -run(`npx tsc -p ${join("src", "tsconfig.json")}`); -run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`); - -if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) { - fs.renameSync( - join("dist", "vite-plugin", "index.original.js"), - join("dist", "vite-plugin", "index.js") - ); -} - -run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`); - -if ( - !fs - .readFileSync(join("dist", "vite-plugin", "index.js")) - .toString("utf8") - .includes("__nccwpck_require__") -) { - fs.cpSync( - join("dist", "vite-plugin", "index.js"), - join("dist", "vite-plugin", "index.original.js") - ); -} - -run( - `npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join( - "dist", - "ncc_out" - )}` -); - -fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => { - assert(!fileBasename.endsWith(".index.js")); - assert(!fileBasename.endsWith(".node")); -}); - -transformCodebase({ - srcDirPath: join("dist", "ncc_out"), - destDirPath: join("dist", "vite-plugin"), - transformSourceCode: ({ fileRelativePath, sourceCode }) => { - assert(fileRelativePath === "index.js"); - - return { modifiedSourceCode: sourceCode }; - } -}); - -fs.rmSync(join("dist", "ncc_out"), { recursive: true }); - -{ - const { hasBeenPatched } = patchDeprecatedBufferApiUsage( - join("dist", "vite-plugin", "index.js") - ); - - assert(hasBeenPatched); -} - -for (const dirBasename of [ - "src", - WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, - WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 -]) { - const destDirPath = join("dist", dirBasename); - - fs.rmSync(destDirPath, { recursive: true, force: true }); - - fs.cpSync(dirBasename, destDirPath, { recursive: true }); -} - -transformCodebase({ - srcDirPath: join("stories"), - destDirPath: join("dist", "stories"), - transformSourceCode: ({ fileRelativePath, sourceCode }) => { - if (!fileRelativePath.endsWith(".stories.tsx")) { - return undefined; - } - - return { modifiedSourceCode: sourceCode }; - } -}); - -console.log(chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`)); - -function run(command: string) { - console.log(chalk.grey(`$ ${command}`)); - - child_process.execSync(command, { stdio: "inherit" }); -} - -function patchDeprecatedBufferApiUsage(filePath: string) { - const before = fs.readFileSync(filePath).toString("utf8"); - - const after = before.replace( - `var buffer = new Buffer(toRead);`, - `var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);` - ); - - fs.writeFileSync(filePath, Buffer.from(after, "utf8")); - - const hasBeenPatched = after !== before; - - return { hasBeenPatched }; -} diff --git a/scripts/prepare/createAccountV1Dir.ts b/scripts/build/createAccountV1Dir.ts similarity index 86% rename from scripts/prepare/createAccountV1Dir.ts rename to scripts/build/createAccountV1Dir.ts index 19a7e91f..b58435b2 100644 --- a/scripts/prepare/createAccountV1Dir.ts +++ b/scripts/build/createAccountV1Dir.ts @@ -1,23 +1,21 @@ import * as fs from "fs"; import { join as pathJoin } from "path"; -import { KEYCLOAK_VERSION } from "./constants"; +import { KEYCLOAK_VERSION } from "../shared/constants"; import { transformCodebase } from "../../src/bin/tools/transformCodebase"; -import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; +import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme"; import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants"; import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; -import { accountMultiPageSupportedLanguages } from "./generateI18nMessages"; +import { accountMultiPageSupportedLanguages } from "../generate-i18n-messages"; +import * as fsPr from "fs/promises"; export async function createAccountV1Dir() { const { extractedDirPath } = await downloadKeycloakDefaultTheme({ keycloakVersion: KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE }); - // TODO: Exclude unused resources. + const destDirPath = pathJoin(getThisCodebaseRootDirPath(), "dist", "account-v1"); - const destDirPath = pathJoin( - getThisCodebaseRootDirPath(), - WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 - ); + await fsPr.rm(destDirPath, { recursive: true, force: true }); transformCodebase({ srcDirPath: pathJoin(extractedDirPath, "base", "account"), diff --git a/scripts/prepare/createResourcesDir.ts b/scripts/build/createPublicDotKeycloakifyDir.ts similarity index 83% rename from scripts/prepare/createResourcesDir.ts rename to scripts/build/createPublicDotKeycloakifyDir.ts index 1fb13f54..262c4f44 100644 --- a/scripts/prepare/createResourcesDir.ts +++ b/scripts/build/createPublicDotKeycloakifyDir.ts @@ -1,13 +1,14 @@ import { join as pathJoin } from "path"; -import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; -import { KEYCLOAK_VERSION } from "./constants"; +import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme"; +import { KEYCLOAK_VERSION } from "../shared/constants"; import { transformCodebase } from "../../src/bin/tools/transformCodebase"; import { existsAsync } from "../../src/bin/tools/fs.existsAsync"; import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants"; import { assert, type Equals } from "tsafe/assert"; +import * as fsPr from "fs/promises"; -export async function createResourcesDir() { +export async function createPublicDotKeycloakifyDir() { await Promise.all( (["login", "account"] as const).map(async themeType => { const keycloakVersion = (() => { @@ -26,10 +27,14 @@ export async function createResourcesDir() { const destDirPath = pathJoin( getThisCodebaseRootDirPath(), - WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, + "dist", + "public", + WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY, themeType ); + await fsPr.rm(destDirPath, { recursive: true, force: true }); + base_resources: { const srcDirPath = pathJoin( extractedDirPath, diff --git a/scripts/build/main.ts b/scripts/build/main.ts new file mode 100644 index 00000000..40896817 --- /dev/null +++ b/scripts/build/main.ts @@ -0,0 +1,186 @@ +import * as child_process from "child_process"; +import * as fs from "fs"; +import { join } from "path"; +import { assert } from "tsafe/assert"; +import { transformCodebase } from "../../src/bin/tools/transformCodebase"; +import { createPublicDotKeycloakifyDir } from "./createPublicDotKeycloakifyDir"; +import { createAccountV1Dir } from "./createAccountV1Dir"; +import chalk from "chalk"; + +(async () => { + console.log(chalk.cyan("Building Keycloakify...")); + + const startTime = Date.now(); + + if (fs.existsSync(join("dist", "bin", "main.original.js"))) { + fs.renameSync( + join("dist", "bin", "main.original.js"), + join("dist", "bin", "main.js") + ); + + fs.readdirSync(join("dist", "bin")).forEach(fileBasename => { + if (/[0-9]\.index.js/.test(fileBasename) || fileBasename.endsWith(".node")) { + fs.rmSync(join("dist", "bin", fileBasename)); + } + }); + } + + run(`npx tsc -p ${join("src", "bin", "tsconfig.json")}`); + + if ( + !fs + .readFileSync(join("dist", "bin", "main.js")) + .toString("utf8") + .includes("__nccwpck_require__") + ) { + fs.cpSync( + join("dist", "bin", "main.js"), + join("dist", "bin", "main.original.js") + ); + } + + run(`npx ncc build ${join("dist", "bin", "main.js")} -o ${join("dist", "ncc_out")}`); + + transformCodebase({ + srcDirPath: join("dist", "ncc_out"), + destDirPath: join("dist", "bin"), + transformSourceCode: ({ fileRelativePath, sourceCode }) => { + if (fileRelativePath === "index.js") { + return { + newFileName: "main.js", + modifiedSourceCode: sourceCode + }; + } + + return { modifiedSourceCode: sourceCode }; + } + }); + + fs.rmSync(join("dist", "ncc_out"), { recursive: true }); + + { + let hasBeenPatched = false; + + fs.readdirSync(join("dist", "bin")).forEach(fileBasename => { + if (fileBasename !== "main.js" && !fileBasename.endsWith(".index.js")) { + return; + } + + const { hasBeenPatched: hasBeenPatched_i } = patchDeprecatedBufferApiUsage( + join("dist", "bin", fileBasename) + ); + + if (hasBeenPatched_i) { + hasBeenPatched = true; + } + }); + + assert(hasBeenPatched); + } + + fs.chmodSync( + join("dist", "bin", "main.js"), + fs.statSync(join("dist", "bin", "main.js")).mode | + fs.constants.S_IXUSR | + fs.constants.S_IXGRP | + fs.constants.S_IXOTH + ); + + run(`npx tsc -p ${join("src", "tsconfig.json")}`); + run(`npx tsc-alias -p ${join("src", "tsconfig.json")}`); + + if (fs.existsSync(join("dist", "vite-plugin", "index.original.js"))) { + fs.renameSync( + join("dist", "vite-plugin", "index.original.js"), + join("dist", "vite-plugin", "index.js") + ); + } + + run(`npx tsc -p ${join("src", "vite-plugin", "tsconfig.json")}`); + + if ( + !fs + .readFileSync(join("dist", "vite-plugin", "index.js")) + .toString("utf8") + .includes("__nccwpck_require__") + ) { + fs.cpSync( + join("dist", "vite-plugin", "index.js"), + join("dist", "vite-plugin", "index.original.js") + ); + } + + run( + `npx ncc build ${join("dist", "vite-plugin", "index.js")} -o ${join( + "dist", + "ncc_out" + )}` + ); + + fs.readdirSync(join("dist", "ncc_out")).forEach(fileBasename => { + assert(!fileBasename.endsWith(".index.js")); + assert(!fileBasename.endsWith(".node")); + }); + + transformCodebase({ + srcDirPath: join("dist", "ncc_out"), + destDirPath: join("dist", "vite-plugin"), + transformSourceCode: ({ fileRelativePath, sourceCode }) => { + assert(fileRelativePath === "index.js"); + + return { modifiedSourceCode: sourceCode }; + } + }); + + fs.rmSync(join("dist", "ncc_out"), { recursive: true }); + + { + const dirBasename = "src"; + + const destDirPath = join("dist", dirBasename); + + fs.rmSync(destDirPath, { recursive: true, force: true }); + + fs.cpSync(dirBasename, destDirPath, { recursive: true }); + } + + await createPublicDotKeycloakifyDir(); + await createAccountV1Dir(); + + transformCodebase({ + srcDirPath: join("stories"), + destDirPath: join("dist", "stories"), + transformSourceCode: ({ fileRelativePath, sourceCode }) => { + if (!fileRelativePath.endsWith(".stories.tsx")) { + return undefined; + } + + return { modifiedSourceCode: sourceCode }; + } + }); + + console.log( + chalk.green(`✓ built in ${((Date.now() - startTime) / 1000).toFixed(2)}s`) + ); +})(); + +function run(command: string) { + console.log(chalk.grey(`$ ${command}`)); + + child_process.execSync(command, { stdio: "inherit" }); +} + +function patchDeprecatedBufferApiUsage(filePath: string) { + const before = fs.readFileSync(filePath).toString("utf8"); + + const after = before.replace( + `var buffer = new Buffer(toRead);`, + `var buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(toRead) : new Buffer(toRead);` + ); + + fs.writeFileSync(filePath, Buffer.from(after, "utf8")); + + const hasBeenPatched = after !== before; + + return { hasBeenPatched }; +} diff --git a/scripts/copyKeycloakResourcesToStorybookStaticDir.ts b/scripts/copyKeycloakResourcesToStorybookStaticDir.ts deleted file mode 100644 index fd6b4368..00000000 --- a/scripts/copyKeycloakResourcesToStorybookStaticDir.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { join as pathJoin } from "path"; -import { copyKeycloakResourcesToPublic } from "../src/bin/shared/copyKeycloakResourcesToPublic"; -import { getProxyFetchOptions } from "../src/bin/tools/fetchProxyOptions"; -import { LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT } from "../src/bin/shared/constants"; - -export async function copyKeycloakResourcesToStorybookStaticDir() { - await copyKeycloakResourcesToPublic({ - buildContext: { - cacheDirPath: pathJoin(__dirname, "..", "node_modules", ".cache", "scripts"), - fetchOptions: getProxyFetchOptions({ - npmConfigGetCwd: pathJoin(__dirname, "..") - }), - loginThemeResourcesFromKeycloakVersion: - LOGIN_THEME_RESOURCES_FROM_KEYCLOAK_VERSION_DEFAULT, - publicDirPath: pathJoin(__dirname, "..", ".storybook", "static") - } - }); -} diff --git a/scripts/prepare/generateI18nMessages.ts b/scripts/generate-i18n-messages.ts similarity index 97% rename from scripts/prepare/generateI18nMessages.ts rename to scripts/generate-i18n-messages.ts index a127002e..39beaba7 100644 --- a/scripts/prepare/generateI18nMessages.ts +++ b/scripts/generate-i18n-messages.ts @@ -8,20 +8,19 @@ import { } from "path"; import { assert } from "tsafe/assert"; import { same } from "evt/tools/inDepth"; -import { crawl } from "../../src/bin/tools/crawl"; -import { downloadKeycloakDefaultTheme } from "./downloadKeycloakDefaultTheme"; -import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; -import { deepAssign } from "../../src/tools/deepAssign"; -import { THEME_TYPES } from "../../src/bin/shared/constants"; -import { KEYCLOAK_VERSION } from "./constants"; +import { crawl } from "../src/bin/tools/crawl"; +import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme"; +import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath"; +import { deepAssign } from "../src/tools/deepAssign"; +import { THEME_TYPES } from "../src/bin/shared/constants"; +import { KEYCLOAK_VERSION } from "./shared/constants"; +const propertiesParser: any = require("properties-parser"); -// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files, -// update the version array for generating for newer version. +if (require.main === module) { + generateI18nMessages(); +} -//@ts-ignore -const propertiesParser = require("properties-parser"); - -export async function generateI18nMessages() { +async function generateI18nMessages() { const thisCodebaseRootDirPath = getThisCodebaseRootDirPath(); type Dictionary = { [idiomId: string]: string }; diff --git a/scripts/grant-exec-perms.ts b/scripts/grant-exec-perms.ts deleted file mode 100644 index 871eae65..00000000 --- a/scripts/grant-exec-perms.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { join as pathJoin } from "path"; -import { constants } from "fs"; -import { chmod, stat } from "fs/promises"; - -(async () => { - const thisCodebaseRootDirPath = pathJoin(__dirname, ".."); - - const { bin } = await import(pathJoin(thisCodebaseRootDirPath, "package.json")); - - const promises = Object.values(bin).map(async scriptPath => { - const fullPath = pathJoin(thisCodebaseRootDirPath, scriptPath); - const oldMode = (await stat(fullPath)).mode; - const newMode = - oldMode | constants.S_IXUSR | constants.S_IXGRP | constants.S_IXOTH; - await chmod(fullPath, newMode); - }); - - await Promise.all(promises); -})(); diff --git a/scripts/link-in-starter.ts b/scripts/link-in-starter.ts index 3c9c84eb..85117cf9 100644 --- a/scripts/link-in-starter.ts +++ b/scripts/link-in-starter.ts @@ -1,7 +1,7 @@ import * as child_process from "child_process"; import * as fs from "fs"; import { join } from "path"; -import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange"; +import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange"; import { crawl } from "../src/bin/tools/crawl"; { diff --git a/scripts/prepare/main.ts b/scripts/prepare/main.ts deleted file mode 100644 index 4b35dcf5..00000000 --- a/scripts/prepare/main.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { generateI18nMessages } from "./generateI18nMessages"; -import { createAccountV1Dir } from "./createAccountV1Dir"; -import { createResourcesDir } from "./createResourcesDir"; - -(async () => { - console.log("Pulling i18n messages..."); - await generateI18nMessages(); - console.log("Creating account-v1 dir..."); - await createAccountV1Dir(); - console.log("Creating resources dir..."); - await createResourcesDir(); - console.log("Done!"); -})(); diff --git a/scripts/prepare/constants.ts b/scripts/shared/constants.ts similarity index 100% rename from scripts/prepare/constants.ts rename to scripts/shared/constants.ts diff --git a/scripts/prepare/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts similarity index 95% rename from scripts/prepare/downloadKeycloakDefaultTheme.ts rename to scripts/shared/downloadKeycloakDefaultTheme.ts index 7848e969..953cac66 100644 --- a/scripts/prepare/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -18,7 +18,7 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st fetchOptions: getProxyFetchOptions({ npmConfigGetCwd: getThisCodebaseRootDirPath() }), - uniqueIdOfOnArchiveFile: "downloadKeycloakDefaultTheme", + uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles", onArchiveFile: async ({ fileRelativePath, writeFile }) => { const fileRelativePath_target = pathRelative("theme", fileRelativePath); diff --git a/scripts/startRebuildOnSrcChange.ts b/scripts/shared/startRebuildOnSrcChange.ts similarity index 100% rename from scripts/startRebuildOnSrcChange.ts rename to scripts/shared/startRebuildOnSrcChange.ts diff --git a/src/PUBLIC_URL.ts b/src/PUBLIC_URL.ts index 6ed04151..4609bc83 100644 --- a/src/PUBLIC_URL.ts +++ b/src/PUBLIC_URL.ts @@ -1,4 +1,4 @@ -import { BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR } from "keycloakify/bin/shared/constants"; +import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "keycloakify/bin/shared/constants"; import { assert } from "tsafe/assert"; /** @@ -6,7 +6,9 @@ import { assert } from "tsafe/assert"; * This works both in your main app and in your Keycloak theme. */ export const PUBLIC_URL = (() => { - const kcContext = (window as any).kcContext; + const kcContext: { "x-keycloakify": { resourcesPath: string } } | undefined = ( + window as any + ).kcContext; if (kcContext === undefined || process.env.NODE_ENV === "development") { assert( @@ -17,5 +19,5 @@ export const PUBLIC_URL = (() => { return process.env.PUBLIC_URL; } - return `${kcContext.url.resourcesPath}/${BASENAME_OF_KEYCLOAKIFY_RESOURCES_DIR}`; + return `${kcContext["x-keycloakify"].resourcesPath}/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}`; })(); diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts index 43dd7a89..14b601ed 100644 --- a/src/bin/initialize-email-theme.ts +++ b/src/bin/initialize-email-theme.ts @@ -1,10 +1,10 @@ -import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme"; import { join as pathJoin, relative as pathRelative } from "path"; import { transformCodebase } from "./tools/transformCodebase"; import { promptKeycloakVersion } from "./shared/promptKeycloakVersion"; import { getBuildContext } from "./shared/buildContext"; import * as fs from "fs"; import type { CliCommandOptions } from "./main"; +import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive"; export async function command(params: { cliCommandOptions: CliCommandOptions }) { const { cliCommandOptions } = params; @@ -13,9 +13,12 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email"); - if (fs.existsSync(emailThemeSrcDirPath)) { + if ( + fs.existsSync(emailThemeSrcDirPath) && + fs.readdirSync(emailThemeSrcDirPath).length > 0 + ) { console.warn( - `There is already a ${pathRelative( + `There is already a non empty ${pathRelative( process.cwd(), emailThemeSrcDirPath )} directory in your project. Aborting.` @@ -34,13 +37,27 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) buildContext }); - const { defaultThemeDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion, - buildContext + const { extractedDirPath } = await downloadAndExtractArchive({ + url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, + cacheDirPath: buildContext.cacheDirPath, + fetchOptions: buildContext.fetchOptions, + uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme", + onArchiveFile: async ({ fileRelativePath, writeFile }) => { + const fileRelativePath_target = pathRelative( + pathJoin("theme", "base", "email"), + fileRelativePath + ); + + if (fileRelativePath_target.startsWith("..")) { + return; + } + + await writeFile({ fileRelativePath: fileRelativePath_target }); + } }); transformCodebase({ - srcDirPath: pathJoin(defaultThemeDirPath, "base", "email"), + srcDirPath: extractedDirPath, destDirPath: emailThemeSrcDirPath }); diff --git a/src/bin/keycloakify/buildJars/buildJar.ts b/src/bin/keycloakify/buildJars/buildJar.ts index c8821e1f..49e40558 100644 --- a/src/bin/keycloakify/buildJars/buildJar.ts +++ b/src/bin/keycloakify/buildJars/buildJar.ts @@ -7,7 +7,6 @@ import { join as pathJoin, dirname as pathDirname } from "path"; import { transformCodebase } from "../../tools/transformCodebase"; import type { BuildContext } from "../../shared/buildContext"; import * as fs from "fs/promises"; -import { ACCOUNT_V1_THEME_NAME } from "../../shared/constants"; import { generatePom, BuildContextLike as BuildContextLike_generatePom @@ -75,7 +74,7 @@ export async function buildJar(params: { if ( isInside({ - dirPath: pathJoin("theme", ACCOUNT_V1_THEME_NAME), + dirPath: pathJoin("theme", "account-v1"), filePath: fileRelativePath }) ) { @@ -90,10 +89,7 @@ export async function buildJar(params: { const modifiedSourceCode = Buffer.from( sourceCode .toString("utf8") - .replace( - `parent=${ACCOUNT_V1_THEME_NAME}`, - "parent=keycloak" - ), + .replace(`parent=account-v1`, "parent=keycloak"), "utf8" ); @@ -126,7 +122,7 @@ export async function buildJar(params: { assert(metaInfKeycloakTheme !== undefined); metaInfKeycloakTheme.themes = metaInfKeycloakTheme.themes.filter( - ({ name }) => name !== ACCOUNT_V1_THEME_NAME + ({ name }) => name !== "account-v1" ); return metaInfKeycloakTheme; diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index fc36cd57..fc0c772c 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -254,7 +254,8 @@ export async function generateResourcesForMainTheme(params: { transformCodebase({ srcDirPath: pathJoin( getThisCodebaseRootDirPath(), - WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES, + "public", + WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY, themeType ), destDirPath: pathJoin(themeTypeDirPath, "resources") @@ -309,10 +310,7 @@ export async function generateResourcesForMainTheme(params: { } transformCodebase({ - srcDirPath: pathJoin( - getThisCodebaseRootDirPath(), - WELL_KNOWN_DIRECTORY_BASE_NAME.ACCOUNT_V1 - ), + srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "account-v1"), destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account") }); } diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts index 1f3f2f84..adf6e3fc 100644 --- a/src/bin/shared/constants.ts +++ b/src/bin/shared/constants.ts @@ -1,10 +1,8 @@ export const WELL_KNOWN_DIRECTORY_BASE_NAME = { DOT_KEYCLOAKIFY: ".keycloakify", RESOURCES_COMMON: "resources-common", - DIST: "dist", - RESOURCES: "resources", - ACCOUNT_V1: "account-v1" -}; + DIST: "dist" +} as const; export const THEME_TYPES = ["login", "account"] as const; diff --git a/src/bin/shared/copyKeycloakResourcesToPublic.ts b/src/bin/shared/copyKeycloakResourcesToPublic.ts index ff6b6241..e4233c9e 100644 --- a/src/bin/shared/copyKeycloakResourcesToPublic.ts +++ b/src/bin/shared/copyKeycloakResourcesToPublic.ts @@ -65,7 +65,8 @@ export function copyKeycloakResourcesToPublic(params: { transformCodebase({ srcDirPath: pathJoin( getThisCodebaseRootDirPath(), - WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES + "public", + WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY ), destDirPath }); diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts index d4242309..736ce0f5 100644 --- a/src/bin/start-keycloak/start-keycloak.ts +++ b/src/bin/start-keycloak/start-keycloak.ts @@ -2,7 +2,7 @@ import { getBuildContext } from "../shared/buildContext"; import { exclude } from "tsafe/exclude"; import type { CliCommandOptions as CliCommandOptions_common } from "../main"; import { promptKeycloakVersion } from "../shared/promptKeycloakVersion"; -import { ACCOUNT_V1_THEME_NAME, CONTAINER_NAME } from "../shared/constants"; +import { CONTAINER_NAME } from "../shared/constants"; import { SemVer } from "../tools/SemVer"; import { assert, type Equals } from "tsafe/assert"; import * as fs from "fs"; @@ -409,13 +409,9 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) ...[ ...buildContext.themeNames, ...(fs.existsSync( - pathJoin( - buildContext.keycloakifyBuildDirPath, - "theme", - ACCOUNT_V1_THEME_NAME - ) + pathJoin(buildContext.keycloakifyBuildDirPath, "theme", "account-v1") ) - ? [ACCOUNT_V1_THEME_NAME] + ? ["account-v1"] : []) ] .map(themeName => ({ From 01c3b148e65156c20eac0511ce7635430a6d8a3b Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 12:06:49 +0200 Subject: [PATCH 07/20] Group all build time generated resource under a 'res' directory --- .storybook/main.js | 2 +- scripts/build/createAccountV1Dir.ts | 7 ++++++- scripts/build/createPublicDotKeycloakifyDir.ts | 1 + src/bin/initialize-email-theme.ts | 5 ++++- .../generateResources/generateResourcesForMainTheme.ts | 2 +- src/bin/shared/copyKeycloakResourcesToPublic.ts | 1 + 6 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.storybook/main.js b/.storybook/main.js index d33953e7..49d4ca27 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -9,5 +9,5 @@ module.exports = { core: { builder: "webpack5" }, - staticDirs: ["./static", "../dist/public"] + staticDirs: ["./static", "../dist/res/public"] }; diff --git a/scripts/build/createAccountV1Dir.ts b/scripts/build/createAccountV1Dir.ts index b58435b2..e00333c9 100644 --- a/scripts/build/createAccountV1Dir.ts +++ b/scripts/build/createAccountV1Dir.ts @@ -13,7 +13,12 @@ export async function createAccountV1Dir() { keycloakVersion: KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE }); - const destDirPath = pathJoin(getThisCodebaseRootDirPath(), "dist", "account-v1"); + const destDirPath = pathJoin( + getThisCodebaseRootDirPath(), + "dist", + "res", + "account-v1" + ); await fsPr.rm(destDirPath, { recursive: true, force: true }); diff --git a/scripts/build/createPublicDotKeycloakifyDir.ts b/scripts/build/createPublicDotKeycloakifyDir.ts index 262c4f44..13044a96 100644 --- a/scripts/build/createPublicDotKeycloakifyDir.ts +++ b/scripts/build/createPublicDotKeycloakifyDir.ts @@ -28,6 +28,7 @@ export async function createPublicDotKeycloakifyDir() { const destDirPath = pathJoin( getThisCodebaseRootDirPath(), "dist", + "res", "public", WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY, themeType diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts index 14b601ed..f2520645 100644 --- a/src/bin/initialize-email-theme.ts +++ b/src/bin/initialize-email-theme.ts @@ -67,7 +67,10 @@ export async function command(params: { cliCommandOptions: CliCommandOptions }) fs.writeFileSync( themePropertyFilePath, Buffer.from( - `parent=base\n${fs.readFileSync(themePropertyFilePath).toString("utf8")}`, + [ + `parent=base`, + fs.readFileSync(themePropertyFilePath).toString("utf8") + ].join("\n"), "utf8" ) ); diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index fc0c772c..2f1a8115 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -310,7 +310,7 @@ export async function generateResourcesForMainTheme(params: { } transformCodebase({ - srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "account-v1"), + srcDirPath: pathJoin(getThisCodebaseRootDirPath(), "res", "account-v1"), destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account") }); } diff --git a/src/bin/shared/copyKeycloakResourcesToPublic.ts b/src/bin/shared/copyKeycloakResourcesToPublic.ts index e4233c9e..e9a90762 100644 --- a/src/bin/shared/copyKeycloakResourcesToPublic.ts +++ b/src/bin/shared/copyKeycloakResourcesToPublic.ts @@ -65,6 +65,7 @@ export function copyKeycloakResourcesToPublic(params: { transformCodebase({ srcDirPath: pathJoin( getThisCodebaseRootDirPath(), + "res", "public", WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY ), From 98d3d1967a104a76a23161acbec22e66f0776cb4 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 12:09:19 +0200 Subject: [PATCH 08/20] Fix import error --- scripts/start-storybook.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/scripts/start-storybook.ts b/scripts/start-storybook.ts index 2c966b57..371828f5 100644 --- a/scripts/start-storybook.ts +++ b/scripts/start-storybook.ts @@ -1,12 +1,9 @@ import * as child_process from "child_process"; -import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange"; -import { copyKeycloakResourcesToStorybookStaticDir } from "./copyKeycloakResourcesToStorybookStaticDir"; +import { startRebuildOnSrcChange } from "./shared/startRebuildOnSrcChange"; (async () => { run("yarn build"); - await copyKeycloakResourcesToStorybookStaticDir(); - { const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"], { shell: true From ee6322aae4dc6cf55eab95e6c0ab479dff6488b1 Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 12:40:28 +0200 Subject: [PATCH 09/20] Extract only required files --- scripts/build/createAccountV1Dir.ts | 3 +- .../build/createPublicDotKeycloakifyDir.ts | 21 +- scripts/generate-i18n-messages.ts | 10 +- scripts/shared/constants.ts | 4 - .../shared/downloadKeycloakDefaultTheme.ts | 310 +++++++++++++++++- 5 files changed, 319 insertions(+), 29 deletions(-) delete mode 100644 scripts/shared/constants.ts diff --git a/scripts/build/createAccountV1Dir.ts b/scripts/build/createAccountV1Dir.ts index e00333c9..2865e251 100644 --- a/scripts/build/createAccountV1Dir.ts +++ b/scripts/build/createAccountV1Dir.ts @@ -1,6 +1,5 @@ import * as fs from "fs"; import { join as pathJoin } from "path"; -import { KEYCLOAK_VERSION } from "../shared/constants"; import { transformCodebase } from "../../src/bin/tools/transformCodebase"; import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme"; import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../../src/bin/shared/constants"; @@ -10,7 +9,7 @@ import * as fsPr from "fs/promises"; export async function createAccountV1Dir() { const { extractedDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion: KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE + keycloakVersionId: "FOR_ACCOUNT_MULTI_PAGE" }); const destDirPath = pathJoin( diff --git a/scripts/build/createPublicDotKeycloakifyDir.ts b/scripts/build/createPublicDotKeycloakifyDir.ts index 13044a96..667124ea 100644 --- a/scripts/build/createPublicDotKeycloakifyDir.ts +++ b/scripts/build/createPublicDotKeycloakifyDir.ts @@ -1,6 +1,5 @@ import { join as pathJoin } from "path"; import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme"; -import { KEYCLOAK_VERSION } from "../shared/constants"; import { transformCodebase } from "../../src/bin/tools/transformCodebase"; import { existsAsync } from "../../src/bin/tools/fs.existsAsync"; import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; @@ -11,18 +10,16 @@ import * as fsPr from "fs/promises"; export async function createPublicDotKeycloakifyDir() { await Promise.all( (["login", "account"] as const).map(async themeType => { - const keycloakVersion = (() => { - switch (themeType) { - case "login": - return KEYCLOAK_VERSION.FOR_LOGIN_THEME; - case "account": - return KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE; - } - assert>(); - })(); - const { extractedDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion + keycloakVersionId: (() => { + switch (themeType) { + case "login": + return "FOR_LOGIN_THEME"; + case "account": + return "FOR_ACCOUNT_MULTI_PAGE"; + } + assert>(); + })() }); const destDirPath = pathJoin( diff --git a/scripts/generate-i18n-messages.ts b/scripts/generate-i18n-messages.ts index 39beaba7..a31c993d 100644 --- a/scripts/generate-i18n-messages.ts +++ b/scripts/generate-i18n-messages.ts @@ -6,14 +6,13 @@ import { dirname as pathDirname, sep as pathSep } from "path"; -import { assert } from "tsafe/assert"; +import { assert, type Equals } from "tsafe/assert"; import { same } from "evt/tools/inDepth"; import { crawl } from "../src/bin/tools/crawl"; import { downloadKeycloakDefaultTheme } from "./shared/downloadKeycloakDefaultTheme"; import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath"; import { deepAssign } from "../src/tools/deepAssign"; import { THEME_TYPES } from "../src/bin/shared/constants"; -import { KEYCLOAK_VERSION } from "./shared/constants"; const propertiesParser: any = require("properties-parser"); if (require.main === module) { @@ -29,13 +28,14 @@ async function generateI18nMessages() { for (const themeType of THEME_TYPES) { const { extractedDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersion: (() => { + keycloakVersionId: (() => { switch (themeType) { case "login": - return KEYCLOAK_VERSION.FOR_LOGIN_THEME; + return "FOR_LOGIN_THEME"; case "account": - return KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE; + return "FOR_ACCOUNT_MULTI_PAGE"; } + assert>(); })() }); diff --git a/scripts/shared/constants.ts b/scripts/shared/constants.ts deleted file mode 100644 index fffddb47..00000000 --- a/scripts/shared/constants.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const KEYCLOAK_VERSION = { - FOR_LOGIN_THEME: "25.0.4", - FOR_ACCOUNT_MULTI_PAGE: "21.1.2" -}; diff --git a/scripts/shared/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts index 953cac66..3681ccd4 100644 --- a/scripts/shared/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -3,9 +3,22 @@ import { downloadAndExtractArchive } from "../../src/bin/tools/downloadAndExtrac import { getProxyFetchOptions } from "../../src/bin/tools/fetchProxyOptions"; import { join as pathJoin } from "path"; import { getThisCodebaseRootDirPath } from "../../src/bin/tools/getThisCodebaseRootDirPath"; +import { assert, type Equals } from "tsafe/assert"; -export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: string }) { - const { keycloakVersion } = params; +const KEYCLOAK_VERSION = { + FOR_LOGIN_THEME: "25.0.4", + FOR_ACCOUNT_MULTI_PAGE: "21.1.2" +} as const; + +export async function downloadKeycloakDefaultTheme(params: { + keycloakVersionId: keyof typeof KEYCLOAK_VERSION; +}) { + const { keycloakVersionId } = params; + + const keycloakVersion = KEYCLOAK_VERSION[keycloakVersionId]; + + let kcNodeModulesKeepFilePaths: Set | undefined = undefined; + let kcNodeModulesKeepFilePaths_lastAccountV1: Set | undefined = undefined; const { extractedDirPath } = await downloadAndExtractArchive({ url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`, @@ -19,14 +32,299 @@ export async function downloadKeycloakDefaultTheme(params: { keycloakVersion: st npmConfigGetCwd: getThisCodebaseRootDirPath() }), uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles", - onArchiveFile: async ({ fileRelativePath, writeFile }) => { - const fileRelativePath_target = pathRelative("theme", fileRelativePath); + onArchiveFile: async params => { + const fileRelativePath = pathRelative("theme", params.fileRelativePath); - if (fileRelativePath_target.startsWith("..")) { + if (fileRelativePath.startsWith("..")) { return; } - await writeFile({ fileRelativePath: fileRelativePath_target }); + const { readFile, writeFile } = params; + + if ( + !fileRelativePath.startsWith("base") && + !fileRelativePath.startsWith("keycloak") + ) { + return; + } + + switch (keycloakVersion) { + case KEYCLOAK_VERSION.FOR_LOGIN_THEME: + if ( + !fileRelativePath.startsWith(pathJoin("base", "login")) && + !fileRelativePath.startsWith(pathJoin("keycloak", "login")) && + !fileRelativePath.startsWith(pathJoin("keycloak", "common")) + ) { + return; + } + + if (fileRelativePath.endsWith(".ftl")) { + return; + } + + break; + case KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE: + if ( + !fileRelativePath.startsWith(pathJoin("base", "account")) && + !fileRelativePath.startsWith(pathJoin("keycloak", "account")) && + !fileRelativePath.startsWith(pathJoin("keycloak", "common")) + ) { + return; + } + + break; + default: + assert>(false); + } + + last_account_v1_transformations: { + if (keycloakVersion !== KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE) { + break last_account_v1_transformations; + } + + skip_web_modules: { + if ( + !fileRelativePath.startsWith( + pathJoin("keycloak", "common", "resources", "web_modules") + ) + ) { + break skip_web_modules; + } + + return; + } + + skip_lib: { + if ( + !fileRelativePath.startsWith( + pathJoin("keycloak", "common", "resources", "lib") + ) + ) { + break skip_lib; + } + + return; + } + + skip_node_modules: { + const nodeModulesRelativeDirPath = pathJoin( + "keycloak", + "common", + "resources", + "node_modules" + ); + + if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) { + break skip_node_modules; + } + + if (kcNodeModulesKeepFilePaths_lastAccountV1 === undefined) { + kcNodeModulesKeepFilePaths_lastAccountV1 = new Set([ + pathJoin("patternfly", "dist", "css", "patternfly.min.css"), + pathJoin( + "patternfly", + "dist", + "css", + "patternfly-additions.min.css" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Regular-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Bold-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Light-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Semibold-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "PatternFlyIcons-webfont.ttf" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "PatternFlyIcons-webfont.woff" + ) + ]); + } + + const fileRelativeToNodeModulesPath = fileRelativePath.substring( + nodeModulesRelativeDirPath.length + 1 + ); + + if ( + kcNodeModulesKeepFilePaths_lastAccountV1.has( + fileRelativeToNodeModulesPath + ) + ) { + break skip_node_modules; + } + + return; + } + + patch_account_css: { + if ( + fileRelativePath !== + pathJoin("keycloak", "account", "resources", "css", "account.css") + ) { + break patch_account_css; + } + + await writeFile({ + fileRelativePath, + modifiedData: Buffer.from( + (await readFile()) + .toString("utf8") + .replace("top: -34px;", "top: -34px !important;"), + "utf8" + ) + }); + + return; + } + } + + skip_unused_resources: { + if (keycloakVersion !== KEYCLOAK_VERSION.FOR_LOGIN_THEME) { + break skip_unused_resources; + } + + skip_node_modules: { + const nodeModulesRelativeDirPath = pathJoin( + "keycloak", + "common", + "resources", + "node_modules" + ); + + if (!fileRelativePath.startsWith(nodeModulesRelativeDirPath)) { + break skip_node_modules; + } + + if (kcNodeModulesKeepFilePaths === undefined) { + kcNodeModulesKeepFilePaths = new Set([ + pathJoin("@patternfly", "patternfly", "patternfly.min.css"), + pathJoin("patternfly", "dist", "css", "patternfly.min.css"), + pathJoin( + "patternfly", + "dist", + "css", + "patternfly-additions.min.css" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Regular-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Light-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Bold-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Bold-webfont.woff" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Bold-webfont.ttf" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "fontawesome-webfont.woff2" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "PatternFlyIcons-webfont.ttf" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "PatternFlyIcons-webfont.woff" + ), + pathJoin( + "patternfly", + "dist", + "fonts", + "OpenSans-Semibold-webfont.woff2" + ), + pathJoin("patternfly", "dist", "img", "bg-login.jpg"), + pathJoin("jquery", "dist", "jquery.min.js") + ]); + } + + const fileRelativeToNodeModulesPath = fileRelativePath.substring( + nodeModulesRelativeDirPath.length + 1 + ); + + if (kcNodeModulesKeepFilePaths.has(fileRelativeToNodeModulesPath)) { + break skip_node_modules; + } + + return; + } + + skip_vendor: { + if ( + !fileRelativePath.startsWith( + pathJoin("keycloak", "common", "resources", "vendor") + ) + ) { + break skip_vendor; + } + + return; + } + + skip_rollup_config: { + if ( + fileRelativePath !== + pathJoin("keycloak", "common", "resources", "rollup.config.js") + ) { + break skip_rollup_config; + } + + return; + } + } + + await writeFile({ fileRelativePath }); } }); From 359e93a1bac0935c0de43689b75c75a608ba190e Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 13:00:40 +0200 Subject: [PATCH 10/20] Fix path error --- .../generateResources/generateResourcesForMainTheme.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts index 2f1a8115..bad77af6 100644 --- a/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts +++ b/src/bin/keycloakify/generateResources/generateResourcesForMainTheme.ts @@ -254,6 +254,7 @@ export async function generateResourcesForMainTheme(params: { transformCodebase({ srcDirPath: pathJoin( getThisCodebaseRootDirPath(), + "res", "public", WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY, themeType From 785ed095bc9a3f18942ee2a59b9f664ac39adceb Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 14:41:45 +0200 Subject: [PATCH 11/20] Repatriate keycloak v24 scripts --- .../build/createPublicDotKeycloakifyDir.ts | 21 +++++++++++++++++++ .../shared/downloadKeycloakDefaultTheme.ts | 12 ++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/scripts/build/createPublicDotKeycloakifyDir.ts b/scripts/build/createPublicDotKeycloakifyDir.ts index 667124ea..b2e19d71 100644 --- a/scripts/build/createPublicDotKeycloakifyDir.ts +++ b/scripts/build/createPublicDotKeycloakifyDir.ts @@ -68,6 +68,27 @@ export async function createPublicDotKeycloakifyDir() { WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON ) }); + + copy_v24_js: { + if (themeType !== "login") { + break copy_v24_js; + } + + const { extractedDirPath } = await downloadKeycloakDefaultTheme({ + keycloakVersionId: "LAST_24" + }); + + transformCodebase({ + srcDirPath: pathJoin( + extractedDirPath, + "base", + "login", + "resources", + "js" + ), + destDirPath: pathJoin(destDirPath, "js", "v24") + }); + } }) ); } diff --git a/scripts/shared/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts index 3681ccd4..f5678ce6 100644 --- a/scripts/shared/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -7,7 +7,8 @@ import { assert, type Equals } from "tsafe/assert"; const KEYCLOAK_VERSION = { FOR_LOGIN_THEME: "25.0.4", - FOR_ACCOUNT_MULTI_PAGE: "21.1.2" + FOR_ACCOUNT_MULTI_PAGE: "21.1.2", + LAST_24: "24.0.4" } as const; export async function downloadKeycloakDefaultTheme(params: { @@ -72,6 +73,15 @@ export async function downloadKeycloakDefaultTheme(params: { return; } + break; + case KEYCLOAK_VERSION.LAST_24: + if ( + !fileRelativePath.startsWith( + pathJoin("base", "login", "resources", "js") + ) + ) { + return; + } break; default: assert>(false); From 9f875160eaba1229e0e0bc2bedeeed670dd96f4d Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 17:31:55 +0200 Subject: [PATCH 12/20] Make template initialization not ejected by default --- .../shared/downloadKeycloakDefaultTheme.ts | 2 +- src/login/KcContext/KcContext.ts | 1 - src/login/Template.tsx | 71 +------------ src/login/useInitTemplate.ts | 99 +++++++++++++++++++ 4 files changed, 104 insertions(+), 69 deletions(-) create mode 100644 src/login/useInitTemplate.ts diff --git a/scripts/shared/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts index f5678ce6..f7dddea3 100644 --- a/scripts/shared/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -8,7 +8,7 @@ import { assert, type Equals } from "tsafe/assert"; const KEYCLOAK_VERSION = { FOR_LOGIN_THEME: "25.0.4", FOR_ACCOUNT_MULTI_PAGE: "21.1.2", - LAST_24: "24.0.4" + LAST_24: "24.0.6" } as const; export async function downloadKeycloakDefaultTheme(params: { diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts index a091225e..819f8c21 100644 --- a/src/login/KcContext/KcContext.ts +++ b/src/login/KcContext/KcContext.ts @@ -152,7 +152,6 @@ export declare namespace KcContext { authenticationSession?: { authSessionId: string; tabId: string; - ssoLoginInOtherTabsUrl: string; }; properties: {}; "x-keycloakify": { diff --git a/src/login/Template.tsx b/src/login/Template.tsx index eebb0c31..ee94ab79 100644 --- a/src/login/Template.tsx +++ b/src/login/Template.tsx @@ -3,11 +3,10 @@ import { assert } from "keycloakify/tools/assert"; import { clsx } from "keycloakify/tools/clsx"; import type { TemplateProps } from "keycloakify/login/TemplateProps"; import { getKcClsx } from "keycloakify/login/lib/kcClsx"; -import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; -import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags"; import { useSetClassName } from "keycloakify/tools/useSetClassName"; import type { I18n } from "./i18n"; import type { KcContext } from "./KcContext"; +import { useInitTemplate } from "keycloakify/login/useInitTemplate"; export default function Template(props: TemplateProps) { const { @@ -30,7 +29,7 @@ export default function Template(props: TemplateProps) { const { msg, msgStr, getChangeLocaleUrl, labelBySupportedLanguageTag, currentLanguageTag } = i18n; - const { realm, locale, auth, url, message, isAppInitiatedAction, authenticationSession, scripts } = kcContext; + const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; useEffect(() => { document.title = documentTitle ?? msgStr("loginTitle", kcContext.realm.displayName); @@ -46,71 +45,9 @@ export default function Template(props: TemplateProps) { className: bodyClassName ?? kcClsx("kcBodyClass") }); - useEffect(() => { - const { currentLanguageTag } = locale ?? {}; + const { isReadyToRender } = useInitTemplate({ kcContext, doUseDefaultCss }); - if (currentLanguageTag === undefined) { - return; - } - - const html = document.querySelector("html"); - assert(html !== null); - html.lang = currentLanguageTag; - }, []); - - const { areAllStyleSheetsLoaded } = useInsertLinkTags({ - componentOrHookName: "Template", - hrefs: !doUseDefaultCss - ? [] - : [ - `${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`, - `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`, - `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`, - `${url.resourcesCommonPath}/lib/pficon/pficon.css`, - `${url.resourcesPath}/css/login.css` - ] - }); - - const { insertScriptTags } = useInsertScriptTags({ - componentOrHookName: "Template", - scriptTags: [ - { - type: "module", - src: `${url.resourcesPath}/js/menu-button-links.js` - }, - ...(authenticationSession === undefined - ? [] - : [ - { - type: "module", - textContent: [ - `import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`, - ``, - `checkCookiesAndSetTimer(`, - ` "${authenticationSession.authSessionId}",`, - ` "${authenticationSession.tabId}",`, - ` "${url.ssoLoginInOtherTabsUrl}"`, - `);` - ].join("\n") - } as const - ]), - ...scripts.map( - script => - ({ - type: "text/javascript", - src: script - }) as const - ) - ] - }); - - useEffect(() => { - if (areAllStyleSheetsLoaded) { - insertScriptTags(); - } - }, [areAllStyleSheetsLoaded]); - - if (!areAllStyleSheetsLoaded) { + if (!isReadyToRender) { return null; } diff --git a/src/login/useInitTemplate.ts b/src/login/useInitTemplate.ts new file mode 100644 index 00000000..5da30ef1 --- /dev/null +++ b/src/login/useInitTemplate.ts @@ -0,0 +1,99 @@ +import { useEffect } from "react"; +import { assert } from "keycloakify/tools/assert"; +import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags"; +import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags"; +import { KcContext } from "keycloakify/login/KcContext/KcContext"; + +export type KcContextLike = { + url: { + resourcesCommonPath: string; + resourcesPath: string; + ssoLoginInOtherTabsUrl: string; + }; + locale?: { + currentLanguageTag: string; + }; + scripts: string[]; + authenticationSession?: { + authSessionId: string; + tabId: string; + }; +}; + +assert(); +assert(); + +export function useInitTemplate(params: { + kcContext: KcContextLike; + doUseDefaultCss: boolean; +}) { + const { kcContext, doUseDefaultCss } = params; + + const { url, locale, scripts, authenticationSession } = kcContext; + + useEffect(() => { + const { currentLanguageTag } = locale ?? {}; + + if (currentLanguageTag === undefined) { + return; + } + + const html = document.querySelector("html"); + assert(html !== null); + html.lang = currentLanguageTag; + }, []); + + const { areAllStyleSheetsLoaded } = useInsertLinkTags({ + componentOrHookName: "Template", + hrefs: !doUseDefaultCss + ? [] + : [ + `${url.resourcesCommonPath}/node_modules/@patternfly/patternfly/patternfly.min.css`, + `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly.min.css`, + `${url.resourcesCommonPath}/node_modules/patternfly/dist/css/patternfly-additions.min.css`, + `${url.resourcesCommonPath}/lib/pficon/pficon.css`, + `${url.resourcesPath}/css/login.css` + ] + }); + + const { insertScriptTags } = useInsertScriptTags({ + componentOrHookName: "Template", + scriptTags: [ + { + type: "module", + src: `${url.resourcesPath}/js/menu-button-links.js` + }, + ...(authenticationSession === undefined + ? [] + : [ + { + type: "module", + textContent: [ + `import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`, + ``, + `checkCookiesAndSetTimer(`, + ` "${authenticationSession.authSessionId}",`, + ` "${authenticationSession.tabId}",`, + ` "${url.ssoLoginInOtherTabsUrl}"`, + `);` + ].join("\n") + } as const + ]), + ...scripts.map( + script => + ({ + type: "text/javascript", + src: script + }) as const + ) + ] + }); + + useEffect(() => { + if (areAllStyleSheetsLoaded) { + insertScriptTags(); + } + }, [areAllStyleSheetsLoaded]); + + return { isReadyToRender: areAllStyleSheetsLoaded }; +} From 1d57f4b4dc9c3cd5d6ccb05f32cf0b9e4dae441f Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Sun, 8 Sep 2024 18:16:49 +0200 Subject: [PATCH 13/20] Include missing dependency file --- scripts/shared/downloadKeycloakDefaultTheme.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/shared/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts index f7dddea3..a93b9dd4 100644 --- a/scripts/shared/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -8,7 +8,7 @@ import { assert, type Equals } from "tsafe/assert"; const KEYCLOAK_VERSION = { FOR_LOGIN_THEME: "25.0.4", FOR_ACCOUNT_MULTI_PAGE: "21.1.2", - LAST_24: "24.0.6" + LAST_24: "24.0.4" } as const; export async function downloadKeycloakDefaultTheme(params: { @@ -295,7 +295,8 @@ export async function downloadKeycloakDefaultTheme(params: { "OpenSans-Semibold-webfont.woff2" ), pathJoin("patternfly", "dist", "img", "bg-login.jpg"), - pathJoin("jquery", "dist", "jquery.min.js") + pathJoin("jquery", "dist", "jquery.min.js"), + pathJoin("rfc4648", "lib", "rfc4648.js") ]); } @@ -332,6 +333,16 @@ export async function downloadKeycloakDefaultTheme(params: { return; } + + skip_package_json: { + if ( + fileRelativePath !== + pathJoin("keycloak", "common", "resources", "package.json") + ) { + break skip_package_json; + } + return; + } } await writeFile({ fileRelativePath }); From 7e5abe8589e1d61999023a95ca203253277918ed Mon Sep 17 00:00:00 2001 From: Joseph Garrone Date: Mon, 9 Sep 2024 06:59:11 +0200 Subject: [PATCH 14/20] Fix LoginPasskesConditionalAuthenticate --- .../build/createPublicDotKeycloakifyDir.ts | 21 ----- .../shared/downloadKeycloakDefaultTheme.ts | 12 +-- src/login/KcContext/KcContext.ts | 6 +- src/login/Template.tsx | 4 +- ...ate.ts => Template.useStylesAndScripts.ts} | 53 +++++------ .../pages/LoginIdpLinkConfirmOverride.tsx | 1 - .../LoginPasskeysConditionalAuthenticate.tsx | 87 ++----------------- ...skeysConditionalAuthenticate.useScript.tsx | 67 ++++++++++++++ src/tools/useInsertScriptTags.ts | 2 +- 9 files changed, 102 insertions(+), 151 deletions(-) rename src/login/{useInitTemplate.ts => Template.useStylesAndScripts.ts} (65%) create mode 100644 src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx diff --git a/scripts/build/createPublicDotKeycloakifyDir.ts b/scripts/build/createPublicDotKeycloakifyDir.ts index b2e19d71..667124ea 100644 --- a/scripts/build/createPublicDotKeycloakifyDir.ts +++ b/scripts/build/createPublicDotKeycloakifyDir.ts @@ -68,27 +68,6 @@ export async function createPublicDotKeycloakifyDir() { WELL_KNOWN_DIRECTORY_BASE_NAME.RESOURCES_COMMON ) }); - - copy_v24_js: { - if (themeType !== "login") { - break copy_v24_js; - } - - const { extractedDirPath } = await downloadKeycloakDefaultTheme({ - keycloakVersionId: "LAST_24" - }); - - transformCodebase({ - srcDirPath: pathJoin( - extractedDirPath, - "base", - "login", - "resources", - "js" - ), - destDirPath: pathJoin(destDirPath, "js", "v24") - }); - } }) ); } diff --git a/scripts/shared/downloadKeycloakDefaultTheme.ts b/scripts/shared/downloadKeycloakDefaultTheme.ts index a93b9dd4..76c5a51c 100644 --- a/scripts/shared/downloadKeycloakDefaultTheme.ts +++ b/scripts/shared/downloadKeycloakDefaultTheme.ts @@ -7,8 +7,7 @@ import { assert, type Equals } from "tsafe/assert"; const KEYCLOAK_VERSION = { FOR_LOGIN_THEME: "25.0.4", - FOR_ACCOUNT_MULTI_PAGE: "21.1.2", - LAST_24: "24.0.4" + FOR_ACCOUNT_MULTI_PAGE: "21.1.2" } as const; export async function downloadKeycloakDefaultTheme(params: { @@ -73,15 +72,6 @@ export async function downloadKeycloakDefaultTheme(params: { return; } - break; - case KEYCLOAK_VERSION.LAST_24: - if ( - !fileRelativePath.startsWith( - pathJoin("base", "login", "resources", "js") - ) - ) { - return; - } break; default: assert>(false); diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts index 819f8c21..e64ba30f 100644 --- a/src/login/KcContext/KcContext.ts +++ b/src/login/KcContext/KcContext.ts @@ -149,10 +149,6 @@ export declare namespace KcContext { getFirstError: (...fieldNames: string[]) => string; }; - authenticationSession?: { - authSessionId: string; - tabId: string; - }; properties: {}; "x-keycloakify": { messages: Record; @@ -593,7 +589,7 @@ export declare namespace KcContext { challenge: string; userVerification: string; rpId: string; - createTimeout: number | string; + createTimeout: number; authenticators?: { authenticators: WebauthnAuthenticate.WebauthnAuthenticator[]; diff --git a/src/login/Template.tsx b/src/login/Template.tsx index ee94ab79..ade06dee 100644 --- a/src/login/Template.tsx +++ b/src/login/Template.tsx @@ -4,9 +4,9 @@ import { clsx } from "keycloakify/tools/clsx"; import type { TemplateProps } from "keycloakify/login/TemplateProps"; import { getKcClsx } from "keycloakify/login/lib/kcClsx"; import { useSetClassName } from "keycloakify/tools/useSetClassName"; +import { useStylesAndScripts } from "keycloakify/login/Template.useStylesAndScripts"; import type { I18n } from "./i18n"; import type { KcContext } from "./KcContext"; -import { useInitTemplate } from "keycloakify/login/useInitTemplate"; export default function Template(props: TemplateProps) { const { @@ -45,7 +45,7 @@ export default function Template(props: TemplateProps) { className: bodyClassName ?? kcClsx("kcBodyClass") }); - const { isReadyToRender } = useInitTemplate({ kcContext, doUseDefaultCss }); + const { isReadyToRender } = useStylesAndScripts({ kcContext, doUseDefaultCss }); if (!isReadyToRender) { return null; diff --git a/src/login/useInitTemplate.ts b/src/login/Template.useStylesAndScripts.ts similarity index 65% rename from src/login/useInitTemplate.ts rename to src/login/Template.useStylesAndScripts.ts index 5da30ef1..72933100 100644 --- a/src/login/useInitTemplate.ts +++ b/src/login/Template.useStylesAndScripts.ts @@ -14,22 +14,18 @@ export type KcContextLike = { currentLanguageTag: string; }; scripts: string[]; - authenticationSession?: { - authSessionId: string; - tabId: string; - }; }; assert(); assert(); -export function useInitTemplate(params: { +export function useStylesAndScripts(params: { kcContext: KcContextLike; doUseDefaultCss: boolean; }) { const { kcContext, doUseDefaultCss } = params; - const { url, locale, scripts, authenticationSession } = kcContext; + const { url, locale, scripts } = kcContext; useEffect(() => { const { currentLanguageTag } = locale ?? {}; @@ -59,33 +55,32 @@ export function useInitTemplate(params: { const { insertScriptTags } = useInsertScriptTags({ componentOrHookName: "Template", scriptTags: [ + { + type: "importmap", + textContent: JSON.stringify({ + imports: { + rfc4648: `${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js` + } + }) + }, { type: "module", src: `${url.resourcesPath}/js/menu-button-links.js` }, - ...(authenticationSession === undefined - ? [] - : [ - { - type: "module", - textContent: [ - `import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";`, - ``, - `checkCookiesAndSetTimer(`, - ` "${authenticationSession.authSessionId}",`, - ` "${authenticationSession.tabId}",`, - ` "${url.ssoLoginInOtherTabsUrl}"`, - `);` - ].join("\n") - } as const - ]), - ...scripts.map( - script => - ({ - type: "text/javascript", - src: script - }) as const - ) + ...scripts.map(src => ({ + type: "text/javascript" as const, + src + })), + { + type: "module", + textContent: ` + import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js"; + + checkCookiesAndSetTimer( + "${url.ssoLoginInOtherTabsUrl}" + ); + ` + } ] }); diff --git a/src/login/pages/LoginIdpLinkConfirmOverride.tsx b/src/login/pages/LoginIdpLinkConfirmOverride.tsx index dbad8a32..ec4186cd 100644 --- a/src/login/pages/LoginIdpLinkConfirmOverride.tsx +++ b/src/login/pages/LoginIdpLinkConfirmOverride.tsx @@ -3,7 +3,6 @@ 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, I18n>) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; diff --git a/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx b/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx index bb3f8b70..e5aa5b2f 100644 --- a/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx +++ b/src/login/pages/LoginPasskeysConditionalAuthenticate.tsx @@ -1,33 +1,17 @@ -import { useEffect, Fragment } from "react"; +import { 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 { useScript } from "keycloakify/login/pages/LoginPasskeysConditionalAuthenticate.useScript"; import type { KcContext } from "../KcContext"; import type { I18n } from "../i18n"; -// NOTE: From Keycloak 25.0.4 export default function LoginPasskeysConditionalAuthenticate( props: PageProps, I18n> ) { const { kcContext, i18n, doUseDefaultCss, Template, classes } = props; - const { - messagesPerField, - login, - url, - usernameHidden, - shouldDisplayAuthenticators, - authenticators, - registrationDisabled, - realm, - isUserIdentified, - challenge, - userVerification, - rpId, - createTimeout - } = kcContext; + const { messagesPerField, login, url, usernameHidden, shouldDisplayAuthenticators, authenticators, registrationDisabled, realm } = kcContext; const { msg, msgStr, advancedMsg } = i18n; @@ -36,46 +20,9 @@ export default function LoginPasskeysConditionalAuthenticate( 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 authButtonId = "authenticateWebAuthnButton"; - 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(); - }, []); + useScript({ authButtonId, kcContext, i18n }); return (