Merge pull request #632 from keycloakify/passkey-conditional-authenticate
Passkey conditional authenticate
This commit is contained in:
commit
d20964ec94
@ -9,5 +9,5 @@ module.exports = {
|
||||
core: {
|
||||
builder: "webpack5"
|
||||
},
|
||||
staticDirs: ["./static"]
|
||||
staticDirs: ["./static", "../dist/res/public"]
|
||||
};
|
||||
|
@ -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]**.
|
@ -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]**.
|
@ -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]**.
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "keycloakify",
|
||||
"version": "10.0.6",
|
||||
"version": "10.1.0-rc.1",
|
||||
"description": "Create Keycloak themes using React",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -8,7 +8,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "tsx scripts/generate-i18n-messages.ts",
|
||||
"build": "tsx scripts/build.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",
|
||||
|
@ -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");
|
||||
})();
|
||||
|
||||
|
176
scripts/build.ts
176
scripts/build.ts
@ -1,176 +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";
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
fs.rmSync(join("dist", "src"), { recursive: true, force: true });
|
||||
|
||||
fs.cpSync("src", join("dist", "src"), { 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 };
|
||||
}
|
79
scripts/build/createAccountV1Dir.ts
Normal file
79
scripts/build/createAccountV1Dir.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import * as fs from "fs";
|
||||
import { join as pathJoin } from "path";
|
||||
import { transformCodebase } from "../../src/bin/tools/transformCodebase";
|
||||
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 "../generate-i18n-messages";
|
||||
import * as fsPr from "fs/promises";
|
||||
|
||||
export async function createAccountV1Dir() {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: "FOR_ACCOUNT_MULTI_PAGE"
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"account-v1"
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
73
scripts/build/createPublicDotKeycloakifyDir.ts
Normal file
73
scripts/build/createPublicDotKeycloakifyDir.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { join as pathJoin } from "path";
|
||||
import { downloadKeycloakDefaultTheme } from "../shared/downloadKeycloakDefaultTheme";
|
||||
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 createPublicDotKeycloakifyDir() {
|
||||
await Promise.all(
|
||||
(["login", "account"] as const).map(async themeType => {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
|
||||
const destDirPath = pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"dist",
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY,
|
||||
themeType
|
||||
);
|
||||
|
||||
await fsPr.rm(destDirPath, { recursive: true, force: true });
|
||||
|
||||
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
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
186
scripts/build/main.ts
Normal file
186
scripts/build/main.ts
Normal file
@ -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 };
|
||||
}
|
@ -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_FROMkEYCLOAK_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_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||
publicDirPath: pathJoin(__dirname, "..", ".storybook", "static")
|
||||
}
|
||||
});
|
||||
}
|
@ -6,46 +6,41 @@ 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 "../src/bin/shared/downloadKeycloakDefaultTheme";
|
||||
import { downloadKeycloakDefaultTheme } from "./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 } from "../src/bin/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.
|
||||
|
||||
//@ts-ignore
|
||||
const propertiesParser = require("properties-parser");
|
||||
|
||||
async function main() {
|
||||
const keycloakVersion = "24.0.4";
|
||||
if (require.main === module) {
|
||||
generateI18nMessages();
|
||||
}
|
||||
|
||||
async function generateI18nMessages() {
|
||||
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 } } = {};
|
||||
|
||||
for (const themeType of THEME_TYPES) {
|
||||
const { extractedDirPath } = await downloadKeycloakDefaultTheme({
|
||||
keycloakVersionId: (() => {
|
||||
switch (themeType) {
|
||||
case "login":
|
||||
return "FOR_LOGIN_THEME";
|
||||
case "account":
|
||||
return "FOR_ACCOUNT_MULTI_PAGE";
|
||||
}
|
||||
assert<Equals<typeof themeType, never>>();
|
||||
})()
|
||||
});
|
||||
|
||||
{
|
||||
const baseThemeDirPath = pathJoin(defaultThemeDirPath, "base");
|
||||
const baseThemeDirPath = pathJoin(extractedDirPath, "base");
|
||||
const re = new RegExp(
|
||||
`^([^\\${pathSep}]+)\\${pathSep}messages\\${pathSep}messages_([^.]+).properties$`
|
||||
);
|
||||
@ -60,9 +55,14 @@ async function main() {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, typeOfPage, language] = match;
|
||||
const [, themeType_here, language] = match;
|
||||
|
||||
(record[typeOfPage] ??= {})[language.replace(/_/g, "-")] = Object.fromEntries(
|
||||
if (themeType_here !== themeType) {
|
||||
return;
|
||||
}
|
||||
|
||||
(record[themeType] ??= {})[language.replace(/_/g, "-")] =
|
||||
Object.fromEntries(
|
||||
Object.entries(
|
||||
propertiesParser.parse(
|
||||
fs
|
||||
@ -75,16 +75,14 @@ async function main() {
|
||||
key === "locale_pt_BR" ? "locale_pt-BR" : key,
|
||||
value
|
||||
])
|
||||
.map(([key, value]) => [key, key === "termsText" ? "" : value])
|
||||
.map(([key, value]) => [
|
||||
key,
|
||||
key === "termsText" ? "" : value
|
||||
])
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Object.keys(record).forEach(themeType => {
|
||||
if (themeType !== "login" && themeType !== "account") {
|
||||
return;
|
||||
}
|
||||
|
||||
const recordForThemeType = record[themeType];
|
||||
|
||||
const languages = Object.keys(recordForThemeType);
|
||||
@ -99,6 +97,29 @@ async function main() {
|
||||
assert(false);
|
||||
})();
|
||||
|
||||
/* Migration helper
|
||||
|
||||
console.log({ themeType });
|
||||
|
||||
{
|
||||
|
||||
const all = new Set<string>();
|
||||
|
||||
languages.forEach(languages => all.add(languages));
|
||||
const currentlySupportedLanguages = Object.keys(keycloakifyExtraMessages);
|
||||
currentlySupportedLanguages.forEach(languages => all.add(languages));
|
||||
|
||||
all.forEach(language => {
|
||||
console.log([
|
||||
`"${language}": `,
|
||||
`isInLanguages: ${languages.includes(language)}`,
|
||||
`isInKeycloakifyExtraMessages: ${currentlySupportedLanguages.includes(language)}`
|
||||
].join(" "))
|
||||
});
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
assert(
|
||||
same(languages, Object.keys(keycloakifyExtraMessages), {
|
||||
takeIntoAccountArraysOrdering: false
|
||||
@ -180,7 +201,7 @@ async function main() {
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const keycloakifyExtraMessages_login: Record<
|
||||
@ -203,6 +224,7 @@ const keycloakifyExtraMessages_login: Record<
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
@ -210,7 +232,9 @@ const keycloakifyExtraMessages_login: Record<
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "zh-CN",
|
||||
| "ka"
|
||||
| "zh-CN"
|
||||
| "zh-TW",
|
||||
Record<
|
||||
| "shouldBeEqual"
|
||||
| "shouldBeDifferent"
|
||||
@ -434,6 +458,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 +546,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,38 +567,49 @@ 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 */
|
||||
};
|
||||
|
||||
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"
|
||||
| "el"
|
||||
| "es"
|
||||
| "fa"
|
||||
| "fi"
|
||||
| "fr"
|
||||
| "hu"
|
||||
| "it"
|
||||
| "ja"
|
||||
| "lt"
|
||||
| "lv"
|
||||
| "nl"
|
||||
| "no"
|
||||
| "pl"
|
||||
| "pt-BR"
|
||||
| "ru"
|
||||
| "sk"
|
||||
| "sv"
|
||||
| "th"
|
||||
| "tr"
|
||||
| "uk"
|
||||
| "zh-CN",
|
||||
(typeof accountMultiPageSupportedLanguages)[number],
|
||||
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
|
||||
> = {
|
||||
en: {
|
||||
@ -580,18 +637,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,25 +698,13 @@ 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: "密码确认不匹配"
|
||||
}
|
||||
/* spell-checker: enable */
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
@ -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<string>(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);
|
||||
})();
|
@ -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";
|
||||
|
||||
{
|
||||
|
@ -1,30 +1,37 @@
|
||||
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 { 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";
|
||||
import { assert, type Equals } from "tsafe/assert";
|
||||
|
||||
export type BuildContextLike = {
|
||||
cacheDirPath: string;
|
||||
fetchOptions: BuildContext["fetchOptions"];
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
const KEYCLOAK_VERSION = {
|
||||
FOR_LOGIN_THEME: "25.0.4",
|
||||
FOR_ACCOUNT_MULTI_PAGE: "21.1.2"
|
||||
} as const;
|
||||
|
||||
export async function downloadKeycloakDefaultTheme(params: {
|
||||
keycloakVersion: string;
|
||||
buildContext: BuildContextLike;
|
||||
}): Promise<{ defaultThemeDirPath: string }> {
|
||||
const { keycloakVersion, buildContext } = params;
|
||||
keycloakVersionId: keyof typeof KEYCLOAK_VERSION;
|
||||
}) {
|
||||
const { keycloakVersionId } = params;
|
||||
|
||||
const keycloakVersion = KEYCLOAK_VERSION[keycloakVersionId];
|
||||
|
||||
let kcNodeModulesKeepFilePaths: Set<string> | undefined = undefined;
|
||||
let kcNodeModulesKeepFilePaths_lastAccountV1: Set<string> | undefined = undefined;
|
||||
|
||||
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",
|
||||
cacheDirPath: pathJoin(
|
||||
getThisCodebaseRootDirPath(),
|
||||
"node_modules",
|
||||
".cache",
|
||||
"scripts"
|
||||
),
|
||||
fetchOptions: getProxyFetchOptions({
|
||||
npmConfigGetCwd: getThisCodebaseRootDirPath()
|
||||
}),
|
||||
uniqueIdOfOnArchiveFile: "extractOnlyRequiredFiles",
|
||||
onArchiveFile: async params => {
|
||||
const fileRelativePath = pathRelative("theme", params.fileRelativePath);
|
||||
|
||||
@ -34,16 +41,44 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
|
||||
const { readFile, writeFile } = params;
|
||||
|
||||
skip_keycloak_v2: {
|
||||
if (!fileRelativePath.startsWith(pathJoin("keycloak.v2"))) {
|
||||
break skip_keycloak_v2;
|
||||
}
|
||||
|
||||
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<Equals<typeof keycloakVersion, never>>(false);
|
||||
}
|
||||
|
||||
last_account_v1_transformations: {
|
||||
if (LAST_KEYCLOAK_VERSION_WITH_ACCOUNT_V1 !== keycloakVersion) {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_ACCOUNT_MULTI_PAGE) {
|
||||
break last_account_v1_transformations;
|
||||
}
|
||||
|
||||
@ -169,7 +204,7 @@ export async function downloadKeycloakDefaultTheme(params: {
|
||||
}
|
||||
|
||||
skip_unused_resources: {
|
||||
if (keycloakVersion !== "24.0.4") {
|
||||
if (keycloakVersion !== KEYCLOAK_VERSION.FOR_LOGIN_THEME) {
|
||||
break skip_unused_resources;
|
||||
}
|
||||
|
||||
@ -250,7 +285,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")
|
||||
]);
|
||||
}
|
||||
|
||||
@ -287,11 +323,21 @@ 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 });
|
||||
}
|
||||
});
|
||||
|
||||
return { defaultThemeDirPath: extractedDirPath };
|
||||
return { extractedDirPath };
|
||||
}
|
@ -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
|
||||
|
@ -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}`;
|
||||
})();
|
||||
|
@ -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: "#",
|
||||
|
@ -7,7 +7,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
|
||||
|
||||
const buildContext = getBuildContext({ cliCommandOptions });
|
||||
|
||||
await copyKeycloakResourcesToPublic({
|
||||
copyKeycloakResourcesToPublic({
|
||||
buildContext
|
||||
});
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
|
||||
@ -50,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"
|
||||
)
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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 ?? ""
|
||||
|
@ -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<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
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"
|
||||
)
|
||||
);
|
||||
}
|
@ -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,12 +27,10 @@ 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 & {
|
||||
export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode & {
|
||||
extraThemeProperties: string[] | undefined;
|
||||
loginThemeResourcesFromKeycloakVersion: string;
|
||||
projectDirPath: string;
|
||||
projectBuildDirPath: string;
|
||||
environmentVariables: { name: string; default: string }[];
|
||||
@ -56,7 +38,7 @@ export type BuildContextLike = BuildContextLike_kcContextExclusionsFtlCode &
|
||||
themeSrcDirPath: string;
|
||||
bundler: "vite" | "webpack";
|
||||
packageJsonFilePath: string;
|
||||
};
|
||||
};
|
||||
|
||||
assert<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
@ -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,52 @@ 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(),
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY,
|
||||
themeType
|
||||
),
|
||||
destDirPath: pathJoin(themeTypeDirPath, "resources")
|
||||
});
|
||||
}
|
||||
|
||||
@ -259,7 +270,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 +310,9 @@ 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(), "res", "account-v1"),
|
||||
destDirPath: pathJoin(resourcesDirPath, "theme", "account-v1", "account")
|
||||
});
|
||||
}
|
||||
|
||||
@ -349,7 +328,7 @@ export async function generateResourcesForMainTheme(params: {
|
||||
|
||||
if (buildContext.implementedThemeTypes.account.isImplemented) {
|
||||
metaInfKeycloakThemes.themes.push({
|
||||
name: ACCOUNT_V1_THEME_NAME,
|
||||
name: "account-v1",
|
||||
types: ["account"]
|
||||
});
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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}")`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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 };
|
||||
|
@ -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_FROMkEYCLOAK_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_FROMkEYCLOAK_VERSION_DEFAULT,
|
||||
projectDirPath,
|
||||
projectBuildDirPath,
|
||||
keycloakifyBuildDirPath: (() => {
|
||||
|
@ -1,10 +1,10 @@
|
||||
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"
|
||||
} as const;
|
||||
|
||||
export const THEME_TYPES = ["login", "account"] as const;
|
||||
export const ACCOUNT_V1_THEME_NAME = "account-v1";
|
||||
|
||||
export type ThemeType = (typeof THEME_TYPES)[number];
|
||||
|
||||
@ -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 = [
|
||||
@ -69,5 +71,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_FROMkEYCLOAK_VERSION_DEFAULT = "24.0.4";
|
||||
|
@ -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<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
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,35 @@ 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(),
|
||||
"res",
|
||||
"public",
|
||||
WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY
|
||||
),
|
||||
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")
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -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<BuildContext extends BuildContextLike ? true : false>();
|
||||
|
||||
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)
|
||||
});
|
||||
}
|
@ -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 => ({
|
||||
|
@ -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<KcContext, I18n> & {
|
||||
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
|
||||
@ -121,6 +123,10 @@ export default function DefaultPage(props: DefaultPageProps) {
|
||||
return <LoginX509Info kcContext={kcContext} {...rest} />;
|
||||
case "webauthn-error.ftl":
|
||||
return <WebauthnError kcContext={kcContext} {...rest} />;
|
||||
case "login-passkeys-conditional-authenticate.ftl":
|
||||
return <LoginPasskeysConditionalAuthenticate kcContext={kcContext} {...rest} />;
|
||||
case "login-idp-link-confirm-override.ftl":
|
||||
return <LoginIdpLinkConfirmOverride kcContext={kcContext} {...rest} />;
|
||||
}
|
||||
assert<Equals<typeof kcContext, never>>(false);
|
||||
})()}
|
||||
|
@ -59,7 +59,9 @@ export type KcContext =
|
||||
| KcContext.LoginRecoveryAuthnCodeInput
|
||||
| KcContext.LoginResetOtp
|
||||
| KcContext.LoginX509Info
|
||||
| KcContext.WebauthnError;
|
||||
| KcContext.WebauthnError
|
||||
| KcContext.LoginPasskeysConditionalAuthenticate
|
||||
| KcContext.LoginIdpLinkConfirmOverride;
|
||||
|
||||
assert<KcContext["themeType"] extends ThemeType ? true : false>();
|
||||
|
||||
@ -147,11 +149,6 @@ export declare namespace KcContext {
|
||||
|
||||
getFirstError: (...fieldNames: string[]) => string;
|
||||
};
|
||||
authenticationSession?: {
|
||||
authSessionId: string;
|
||||
tabId: string;
|
||||
ssoLoginInOtherTabsUrl: string;
|
||||
};
|
||||
properties: {};
|
||||
"x-keycloakify": {
|
||||
messages: Record<string, string>;
|
||||
@ -191,7 +188,7 @@ export declare namespace KcContext {
|
||||
password?: string;
|
||||
};
|
||||
usernameHidden?: boolean;
|
||||
social: {
|
||||
social?: {
|
||||
displayInfo: boolean;
|
||||
providers?: {
|
||||
loginUrl: string;
|
||||
@ -211,9 +208,12 @@ export declare namespace KcContext {
|
||||
registrationAction: string;
|
||||
};
|
||||
passwordRequired: boolean;
|
||||
recaptchaRequired: boolean;
|
||||
recaptchaRequired?: boolean;
|
||||
recaptchaVisible?: boolean;
|
||||
recaptchaSiteKey?: string;
|
||||
recaptchaAction?: string;
|
||||
termsAcceptanceRequired?: boolean;
|
||||
messageHeader?: string;
|
||||
};
|
||||
|
||||
export type Info = Common & {
|
||||
@ -328,7 +328,7 @@ export declare namespace KcContext {
|
||||
rememberMe?: string;
|
||||
};
|
||||
usernameHidden?: boolean;
|
||||
social: Login["social"];
|
||||
social?: Login["social"];
|
||||
};
|
||||
|
||||
export type LoginPassword = Common & {
|
||||
@ -346,9 +346,6 @@ export declare namespace KcContext {
|
||||
showTryAnotherWayLink?: boolean;
|
||||
attemptedUsername?: string;
|
||||
};
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type WebauthnAuthenticate = Common & {
|
||||
@ -360,13 +357,9 @@ export declare namespace KcContext {
|
||||
// I hate this:
|
||||
userVerification: UserVerificationRequirement | "not specified";
|
||||
rpId: string;
|
||||
createTimeout: string;
|
||||
createTimeout: string | number;
|
||||
isUserIdentified: "true" | "false";
|
||||
shouldDisplayAuthenticators: boolean;
|
||||
social: {
|
||||
displayInfo: boolean;
|
||||
};
|
||||
login: {};
|
||||
realm: {
|
||||
password: boolean;
|
||||
registrationAllowed: boolean;
|
||||
@ -401,7 +394,7 @@ export declare namespace KcContext {
|
||||
authenticatorAttachment: string;
|
||||
requireResidentKey: string;
|
||||
userVerificationRequirement: string;
|
||||
createTimeout: number;
|
||||
createTimeout: number | string;
|
||||
excludeCredentialIds: string;
|
||||
isSetRetry?: boolean;
|
||||
isAppInitiatedAction?: boolean;
|
||||
@ -577,6 +570,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 = {
|
||||
|
@ -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: "#"
|
||||
@ -327,9 +326,6 @@ export const kcContextMocks = [
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
resetPasswordAllowed: true
|
||||
},
|
||||
social: {
|
||||
displayInfo: false
|
||||
}
|
||||
}),
|
||||
id<KcContext.WebauthnAuthenticate>({
|
||||
@ -349,11 +345,7 @@ export const kcContextMocks = [
|
||||
rpId: "",
|
||||
createTimeout: "0",
|
||||
isUserIdentified: "false",
|
||||
shouldDisplayAuthenticators: false,
|
||||
social: {
|
||||
displayInfo: false
|
||||
},
|
||||
login: {}
|
||||
shouldDisplayAuthenticators: false
|
||||
}),
|
||||
id<KcContext.LoginUpdatePassword>({
|
||||
...kcContextCommonMock,
|
||||
@ -567,6 +559,39 @@ export const kcContextMocks = [
|
||||
pageId: "webauthn-error.ftl",
|
||||
...kcContextCommonMock,
|
||||
isAppInitiatedAction: true
|
||||
}),
|
||||
id<KcContext.LoginPasskeysConditionalAuthenticate>({
|
||||
pageId: "login-passkeys-conditional-authenticate.ftl",
|
||||
...kcContextCommonMock,
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
registrationUrl: "#"
|
||||
},
|
||||
realm: {
|
||||
...kcContextCommonMock.realm,
|
||||
password: true,
|
||||
registrationAllowed: true
|
||||
},
|
||||
registrationDisabled: false,
|
||||
isUserIdentified: "false",
|
||||
challenge: "",
|
||||
userVerification: "not specified",
|
||||
rpId: "",
|
||||
createTimeout: 0,
|
||||
authenticators: {
|
||||
authenticators: []
|
||||
},
|
||||
shouldDisplayAuthenticators: false,
|
||||
login: {}
|
||||
}),
|
||||
id<KcContext.LoginIdpLinkConfirmOverride>({
|
||||
pageId: "login-idp-link-confirm-override.ftl",
|
||||
...kcContextCommonMock,
|
||||
url: {
|
||||
...kcContextCommonMock.url,
|
||||
loginRestartFlowUrl: "#"
|
||||
},
|
||||
idpDisplayName: "Google"
|
||||
})
|
||||
];
|
||||
|
||||
|
@ -3,9 +3,8 @@ 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 { useStylesAndScripts } from "keycloakify/login/Template.useStylesAndScripts";
|
||||
import type { I18n } from "./i18n";
|
||||
import type { KcContext } from "./KcContext";
|
||||
|
||||
@ -30,7 +29,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
|
||||
|
||||
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<KcContext, I18n>) {
|
||||
className: bodyClassName ?? kcClsx("kcBodyClass")
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { currentLanguageTag } = locale ?? {};
|
||||
const { isReadyToRender } = useStylesAndScripts({ 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;
|
||||
}
|
||||
|
||||
|
94
src/login/Template.useStylesAndScripts.ts
Normal file
94
src/login/Template.useStylesAndScripts.ts
Normal file
@ -0,0 +1,94 @@
|
||||
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[];
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext ? true : false>();
|
||||
assert<KcContext extends KcContextLike ? true : false>();
|
||||
|
||||
export function useStylesAndScripts(params: {
|
||||
kcContext: KcContextLike;
|
||||
doUseDefaultCss: boolean;
|
||||
}) {
|
||||
const { kcContext, doUseDefaultCss } = params;
|
||||
|
||||
const { url, locale, scripts } = 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: "importmap",
|
||||
textContent: JSON.stringify({
|
||||
imports: {
|
||||
rfc4648: `${url.resourcesCommonPath}/node_modules/rfc4648/lib/rfc4648.js`
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
type: "module",
|
||||
src: `${url.resourcesPath}/js/menu-button-links.js`
|
||||
},
|
||||
...scripts.map(src => ({
|
||||
type: "text/javascript" as const,
|
||||
src
|
||||
})),
|
||||
{
|
||||
type: "module",
|
||||
textContent: `
|
||||
import { checkCookiesAndSetTimer } from "${url.resourcesPath}/js/authChecker.js";
|
||||
|
||||
checkCookiesAndSetTimer(
|
||||
"${url.ssoLoginInOtherTabsUrl}"
|
||||
);
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (areAllStyleSheetsLoaded) {
|
||||
insertScriptTags();
|
||||
}
|
||||
}, [areAllStyleSheetsLoaded]);
|
||||
|
||||
return { isReadyToRender: areAllStyleSheetsLoaded };
|
||||
}
|
@ -43,7 +43,7 @@ export default function Login(props: PageProps<Extract<KcContext, { pageId: "log
|
||||
}
|
||||
socialProvidersNode={
|
||||
<>
|
||||
{realm.password && social.providers !== undefined && social.providers.length !== 0 && (
|
||||
{realm.password && social?.providers !== undefined && social.providers.length !== 0 && (
|
||||
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
||||
<hr />
|
||||
<h2>{msg("identity-provider-login-label")}</h2>
|
||||
|
39
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
39
src/login/pages/LoginIdpLinkConfirmOverride.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
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";
|
||||
|
||||
export default function LoginIdpLinkConfirmOverride(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm-override.ftl" }>, I18n>) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, idpDisplayName } = kcContext;
|
||||
|
||||
const { msg } = i18n;
|
||||
|
||||
return (
|
||||
<Template kcContext={kcContext} i18n={i18n} doUseDefaultCss={doUseDefaultCss} classes={classes} headerNode={msg("confirmOverrideIdpTitle")}>
|
||||
<form id="kc-register-form" action={url.loginAction} method="post">
|
||||
{msg("pageExpiredMsg1")}{" "}
|
||||
<a id="loginRestartLink" href={url.loginRestartFlowUrl}>
|
||||
{msg("doClickHere")}
|
||||
</a>
|
||||
<br />
|
||||
<br />
|
||||
<button
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonDefaultClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||
name="submitAction"
|
||||
id="confirmOverride"
|
||||
value="confirmOverride"
|
||||
>
|
||||
{msg("confirmOverrideIdpContinue", idpDisplayName)}
|
||||
</button>
|
||||
</form>
|
||||
</Template>
|
||||
);
|
||||
}
|
177
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
177
src/login/pages/LoginPasskeysConditionalAuthenticate.tsx
Normal file
@ -0,0 +1,177 @@
|
||||
import { Fragment } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useScript } from "keycloakify/login/pages/LoginPasskeysConditionalAuthenticate.useScript";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
|
||||
export default function LoginPasskeysConditionalAuthenticate(
|
||||
props: PageProps<Extract<KcContext, { pageId: "login-passkeys-conditional-authenticate.ftl" }>, I18n>
|
||||
) {
|
||||
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
|
||||
|
||||
const { messagesPerField, login, url, usernameHidden, shouldDisplayAuthenticators, authenticators, registrationDisabled, realm } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
const { kcClsx } = getKcClsx({
|
||||
doUseDefaultCss,
|
||||
classes
|
||||
});
|
||||
|
||||
const authButtonId = "authenticateWebAuthnButton";
|
||||
|
||||
useScript({ authButtonId, kcContext, i18n });
|
||||
|
||||
return (
|
||||
<Template
|
||||
kcContext={kcContext}
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={msg("passkey-login-title")}
|
||||
infoNode={
|
||||
realm.registrationAllowed &&
|
||||
!registrationDisabled && (
|
||||
<div id="kc-registration">
|
||||
<span>
|
||||
${msg("noAccount")}{" "}
|
||||
<a tabIndex={6} href={url.registrationUrl}>
|
||||
{msg("doRegister")}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
>
|
||||
<form id="webauth" action={url.loginAction} method="post">
|
||||
<input type="hidden" id="clientDataJSON" name="clientDataJSON" />
|
||||
<input type="hidden" id="authenticatorData" name="authenticatorData" />
|
||||
<input type="hidden" id="signature" name="signature" />
|
||||
<input type="hidden" id="credentialId" name="credentialId" />
|
||||
<input type="hidden" id="userHandle" name="userHandle" />
|
||||
<input type="hidden" id="error" name="error" />
|
||||
</form>
|
||||
|
||||
<div className={kcClsx("kcFormGroupClass")} no-bottom-margin="true" style={{ marginBottom: 0 }}>
|
||||
{authenticators !== undefined && Object.keys(authenticators).length !== 0 && (
|
||||
<>
|
||||
<form id="authn_select" className={kcClsx("kcFormClass")}>
|
||||
{authenticators.authenticators.map((authenticator, i) => (
|
||||
<input key={i} type="hidden" name="authn_use_chk" readOnly value={authenticator.credentialId} />
|
||||
))}
|
||||
</form>
|
||||
{shouldDisplayAuthenticators && (
|
||||
<>
|
||||
{authenticators.authenticators.length > 1 && (
|
||||
<p className={kcClsx("kcSelectAuthListItemTitle")}>{msg("passkey-available-authenticators")}</p>
|
||||
)}
|
||||
<div className={kcClsx("kcFormClass")}>
|
||||
{authenticators.authenticators.map((authenticator, i) => (
|
||||
<div key={i} id={`kc-webauthn-authenticator-item-${i}`} className={kcClsx("kcSelectAuthListItemClass")}>
|
||||
<i
|
||||
className={clsx(
|
||||
(() => {
|
||||
const className = kcClsx(authenticator.transports.iconClass as any);
|
||||
if (className === authenticator.transports.iconClass) {
|
||||
return kcClsx("kcWebAuthnDefaultIcon");
|
||||
}
|
||||
return className;
|
||||
})(),
|
||||
kcClsx("kcSelectAuthListItemIconPropertyClass")
|
||||
)}
|
||||
/>
|
||||
<div className={kcClsx("kcSelectAuthListItemBodyClass")}>
|
||||
<div
|
||||
id={`kc-webauthn-authenticator-label-${i}`}
|
||||
className={kcClsx("kcSelectAuthListItemHeadingClass")}
|
||||
>
|
||||
{advancedMsg(authenticator.label)}
|
||||
</div>
|
||||
{authenticator.transports !== undefined &&
|
||||
authenticator.transports.displayNameProperties !== undefined &&
|
||||
authenticator.transports.displayNameProperties.length !== 0 && (
|
||||
<div
|
||||
id={`kc-webauthn-authenticator-transport-${i}`}
|
||||
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
|
||||
>
|
||||
{authenticator.transports.displayNameProperties.map((nameProperty, i, arr) => (
|
||||
<Fragment key={i}>
|
||||
<span key={i}> {advancedMsg(nameProperty)} </span>
|
||||
{i !== arr.length - 1 && <span>, </span>}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>
|
||||
<span id={`kc-webauthn-authenticator-createdlabel-${i}`}>{msg("passkey-createdAt-label")}</span>
|
||||
<span id={`kc-webauthn-authenticator-created-${i}`}>{authenticator.createdAt}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div id="kc-form">
|
||||
<div id="kc-form-wrapper">
|
||||
{realm.password && (
|
||||
<form
|
||||
id="kc-form-passkey"
|
||||
action={url.loginAction}
|
||||
method="post"
|
||||
style={{ display: "none" }}
|
||||
onSubmit={event => {
|
||||
try {
|
||||
// @ts-expect-error
|
||||
event.target.login.disabled = true;
|
||||
} catch {}
|
||||
|
||||
return true;
|
||||
}}
|
||||
>
|
||||
{!usernameHidden && (
|
||||
<div className={kcClsx("kcFormGroupClass")}>
|
||||
<label htmlFor="username" className={kcClsx("kcLabelClass")}>
|
||||
{msg("passkey-autofill-select")}
|
||||
</label>
|
||||
<input
|
||||
tabIndex={1}
|
||||
id="username"
|
||||
aria-invalid={messagesPerField.existsError("username")}
|
||||
className={kcClsx("kcInputClass")}
|
||||
name="username"
|
||||
defaultValue={login.username ?? ""}
|
||||
//autoComplete="username webauthn"
|
||||
type="text"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
/>
|
||||
{messagesPerField.existsError("username") && (
|
||||
<span id="input-error-username" className={kcClsx("kcInputErrorMessageClass")} aria-live="polite">
|
||||
{messagesPerField.get("username")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
<div id="kc-form-passkey-button" className={kcClsx("kcFormButtonsClass")} style={{ display: "none" }}>
|
||||
<input
|
||||
id={authButtonId}
|
||||
type="button"
|
||||
autoFocus
|
||||
value={msgStr("passkey-doAuthenticate")}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Template>
|
||||
);
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import { useEffect } from "react";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { KcContext } from "keycloakify/login/KcContext/KcContext";
|
||||
|
||||
type KcContextLike = {
|
||||
url: {
|
||||
resourcesPath: string;
|
||||
};
|
||||
isUserIdentified: boolean | "true" | "false";
|
||||
challenge: string;
|
||||
userVerification: string;
|
||||
rpId: string;
|
||||
createTimeout: number | string;
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext.LoginPasskeysConditionalAuthenticate ? true : false>();
|
||||
assert<KcContext.LoginPasskeysConditionalAuthenticate extends KcContextLike ? true : false>();
|
||||
|
||||
type I18nLike = {
|
||||
msgStr: (key: "webauthn-unsupported-browser-text" | "passkey-unsupported-browser-text") => string;
|
||||
isFetchingTranslations: boolean;
|
||||
};
|
||||
|
||||
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
|
||||
const { authButtonId, kcContext, i18n } = params;
|
||||
|
||||
const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
|
||||
|
||||
const { msgStr, isFetchingTranslations } = i18n;
|
||||
|
||||
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("${authButtonId}");
|
||||
const input = {
|
||||
isUserIdentified : ${isUserIdentified},
|
||||
challenge : ${JSON.stringify(challenge)},
|
||||
userVerification : ${JSON.stringify(userVerification)},
|
||||
rpId : ${JSON.stringify(rpId)},
|
||||
createTimeout : ${createTimeout}
|
||||
};
|
||||
authButton.addEventListener("click", () => {
|
||||
authenticateByWebAuthn({
|
||||
...input,
|
||||
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
|
||||
});
|
||||
});
|
||||
|
||||
initAuthenticate({
|
||||
...input,
|
||||
errmsg : ${JSON.stringify(msgStr("passkey-unsupported-browser-text"))}
|
||||
});
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingTranslations) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertScriptTags();
|
||||
}, [isFetchingTranslations]);
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { useScript } from "keycloakify/login/pages/LoginRecoveryAuthnCodeConfig.useScript";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
@ -18,130 +17,9 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "text/javascript",
|
||||
textContent: `
|
||||
const olRecoveryCodesListId = "kc-recovery-codes-list";
|
||||
|
||||
/* copy recovery codes */
|
||||
function copyRecoveryCodes() {
|
||||
var tmpTextarea = document.createElement("textarea");
|
||||
var codes = document.getElementById("kc-recovery-codes-list").getElementsByTagName("li");
|
||||
for (i = 0; i < codes.length; i++) {
|
||||
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\\n";
|
||||
}
|
||||
document.body.appendChild(tmpTextarea);
|
||||
tmpTextarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(tmpTextarea);
|
||||
}
|
||||
|
||||
var copyButton = document.getElementById("copyRecoveryCodes");
|
||||
copyButton && copyButton.addEventListener("click", function () {
|
||||
copyRecoveryCodes();
|
||||
});
|
||||
|
||||
/* download recovery codes */
|
||||
function formatCurrentDateTime() {
|
||||
var dt = new Date();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
};
|
||||
|
||||
return dt.toLocaleString('en-US', options);
|
||||
}
|
||||
|
||||
function parseRecoveryCodeList() {
|
||||
var recoveryCodes = document.querySelectorAll(".kc-recovery-codes-list li");
|
||||
var recoveryCodeList = "";
|
||||
|
||||
for (var i = 0; i < recoveryCodes.length; i++) {
|
||||
var recoveryCodeLiElement = recoveryCodes[i].innerText;
|
||||
recoveryCodeList += recoveryCodeLiElement + "\\r\\n";
|
||||
}
|
||||
|
||||
return recoveryCodeList;
|
||||
}
|
||||
|
||||
function buildDownloadContent() {
|
||||
var recoveryCodeList = parseRecoveryCodeList();
|
||||
var dt = new Date();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
};
|
||||
|
||||
return fileBodyContent =
|
||||
"${msgStr("recovery-codes-download-file-header")}\\n\\n" +
|
||||
recoveryCodeList + "\\n" +
|
||||
"${msgStr("recovery-codes-download-file-description")}\\n\\n" +
|
||||
"${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime();
|
||||
}
|
||||
|
||||
function setUpDownloadLinkAndDownload(filename, text) {
|
||||
var el = document.createElement('a');
|
||||
el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
el.setAttribute('download', filename);
|
||||
el.style.display = 'none';
|
||||
document.body.appendChild(el);
|
||||
el.click();
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
function downloadRecoveryCodes() {
|
||||
setUpDownloadLinkAndDownload('kc-download-recovery-codes.txt', buildDownloadContent());
|
||||
}
|
||||
|
||||
var downloadButton = document.getElementById("downloadRecoveryCodes");
|
||||
downloadButton && downloadButton.addEventListener("click", downloadRecoveryCodes);
|
||||
|
||||
/* print recovery codes */
|
||||
function buildPrintContent() {
|
||||
var recoveryCodeListHTML = document.getElementById('kc-recovery-codes-list').innerHTML;
|
||||
var styles =
|
||||
\`@page { size: auto; margin-top: 0; }
|
||||
body { width: 480px; }
|
||||
div { list-style-type: none; font-family: monospace }
|
||||
p:first-of-type { margin-top: 48px }\`;
|
||||
|
||||
return printFileContent =
|
||||
"<html><style>" + styles + "</style><body>" +
|
||||
"<title>kc-download-recovery-codes</title>" +
|
||||
"<p>${msgStr("recovery-codes-download-file-header")}</p>" +
|
||||
"<div>" + recoveryCodeListHTML + "</div>" +
|
||||
"<p>${msgStr("recovery-codes-download-file-description")}</p>" +
|
||||
"<p>${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime() + "</p>" +
|
||||
"</body></html>";
|
||||
}
|
||||
|
||||
function printRecoveryCodes() {
|
||||
var w = window.open();
|
||||
w.document.write(buildPrintContent());
|
||||
w.print();
|
||||
w.close();
|
||||
}
|
||||
|
||||
var printButton = document.getElementById("printRecoveryCodes");
|
||||
printButton && printButton.addEventListener("click", printRecoveryCodes);
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
insertScriptTags();
|
||||
}, []);
|
||||
useScript({ olRecoveryCodesListId, i18n });
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -164,7 +42,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ol id="kc-recovery-codes-list" className={kcClsx("kcRecoveryCodesList")}>
|
||||
<ol id={olRecoveryCodesListId} className={kcClsx("kcRecoveryCodesList")}>
|
||||
{recoveryAuthnCodesConfigBean.generatedRecoveryAuthnCodesList.map((code, index) => (
|
||||
<li key={index}>
|
||||
<span>{index + 1}:</span> {code.slice(0, 4)}-{code.slice(4, 8)}-{code.slice(8)}
|
||||
|
142
src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx
Normal file
142
src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import { useEffect } from "react";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
|
||||
type I18nLike = {
|
||||
msgStr: (key: "recovery-codes-download-file-header" | "recovery-codes-download-file-description" | "recovery-codes-download-file-date") => string;
|
||||
isFetchingTranslations: boolean;
|
||||
};
|
||||
|
||||
export function useScript(params: { olRecoveryCodesListId: string; i18n: I18nLike }) {
|
||||
const { olRecoveryCodesListId, i18n } = params;
|
||||
|
||||
const { msgStr, isFetchingTranslations } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "text/javascript",
|
||||
textContent: () => `
|
||||
|
||||
/* copy recovery codes */
|
||||
function copyRecoveryCodes() {
|
||||
var tmpTextarea = document.createElement("textarea");
|
||||
var codes = document.querySelectorAll("#${olRecoveryCodesListId} li");
|
||||
for (i = 0; i < codes.length; i++) {
|
||||
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\\n";
|
||||
}
|
||||
document.body.appendChild(tmpTextarea);
|
||||
tmpTextarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(tmpTextarea);
|
||||
}
|
||||
|
||||
var copyButton = document.getElementById("copyRecoveryCodes");
|
||||
copyButton && copyButton.addEventListener("click", function () {
|
||||
copyRecoveryCodes();
|
||||
});
|
||||
|
||||
/* download recovery codes */
|
||||
function formatCurrentDateTime() {
|
||||
var dt = new Date();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
};
|
||||
|
||||
return dt.toLocaleString('en-US', options);
|
||||
}
|
||||
|
||||
function parseRecoveryCodeList() {
|
||||
var recoveryCodes = document.querySelectorAll("#${olRecoveryCodesListId} li");
|
||||
var recoveryCodeList = "";
|
||||
|
||||
for (var i = 0; i < recoveryCodes.length; i++) {
|
||||
var recoveryCodeLiElement = recoveryCodes[i].innerText;
|
||||
recoveryCodeList += recoveryCodeLiElement + "\\r\\n";
|
||||
}
|
||||
|
||||
return recoveryCodeList;
|
||||
}
|
||||
|
||||
function buildDownloadContent() {
|
||||
var recoveryCodeList = parseRecoveryCodeList();
|
||||
var dt = new Date();
|
||||
var options = {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short'
|
||||
};
|
||||
|
||||
return fileBodyContent =
|
||||
${JSON.stringify(msgStr("recovery-codes-download-file-header"))} + "\\n\\n" +
|
||||
recoveryCodeList + "\\n" +
|
||||
${JSON.stringify(msgStr("recovery-codes-download-file-description"))} + "\\n\\n" +
|
||||
${JSON.stringify(msgStr("recovery-codes-download-file-date"))} + " " + formatCurrentDateTime();
|
||||
}
|
||||
|
||||
function setUpDownloadLinkAndDownload(filename, text) {
|
||||
var el = document.createElement('a');
|
||||
el.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
|
||||
el.setAttribute('download', filename);
|
||||
el.style.display = 'none';
|
||||
document.body.appendChild(el);
|
||||
el.click();
|
||||
document.body.removeChild(el);
|
||||
}
|
||||
|
||||
function downloadRecoveryCodes() {
|
||||
setUpDownloadLinkAndDownload('kc-download-recovery-codes.txt', buildDownloadContent());
|
||||
}
|
||||
|
||||
var downloadButton = document.getElementById("downloadRecoveryCodes");
|
||||
downloadButton && downloadButton.addEventListener("click", downloadRecoveryCodes);
|
||||
|
||||
/* print recovery codes */
|
||||
function buildPrintContent() {
|
||||
var recoveryCodeListHTML = document.getElementById('${olRecoveryCodesListId}').innerHTML;
|
||||
var styles =
|
||||
\`@page { size: auto; margin-top: 0; }
|
||||
body { width: 480px; }
|
||||
div { list-style-type: none; font-family: monospace }
|
||||
p:first-of-type { margin-top: 48px }\`;
|
||||
|
||||
return printFileContent =
|
||||
"<html><style>" + styles + "</style><body>" +
|
||||
"<title>kc-download-recovery-codes</title>" +
|
||||
"<p>" + ${JSON.stringify(msgStr("recovery-codes-download-file-header"))} + "</p>" +
|
||||
"<div>" + recoveryCodeListHTML + "</div>" +
|
||||
"<p>" + ${JSON.stringify(msgStr("recovery-codes-download-file-description"))} + "</p>" +
|
||||
"<p>" + ${JSON.stringify(msgStr("recovery-codes-download-file-date"))} + " " + formatCurrentDateTime() + "</p>" +
|
||||
"</body></html>";
|
||||
}
|
||||
|
||||
function printRecoveryCodes() {
|
||||
var w = window.open();
|
||||
w.document.write(buildPrintContent());
|
||||
w.print();
|
||||
w.close();
|
||||
}
|
||||
|
||||
var printButton = document.getElementById("printRecoveryCodes");
|
||||
printButton && printButton.addEventListener("click", printRecoveryCodes);
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingTranslations) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertScriptTags();
|
||||
}, [isFetchingTranslations]);
|
||||
}
|
@ -40,7 +40,7 @@ export default function LoginUsername(props: PageProps<Extract<KcContext, { page
|
||||
headerNode={msg("doLogIn")}
|
||||
socialProvidersNode={
|
||||
<>
|
||||
{realm.password && social.providers !== undefined && social.providers.length !== 0 && (
|
||||
{realm.password && social?.providers !== undefined && social.providers.length !== 0 && (
|
||||
<div id="kc-social-providers" className={kcClsx("kcFormSocialAccountSectionClass")}>
|
||||
<hr />
|
||||
<h2>{msg("identity-provider-login-label")}</h2>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFieldsProps";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
@ -19,9 +20,10 @@ export default function Register(props: RegisterProps) {
|
||||
classes
|
||||
});
|
||||
|
||||
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey, termsAcceptanceRequired } = kcContext;
|
||||
const { messageHeader, url, messagesPerField, recaptchaRequired, recaptchaVisible, recaptchaSiteKey, recaptchaAction, termsAcceptanceRequired } =
|
||||
kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
|
||||
const [areTermsAccepted, setAreTermsAccepted] = useState(false);
|
||||
@ -32,7 +34,7 @@ export default function Register(props: RegisterProps) {
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
headerNode={msg("registerTitle")}
|
||||
headerNode={messageHeader !== undefined ? advancedMsg(messageHeader) : msg("registerTitle")}
|
||||
displayMessage={messagesPerField.exists("global")}
|
||||
displayRequiredFields
|
||||
>
|
||||
@ -53,10 +55,10 @@ export default function Register(props: RegisterProps) {
|
||||
onAreTermsAcceptedValueChange={setAreTermsAccepted}
|
||||
/>
|
||||
)}
|
||||
{recaptchaRequired && (
|
||||
{recaptchaRequired && (recaptchaVisible || recaptchaAction === undefined) && (
|
||||
<div className="form-group">
|
||||
<div className={kcClsx("kcInputWrapperClass")}>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
|
||||
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} data-action={recaptchaAction}></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@ -68,6 +70,25 @@ export default function Register(props: RegisterProps) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{recaptchaRequired && !recaptchaVisible && recaptchaAction !== undefined ? (
|
||||
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||
<button
|
||||
className={clsx(
|
||||
kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass"),
|
||||
"g-recaptcha"
|
||||
)}
|
||||
data-sitekey={recaptchaSiteKey}
|
||||
data-callback={() => {
|
||||
(document.getElementById("kc-register-form") as HTMLFormElement).submit();
|
||||
}}
|
||||
data-action={recaptchaAction}
|
||||
type="submit"
|
||||
>
|
||||
{msg("doRegister")}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||
<input
|
||||
disabled={!isFormSubmittable || (termsAcceptanceRequired && !areTermsAccepted)}
|
||||
@ -76,6 +97,7 @@ export default function Register(props: RegisterProps) {
|
||||
value={msgStr("doRegister")}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</Template>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useEffect, Fragment } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { Fragment } from "react";
|
||||
import { clsx } from "keycloakify/tools/clsx";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { getKcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useScript } from "keycloakify/login/pages/WebauthnAuthenticate.useScript";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
@ -12,127 +11,17 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const {
|
||||
url,
|
||||
isUserIdentified,
|
||||
challenge,
|
||||
userVerification,
|
||||
rpId,
|
||||
createTimeout,
|
||||
messagesPerField,
|
||||
realm,
|
||||
registrationDisabled,
|
||||
authenticators,
|
||||
shouldDisplayAuthenticators
|
||||
} = kcContext;
|
||||
const { url, realm, registrationDisabled, authenticators, shouldDisplayAuthenticators } = kcContext;
|
||||
|
||||
const { msg, msgStr, advancedMsg } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "WebauthnAuthenticate",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "text/javascript",
|
||||
src: `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
|
||||
},
|
||||
{
|
||||
type: "text/javascript",
|
||||
src: `${url.resourcesPath}/js/base64url.js`
|
||||
},
|
||||
{
|
||||
type: "text/javascript",
|
||||
textContent: `
|
||||
const authButtonId = "authenticateWebAuthnButton";
|
||||
|
||||
function webAuthnAuthenticate() {
|
||||
let isUserIdentified = ${isUserIdentified};
|
||||
if (!isUserIdentified) {
|
||||
doAuthenticate([]);
|
||||
return;
|
||||
}
|
||||
checkAllowCredentials();
|
||||
}
|
||||
|
||||
function checkAllowCredentials() {
|
||||
let allowCredentials = [];
|
||||
let authn_use = document.forms['authn_select'].authn_use_chk;
|
||||
|
||||
if (authn_use !== undefined) {
|
||||
if (authn_use.length === undefined) {
|
||||
allowCredentials.push({
|
||||
id: base64url.decode(authn_use.value, {loose: true}),
|
||||
type: 'public-key',
|
||||
useScript({
|
||||
authButtonId,
|
||||
kcContext,
|
||||
i18n
|
||||
});
|
||||
} else {
|
||||
for (let i = 0; i < authn_use.length; i++) {
|
||||
allowCredentials.push({
|
||||
id: base64url.decode(authn_use[i].value, {loose: true}),
|
||||
type: 'public-key',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
doAuthenticate(allowCredentials);
|
||||
}
|
||||
|
||||
|
||||
function doAuthenticate(allowCredentials) {
|
||||
|
||||
// Check if WebAuthn is supported by this browser
|
||||
if (!window.PublicKeyCredential) {
|
||||
$("#error").val("${msgStr("webauthn-unsupported-browser-text")}");
|
||||
$("#webauth").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
let challenge = "${challenge}";
|
||||
let userVerification = "${userVerification}";
|
||||
let rpId = "${rpId}";
|
||||
let publicKey = {
|
||||
rpId : rpId,
|
||||
challenge: base64url.decode(challenge, { loose: true })
|
||||
};
|
||||
|
||||
let createTimeout = ${createTimeout};
|
||||
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
|
||||
|
||||
if (allowCredentials.length) {
|
||||
publicKey.allowCredentials = allowCredentials;
|
||||
}
|
||||
|
||||
if (userVerification !== 'not specified') publicKey.userVerification = userVerification;
|
||||
|
||||
navigator.credentials.get({publicKey})
|
||||
.then((result) => {
|
||||
window.result = result;
|
||||
|
||||
let clientDataJSON = result.response.clientDataJSON;
|
||||
let authenticatorData = result.response.authenticatorData;
|
||||
let signature = result.response.signature;
|
||||
|
||||
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), { pad: false }));
|
||||
$("#authenticatorData").val(base64url.encode(new Uint8Array(authenticatorData), { pad: false }));
|
||||
$("#signature").val(base64url.encode(new Uint8Array(signature), { pad: false }));
|
||||
$("#credentialId").val(result.id);
|
||||
if(result.response.userHandle) {
|
||||
$("#userHandle").val(base64url.encode(new Uint8Array(result.response.userHandle), { pad: false }));
|
||||
}
|
||||
$("#webauth").submit();
|
||||
})
|
||||
.catch((err) => {
|
||||
$("#error").val(err);
|
||||
$("#webauth").submit();
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
insertScriptTags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -140,8 +29,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
i18n={i18n}
|
||||
doUseDefaultCss={doUseDefaultCss}
|
||||
classes={classes}
|
||||
displayMessage={!messagesPerField.existsError("username")}
|
||||
displayInfo={realm.password && realm.registrationAllowed && !registrationDisabled}
|
||||
displayInfo={realm.registrationAllowed && !registrationDisabled}
|
||||
infoNode={
|
||||
<div id="kc-registration">
|
||||
<span>
|
||||
@ -179,7 +67,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
)}
|
||||
<div className={kcClsx("kcFormOptionsClass")}>
|
||||
{authenticators.authenticators.map((authenticator, i) => (
|
||||
<div key={i} id="kc-webauthn-authenticator" className={kcClsx("kcSelectAuthListItemClass")}>
|
||||
<div key={i} id={`kc-webauthn-authenticator-item-${i}`} className={kcClsx("kcSelectAuthListItemClass")}>
|
||||
<div className={kcClsx("kcSelectAuthListItemIconClass")}>
|
||||
<i
|
||||
className={clsx(
|
||||
@ -195,12 +83,15 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
/>
|
||||
</div>
|
||||
<div className={kcClsx("kcSelectAuthListItemArrowIconClass")}>
|
||||
<div id="kc-webauthn-authenticator-label" className={kcClsx("kcSelectAuthListItemHeadingClass")}>
|
||||
<div
|
||||
id={`kc-webauthn-authenticator-label-${i}`}
|
||||
className={kcClsx("kcSelectAuthListItemHeadingClass")}
|
||||
>
|
||||
{advancedMsg(authenticator.label)}
|
||||
</div>
|
||||
{authenticator.transports.displayNameProperties?.length && (
|
||||
<div
|
||||
id="kc-webauthn-authenticator-transport"
|
||||
id={`kc-webauthn-authenticator-transport-${i}`}
|
||||
className={kcClsx("kcSelectAuthListItemDescriptionClass")}
|
||||
>
|
||||
{authenticator.transports.displayNameProperties
|
||||
@ -217,8 +108,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
</div>
|
||||
)}
|
||||
<div className={kcClsx("kcSelectAuthListItemDescriptionClass")}>
|
||||
<span id="kc-webauthn-authenticator-created-label">{msg("webauthn-createdAt-label")}</span>
|
||||
<span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
|
||||
<span id={`kc-webauthn-authenticator-createdlabel-${i}`}>
|
||||
{msg("webauthn-createdAt-label")}
|
||||
</span>
|
||||
<span id={`kc-webauthn-authenticator-created-${i}`}>{authenticator.createdAt}</span>
|
||||
</div>
|
||||
<div className={kcClsx("kcSelectAuthListItemFillClass")} />
|
||||
</div>
|
||||
@ -229,16 +122,10 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div id="kc-form-buttons" className={kcClsx("kcFormButtonsClass")}>
|
||||
<input
|
||||
id="authenticateWebAuthnButton"
|
||||
id={authButtonId}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
assert("webAuthnAuthenticate" in window);
|
||||
assert(typeof window.webAuthnAuthenticate === "function");
|
||||
window.webAuthnAuthenticate();
|
||||
}}
|
||||
autoFocus
|
||||
value={msgStr("webauthn-doAuthenticate")}
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||
|
64
src/login/pages/WebauthnAuthenticate.useScript.tsx
Normal file
64
src/login/pages/WebauthnAuthenticate.useScript.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import { useEffect } from "react";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { KcContext } from "keycloakify/login/KcContext/KcContext";
|
||||
|
||||
type KcContextLike = {
|
||||
url: {
|
||||
resourcesPath: string;
|
||||
};
|
||||
isUserIdentified: "true" | "false";
|
||||
challenge: string;
|
||||
userVerification: KcContext.WebauthnAuthenticate["userVerification"];
|
||||
rpId: string;
|
||||
createTimeout: number | string;
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext.WebauthnAuthenticate ? true : false>();
|
||||
assert<KcContext.WebauthnAuthenticate extends KcContextLike ? true : false>();
|
||||
|
||||
type I18nLike = {
|
||||
msgStr: (key: "webauthn-unsupported-browser-text") => string;
|
||||
isFetchingTranslations: boolean;
|
||||
};
|
||||
|
||||
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
|
||||
const { authButtonId, kcContext, i18n } = params;
|
||||
|
||||
const { url, isUserIdentified, challenge, userVerification, rpId, createTimeout } = kcContext;
|
||||
|
||||
const { msgStr, isFetchingTranslations } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "WebauthnAuthenticate",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "module",
|
||||
textContent: () => `
|
||||
|
||||
import { authenticateByWebAuthn } from "${url.resourcesPath}/js/webauthnAuthenticate.js";
|
||||
const authButton = document.getElementById('${authButtonId}');
|
||||
authButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
isUserIdentified : ${isUserIdentified},
|
||||
challenge : '${challenge}',
|
||||
userVerification : '${userVerification}',
|
||||
rpId : '${rpId}',
|
||||
createTimeout : ${createTimeout},
|
||||
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
|
||||
};
|
||||
authenticateByWebAuthn(input);
|
||||
});
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingTranslations) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertScriptTags();
|
||||
}, [isFetchingTranslations]);
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { getKcClsx, type KcClsx } from "keycloakify/login/lib/kcClsx";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { useScript } from "keycloakify/login/pages/WebauthnRegister.useScript";
|
||||
import type { PageProps } from "keycloakify/login/pages/PageProps";
|
||||
import type { KcContext } from "../KcContext";
|
||||
import type { I18n } from "../i18n";
|
||||
@ -11,197 +9,17 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
|
||||
|
||||
const { kcClsx } = getKcClsx({ doUseDefaultCss, classes });
|
||||
|
||||
const {
|
||||
url,
|
||||
challenge,
|
||||
userid,
|
||||
username,
|
||||
signatureAlgorithms,
|
||||
rpEntityName,
|
||||
rpId,
|
||||
attestationConveyancePreference,
|
||||
authenticatorAttachment,
|
||||
requireResidentKey,
|
||||
userVerificationRequirement,
|
||||
createTimeout,
|
||||
excludeCredentialIds,
|
||||
isSetRetry,
|
||||
isAppInitiatedAction
|
||||
} = kcContext;
|
||||
const { url, isSetRetry, isAppInitiatedAction } = kcContext;
|
||||
|
||||
const { msg, msgStr } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "WebauthnRegister",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "text/javascript",
|
||||
src: `${url.resourcesCommonPath}/node_modules/jquery/dist/jquery.min.js`
|
||||
},
|
||||
{
|
||||
type: "text/javascript",
|
||||
src: `${url.resourcesPath}/js/base64url.js`
|
||||
},
|
||||
{
|
||||
type: "text/javascript",
|
||||
textContent: `
|
||||
function registerSecurityKey() {
|
||||
|
||||
// Check if WebAuthn is supported by this browser
|
||||
if (!window.PublicKeyCredential) {
|
||||
$("#error").val("${msgStr("webauthn-unsupported-browser-text")}");
|
||||
$("#register").submit();
|
||||
return;
|
||||
}
|
||||
|
||||
// mandatory parameters
|
||||
let challenge = "${challenge}";
|
||||
let userid = "${userid}";
|
||||
let username = "${username}";
|
||||
|
||||
let signatureAlgorithms =${JSON.stringify(signatureAlgorithms)};
|
||||
let pubKeyCredParams = getPubKeyCredParams(signatureAlgorithms);
|
||||
|
||||
let rpEntityName = "${rpEntityName}";
|
||||
let rp = {name: rpEntityName};
|
||||
|
||||
let publicKey = {
|
||||
challenge: base64url.decode(challenge, {loose: true}),
|
||||
rp: rp,
|
||||
user: {
|
||||
id: base64url.decode(userid, {loose: true}),
|
||||
name: username,
|
||||
displayName: username
|
||||
},
|
||||
pubKeyCredParams: pubKeyCredParams,
|
||||
};
|
||||
|
||||
// optional parameters
|
||||
let rpId = "${rpId}";
|
||||
publicKey.rp.id = rpId;
|
||||
|
||||
let attestationConveyancePreference = "${attestationConveyancePreference}";
|
||||
if (attestationConveyancePreference !== 'not specified') publicKey.attestation = attestationConveyancePreference;
|
||||
|
||||
let authenticatorSelection = {};
|
||||
let isAuthenticatorSelectionSpecified = false;
|
||||
|
||||
let authenticatorAttachment = "${authenticatorAttachment}";
|
||||
if (authenticatorAttachment !== 'not specified') {
|
||||
authenticatorSelection.authenticatorAttachment = authenticatorAttachment;
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
let requireResidentKey = "${requireResidentKey}";
|
||||
if (requireResidentKey !== 'not specified') {
|
||||
if (requireResidentKey === 'Yes')
|
||||
authenticatorSelection.requireResidentKey = true;
|
||||
else
|
||||
authenticatorSelection.requireResidentKey = false;
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
let userVerificationRequirement = "${userVerificationRequirement}";
|
||||
if (userVerificationRequirement !== 'not specified') {
|
||||
authenticatorSelection.userVerification = userVerificationRequirement;
|
||||
isAuthenticatorSelectionSpecified = true;
|
||||
}
|
||||
|
||||
if (isAuthenticatorSelectionSpecified) publicKey.authenticatorSelection = authenticatorSelection;
|
||||
|
||||
let createTimeout = ${createTimeout};
|
||||
if (createTimeout !== 0) publicKey.timeout = createTimeout * 1000;
|
||||
|
||||
let excludeCredentialIds = "${excludeCredentialIds}";
|
||||
let excludeCredentials = getExcludeCredentials(excludeCredentialIds);
|
||||
if (excludeCredentials.length > 0) publicKey.excludeCredentials = excludeCredentials;
|
||||
|
||||
navigator.credentials.create({publicKey})
|
||||
.then(function (result) {
|
||||
window.result = result;
|
||||
let clientDataJSON = result.response.clientDataJSON;
|
||||
let attestationObject = result.response.attestationObject;
|
||||
let publicKeyCredentialId = result.rawId;
|
||||
|
||||
$("#clientDataJSON").val(base64url.encode(new Uint8Array(clientDataJSON), {pad: false}));
|
||||
$("#attestationObject").val(base64url.encode(new Uint8Array(attestationObject), {pad: false}));
|
||||
$("#publicKeyCredentialId").val(base64url.encode(new Uint8Array(publicKeyCredentialId), {pad: false}));
|
||||
|
||||
if (typeof result.response.getTransports === "function") {
|
||||
let transports = result.response.getTransports();
|
||||
if (transports) {
|
||||
$("#transports").val(getTransportsAsString(transports));
|
||||
}
|
||||
} else {
|
||||
console.log("Your browser is not able to recognize supported transport media for the authenticator.");
|
||||
}
|
||||
|
||||
let initLabel = "WebAuthn Authenticator (Default Label)";
|
||||
let labelResult = window.prompt("Please input your registered authenticator's label", initLabel);
|
||||
if (labelResult === null) labelResult = initLabel;
|
||||
$("#authenticatorLabel").val(labelResult);
|
||||
|
||||
$("#register").submit();
|
||||
|
||||
})
|
||||
.catch(function (err) {
|
||||
$("#error").val(err);
|
||||
$("#register").submit();
|
||||
const authButtonId = "authenticateWebAuthnButton";
|
||||
|
||||
useScript({
|
||||
authButtonId,
|
||||
kcContext,
|
||||
i18n
|
||||
});
|
||||
}
|
||||
|
||||
function getPubKeyCredParams(signatureAlgorithmsList) {
|
||||
let pubKeyCredParams = [];
|
||||
if (signatureAlgorithmsList.length === 0) {
|
||||
pubKeyCredParams.push({type: "public-key", alg: -7});
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
||||
for (let i = 0; i < signatureAlgorithmsList.length; i++) {
|
||||
pubKeyCredParams.push({
|
||||
type: "public-key",
|
||||
alg: signatureAlgorithmsList[i]
|
||||
});
|
||||
}
|
||||
return pubKeyCredParams;
|
||||
}
|
||||
|
||||
function getExcludeCredentials(excludeCredentialIds) {
|
||||
let excludeCredentials = [];
|
||||
if (excludeCredentialIds === "") return excludeCredentials;
|
||||
|
||||
let excludeCredentialIdsList = excludeCredentialIds.split(',');
|
||||
|
||||
for (let i = 0; i < excludeCredentialIdsList.length; i++) {
|
||||
excludeCredentials.push({
|
||||
type: "public-key",
|
||||
id: base64url.decode(excludeCredentialIdsList[i],
|
||||
{loose: true})
|
||||
});
|
||||
}
|
||||
return excludeCredentials;
|
||||
}
|
||||
|
||||
function getTransportsAsString(transportsList) {
|
||||
if (transportsList === '' || Array.isArray(transportsList)) return "";
|
||||
|
||||
let transportsString = "";
|
||||
|
||||
for (let i = 0; i < transportsList.length; i++) {
|
||||
transportsString += transportsList[i] + ",";
|
||||
}
|
||||
|
||||
return transportsString.slice(0, -1);
|
||||
}
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
insertScriptTags();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Template
|
||||
@ -230,13 +48,8 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
|
||||
<input
|
||||
type="submit"
|
||||
className={kcClsx("kcButtonClass", "kcButtonPrimaryClass", "kcButtonBlockClass", "kcButtonLargeClass")}
|
||||
id="registerWebAuthn"
|
||||
id={authButtonId}
|
||||
value={msgStr("doRegisterSecurityKey")}
|
||||
onClick={() => {
|
||||
assert("registerSecurityKey" in window);
|
||||
assert(typeof window.registerSecurityKey === "function");
|
||||
window.registerSecurityKey();
|
||||
}}
|
||||
/>
|
||||
|
||||
{!isSetRetry && isAppInitiatedAction && (
|
||||
|
93
src/login/pages/WebauthnRegister.useScript.tsx
Normal file
93
src/login/pages/WebauthnRegister.useScript.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { useEffect } from "react";
|
||||
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
|
||||
import { assert } from "keycloakify/tools/assert";
|
||||
import { KcContext } from "keycloakify/login/KcContext/KcContext";
|
||||
|
||||
type KcContextLike = {
|
||||
url: {
|
||||
resourcesPath: string;
|
||||
};
|
||||
challenge: string;
|
||||
userid: string;
|
||||
username: string;
|
||||
signatureAlgorithms: string[];
|
||||
rpEntityName: string;
|
||||
rpId: string;
|
||||
attestationConveyancePreference: string;
|
||||
authenticatorAttachment: string;
|
||||
requireResidentKey: string;
|
||||
userVerificationRequirement: string;
|
||||
createTimeout: number | string;
|
||||
excludeCredentialIds: string;
|
||||
};
|
||||
|
||||
assert<keyof KcContextLike extends keyof KcContext.WebauthnRegister ? true : false>();
|
||||
assert<KcContext.WebauthnRegister extends KcContextLike ? true : false>();
|
||||
|
||||
type I18nLike = {
|
||||
msgStr: (key: "webauthn-registration-init-label" | "webauthn-registration-init-label-prompt" | "webauthn-unsupported-browser-text") => string;
|
||||
isFetchingTranslations: boolean;
|
||||
};
|
||||
|
||||
export function useScript(params: { authButtonId: string; kcContext: KcContextLike; i18n: I18nLike }) {
|
||||
const { authButtonId, kcContext, i18n } = params;
|
||||
|
||||
const {
|
||||
url,
|
||||
challenge,
|
||||
userid,
|
||||
username,
|
||||
signatureAlgorithms,
|
||||
rpEntityName,
|
||||
rpId,
|
||||
attestationConveyancePreference,
|
||||
authenticatorAttachment,
|
||||
requireResidentKey,
|
||||
userVerificationRequirement,
|
||||
createTimeout,
|
||||
excludeCredentialIds
|
||||
} = kcContext;
|
||||
|
||||
const { msgStr, isFetchingTranslations } = i18n;
|
||||
|
||||
const { insertScriptTags } = useInsertScriptTags({
|
||||
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
|
||||
scriptTags: [
|
||||
{
|
||||
type: "module",
|
||||
textContent: () => `
|
||||
import { registerByWebAuthn } from "${url.resourcesPath}/js/webauthnRegister.js";
|
||||
const registerButton = document.getElementById('${authButtonId}');
|
||||
registerButton.addEventListener("click", function() {
|
||||
const input = {
|
||||
challenge : '${challenge}',
|
||||
userid : '${userid}',
|
||||
username : '${username}',
|
||||
signatureAlgorithms : ${JSON.stringify(signatureAlgorithms)},
|
||||
rpEntityName : ${JSON.stringify(rpEntityName)},
|
||||
rpId : ${JSON.stringify(rpId)},
|
||||
attestationConveyancePreference : ${JSON.stringify(attestationConveyancePreference)},
|
||||
authenticatorAttachment : ${JSON.stringify(authenticatorAttachment)},
|
||||
requireResidentKey : ${JSON.stringify(requireResidentKey)},
|
||||
userVerificationRequirement : ${JSON.stringify(userVerificationRequirement)},
|
||||
createTimeout : ${createTimeout},
|
||||
excludeCredentialIds : ${JSON.stringify(excludeCredentialIds)},
|
||||
initLabel : ${JSON.stringify(msgStr("webauthn-registration-init-label"))},
|
||||
initLabelPrompt : ${JSON.stringify(msgStr("webauthn-registration-init-label-prompt"))},
|
||||
errmsg : ${JSON.stringify(msgStr("webauthn-unsupported-browser-text"))}
|
||||
};
|
||||
registerByWebAuthn(input);
|
||||
});
|
||||
`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isFetchingTranslations) {
|
||||
return;
|
||||
}
|
||||
|
||||
insertScriptTags();
|
||||
}, [isFetchingTranslations]);
|
||||
}
|
@ -6,11 +6,11 @@ export type ScriptTag = ScriptTag.TextContent | ScriptTag.Src;
|
||||
|
||||
export namespace ScriptTag {
|
||||
type Common = {
|
||||
type: "text/javascript" | "module";
|
||||
type: "text/javascript" | "module" | "importmap";
|
||||
};
|
||||
|
||||
export type TextContent = Common & {
|
||||
textContent: string;
|
||||
textContent: string | (() => string);
|
||||
};
|
||||
export type Src = Common & {
|
||||
src: string;
|
||||
@ -69,7 +69,12 @@ export function useInsertScriptTags(params: {
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
const script = scripts[i];
|
||||
if ("textContent" in scriptTag) {
|
||||
if (script.textContent === scriptTag.textContent) {
|
||||
const textContent =
|
||||
typeof scriptTag.textContent === "function"
|
||||
? scriptTag.textContent()
|
||||
: scriptTag.textContent;
|
||||
|
||||
if (script.textContent === textContent) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
@ -90,7 +95,12 @@ export function useInsertScriptTags(params: {
|
||||
|
||||
(() => {
|
||||
if ("textContent" in scriptTag) {
|
||||
htmlElement.textContent = scriptTag.textContent;
|
||||
const textContent =
|
||||
typeof scriptTag.textContent === "function"
|
||||
? scriptTag.textContent()
|
||||
: scriptTag.textContent;
|
||||
|
||||
htmlElement.textContent = textContent;
|
||||
return;
|
||||
}
|
||||
if ("src" in scriptTag) {
|
||||
|
@ -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), {
|
||||
await rm(
|
||||
pathJoin(buildDirPath, WELL_KNOWN_DIRECTORY_BASE_NAME.DOT_KEYCLOAKIFY),
|
||||
{
|
||||
recursive: true,
|
||||
force: true
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
} satisfies Plugin;
|
||||
|
||||
|
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
18
stories/login/pages/LoginIdpLinkConfirmOverride.stories.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-idp-link-confirm-override.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-idp-link-confirm-override.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { createKcPageStory } from "../KcPageStory";
|
||||
|
||||
const { KcPageStory } = createKcPageStory({ pageId: "login-passkeys-conditional-authenticate.ftl" });
|
||||
|
||||
const meta = {
|
||||
title: "login/login-passkeys-conditional-authenticate.ftl",
|
||||
component: KcPageStory
|
||||
} satisfies Meta<typeof KcPageStory>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <KcPageStory />
|
||||
};
|
@ -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");
|
||||
}
|
||||
`;
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user