Compare commits

..

54 Commits

Author SHA1 Message Date
f488327885 Release candidate 2024-06-06 01:31:17 +02:00
593b929254 #525 2024-06-06 01:31:00 +02:00
9218e97315 Relase candidate 2024-06-06 00:33:51 +02:00
beb0e8bd77 Add missing translations 2024-06-06 00:33:34 +02:00
cace66e9f8 No longer need for a eslint rule ignore for with default vite starter 2024-06-05 23:30:28 +02:00
ef850c71fd Release candidate 2024-06-05 23:24:44 +02:00
aa8dc1919f Make getKcContext mock return type less narrow 2024-06-05 23:24:21 +02:00
c7c9b19853 Fix case error 2024-06-05 23:08:40 +02:00
68c26e0f5b Fix case issue 2024-06-05 22:55:09 +02:00
6bcdf286ef Add missing export 2024-06-05 22:53:14 +02:00
d9345396e8 Fix build 2024-06-05 22:48:13 +02:00
4c423900d4 Release candidate 2024-06-05 21:44:54 +02:00
504419b26d Fix storybook 2024-06-05 21:44:26 +02:00
6e058eafed It's less confusing to use "#" as urls in mock kcContext 2024-06-05 21:44:14 +02:00
08fc9d8631 Fix tests by updating vitest 2024-06-05 21:23:34 +02:00
e8a11991a0 Rename kcContext -> KcContext and improve consistency 2024-06-05 21:13:58 +02:00
3e6d679838 Release candidate 2024-06-05 18:50:26 +02:00
4dad859c4d Fix storybook 2024-06-05 18:50:00 +02:00
ef9c933ca8 Relase candidate 2024-06-05 18:42:50 +02:00
0461190a67 Do not export default the Fallback component 2024-06-05 18:42:32 +02:00
06b3211b08 Ease up the instentiation of i18n 2024-06-05 18:41:53 +02:00
2033a9ce0c Release candidate 2024-06-05 18:15:25 +02:00
fca18d9209 Add missing file to the NPM bundle 2024-06-05 18:15:13 +02:00
4f99088449 Release candidate 2024-06-05 06:11:18 +02:00
b1da684008 Re implement asset fetching 2024-06-05 06:10:11 +02:00
89fb6de2d5 Full ordering of stories 2024-06-05 06:09:42 +02:00
b665bae3bb Another improvement on storybook switching from one page to another 2024-06-05 01:32:31 +02:00
0b5a7544ca Address white falshes in storybook 2024-06-05 01:02:17 +02:00
183826ca0d Improve terms story 2024-06-04 04:06:29 +02:00
e507aace6b Change ordering of stories 2024-06-04 04:06:09 +02:00
43c93ef0b4 Update the intro story 2024-06-04 02:05:09 +02:00
093e51e092 Fix escaping error 2024-06-04 01:49:26 +02:00
17e1655eaf Fix recaptcha in storybook 2024-06-04 01:39:54 +02:00
6b570f2b9a Update register story 2024-06-03 23:54:08 +02:00
f239d105a7 Fix missing key 2024-06-03 23:53:53 +02:00
776d8378e3 Shorter white flash when changing stories 2024-06-03 23:40:33 +02:00
dd770cd7c6 Remove unessesary stories 2024-06-03 23:40:21 +02:00
4b3de54e18 Make it more conveignent to run storybook 2024-06-03 23:26:04 +02:00
5741cd1b2b Lower the priority of the without password story. 2024-06-03 23:25:37 +02:00
b780d7136e Fix mistake after using attributesByName instead of attributes 2024-06-03 23:25:02 +02:00
3c28a05746 Fix copy-keycloak-resources-to-public 2024-06-03 22:45:09 +02:00
57ac5badba Update the euristic for getting the NPM workspace root. 2024-06-03 22:37:22 +02:00
e873eb5123 Rollback typescript because updating storybook would add a one month delay to the release 2024-06-03 22:36:54 +02:00
c1a63edd71 Refactor kcContext, avoid having mocks in the dist https://github.com/keycloakify/keycloakify/discussions/299#discussioncomment-9616747 2024-06-03 18:28:34 +02:00
37a060c4db Change ordering of pages 2024-06-03 01:23:41 +02:00
157e4ac485 Add missing storybook pages 2024-06-03 01:23:28 +02:00
ba4d9675a8 More homogeneous storybook setup 2024-06-03 00:11:19 +02:00
e011fb094c Factorize parameters in storybook 2024-06-02 22:37:04 +02:00
f55a934939 Complete migration of storybook from @lordvlad #274 2024-06-02 22:29:53 +02:00
96a88fe865 Fix add remove button for multifield attributes 2024-06-02 00:31:08 +02:00
6cdb83d730 Fix the way we handle multivalued single fileld (multiselct, multiselect-checkboxes) 2024-06-02 00:24:07 +02:00
95f06df45d Extenalize some core logic from the ejectable component 2024-06-01 22:54:17 +02:00
ec52b357d5 Fix logical error with radibuttons 2024-05-30 23:23:16 +02:00
d84546cd7d Correct error validation password policy 2024-05-30 22:50:06 +02:00
174 changed files with 4452 additions and 4427 deletions

View File

@ -34,7 +34,6 @@ export function DocsContainer({ children, context }) {
.docblock-argstable-head th:nth-child(3), .docblock-argstable-body tr > td:nth-child(2) p {
font-size: 13px;
}
`}</style>
<BaseContainer
context={{
@ -64,11 +63,6 @@ export function CanvasContainer({ children }) {
return (
<>
<style>{`
body {
padding: 0 !important;
}
`}</style>
{children}
</>
);

View File

@ -0,0 +1,19 @@
<style>
body.sb-show-main.sb-main-padded {
padding: 0;
}
body:not(.kcBodyClass) {
background-color: #393939;
}
body.sb-show-preparing-docs > .sb-wrapper {
visibility: hidden;
}
body .sb-preparing-story {
visibility: hidden;
}
</style>

View File

@ -116,10 +116,45 @@ const { getHardCodedWeight } = (() => {
const orderedPagesPrefix = [
"Introduction",
"login/login.ftl",
"login/register-user-profile.ftl",
"login/register.ftl",
"login/terms.ftl",
"login/error.ftl",
"login/code.ftl",
"login/delete-account-confirm.ftl",
"login/delete-credential.ftl",
"login/frontchannel-logout.ftl",
"login/idp-review-user-profile.ftl",
"login/info.ftl",
"login/login-config-totp.ftl",
"login/login-idp-link-confirm.ftl",
"login/login-idp-link-email.ftl",
"login/login-oauth-grant.ftl",
"login/login-otp.ftl",
"login/login-page-expired.ftl",
"login/login-password.ftl",
"login/login-reset-otp.ftl",
"login/login-reset-password.ftl",
"login/login-update-password.ftl",
"login/login-update-profile.ftl",
"login/login-username.ftl",
"login/login-verify-email.ftl",
"login/login-x509-info.ftl",
"login/logout-confirm.ftl",
"login/saml-post-form.ftl",
"login/select-authenticator.ftl",
"login/update-email.ftl",
"login/webauthn-authenticate.ftl",
"login/webauthn-error.ftl",
"login/webauthn-register.ftl",
"login/login-oauth2-device-verify-user-code.ftl",
"login/login-recovery-authn-code-config.ftl",
"login/login-recovery-authn-code-input.ftl",
"account/account.ftl",
"account/password.ftl",
"account/federatedIdentity.ftl",
"account/log.ftl",
"account/sessions.ftl",
"account/totp.ftl",
];
function getHardCodedWeight(kind) {

View File

@ -0,0 +1,49 @@
## 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]**.

View File

@ -0,0 +1,49 @@
## 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]**.

View File

@ -0,0 +1,49 @@
## 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]**.

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "10.0.0-rc.23",
"version": "10.0.0-rc.31",
"description": "Create Keycloak themes using React",
"repository": {
"type": "git",
@ -9,7 +9,7 @@
"scripts": {
"prepare": "patch-package && ts-node --skipProject scripts/generate-i18n-messages.ts",
"build": "ts-node --skipProject scripts/build.ts",
"storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && start-storybook -p 6006",
"storybook": "ts-node --skipProject scripts/start-storybook.ts",
"link-in-starter": "ts-node --skipProject scripts/link-in-starter.ts",
"test": "yarn test:types && vitest run",
"test:types": "tsc -p test/tsconfig.json --noEmit",
@ -17,8 +17,7 @@
"format": "yarn _format --write",
"format:check": "yarn _format --list-different",
"link-in-app": "ts-node --skipProject scripts/link-in-app.ts",
"copy-keycloak-resources-to-storybook-static": "PUBLIC_DIR_PATH=.storybook/static node dist/bin/main.js copy-keycloak-resources-to-public",
"build-storybook": "yarn build && yarn copy-keycloak-resources-to-storybook-static && build-storybook",
"build-storybook": "ts-node --skipProject scripts/build-storybook.ts",
"dump-keycloak-realm": "ts-node --skipProject scripts/dump-keycloak-realm.ts"
},
"bin": {
@ -47,6 +46,7 @@
"dist/bin/shared/constants.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.d.ts",
"dist/vite-plugin/vite-plugin.d.ts",
"dist/vite-plugin/index.js"
],
"keywords": [
@ -65,7 +65,6 @@
"react": "*"
},
"dependencies": {
"minimal-polyfills": "^2.2.3",
"react-markdown": "^5.0.3",
"tsafe": "^1.6.6"
},
@ -116,9 +115,9 @@
"ts-node": "^10.9.2",
"tsc-alias": "^1.8.10",
"tss-react": "^4.9.10",
"typescript": "^5.4.5",
"typescript": "^4.9.1-beta",
"vite": "^5.2.11",
"vitest": "^0.29.8",
"vitest": "^1.6.0",
"yauzl": "^2.10.0",
"zod": "^3.17.10",
"evt": "^2.5.7"

View File

@ -0,0 +1,19 @@
import * as child_process from "child_process";
import { join } from "path";
run("yarn build");
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
env: {
...process.env,
PUBLIC_DIR_PATH: join(".storybook", "static")
}
});
run("npx build-storybook");
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
console.log(`$ ${command}`);
child_process.execSync(command, { stdio: "inherit", ...options });
}

View File

@ -6,10 +6,12 @@ import {
dirname as pathDirname,
sep as pathSep
} from "path";
import { assert } from "tsafe/assert";
import { same } from "evt/tools/inDepth";
import { crawl } from "../src/bin/tools/crawl";
import { downloadKeycloakDefaultTheme } from "../src/bin/shared/downloadKeycloakDefaultTheme";
import { getThisCodebaseRootDirPath } from "../src/bin/tools/getThisCodebaseRootDirPath";
import { rmSync } from "../src/bin/tools/fs.rmSync";
import { deepAssign } from "../src/tools/deepAssign";
// NOTE: To run without argument when we want to generate src/i18n/generated_kcMessages files,
// update the version array for generating for newer version.
@ -73,12 +75,35 @@ async function main() {
}
Object.keys(record).forEach(themeType => {
const recordForPageType = record[themeType];
if (themeType !== "login" && themeType !== "account") {
return;
}
const recordForThemeType = record[themeType];
const languages = Object.keys(recordForThemeType);
const keycloakifyExtraMessages = (() => {
switch (themeType) {
case "login":
return keycloakifyExtraMessages_login;
case "account":
return keycloakifyExtraMessages_account;
}
assert(false);
})();
assert(
same(languages, Object.keys(keycloakifyExtraMessages), {
takeIntoAccountArraysOrdering: false
})
);
deepAssign({
target: recordForThemeType,
source: keycloakifyExtraMessages
});
const baseMessagesDirPath = pathJoin(
thisCodebaseRootDirPath,
"src",
@ -87,8 +112,6 @@ async function main() {
"baseMessages"
);
const languages = Object.keys(recordForPageType);
const generatedFileHeader = [
`//This code was automatically generated by running ${pathRelative(
thisCodebaseRootDirPath,
@ -110,7 +133,7 @@ async function main() {
"",
"/* spell-checker: disable */",
`const messages= ${JSON.stringify(
recordForPageType[language],
recordForThemeType[language],
null,
2
)};`,
@ -154,6 +177,491 @@ async function main() {
});
}
const keycloakifyExtraMessages_login: 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",
Record<
| "shouldBeEqual"
| "shouldBeDifferent"
| "shouldMatchPattern"
| "mustBeAnInteger"
| "notAValidOption"
| "selectAnOption"
| "remove"
| "addValue"
| "languages",
string
>
> = {
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
selectAnOption: "Select an option",
remove: "Remove",
addValue: "Add value",
languages: "Languages"
},
/* spell-checker: disable */
ar: {
shouldBeEqual: "{0} يجب أن يكون مساويًا لـ {1}",
shouldBeDifferent: "{0} يجب أن يكون مختلفًا عن {1}",
shouldMatchPattern: "`/يجب أن يطابق النمط: `/{0}/",
mustBeAnInteger: "يجب أن يكون عددًا صحيحًا",
notAValidOption: "ليس خيارًا صالحًا",
selectAnOption: "اختر خيارًا",
remove: "إزالة",
addValue: "أضف قيمة",
languages: "اللغات"
},
ca: {
shouldBeEqual: "{0} hauria de ser igual a {1}",
shouldBeDifferent: "{0} hauria de ser diferent de {1}",
shouldMatchPattern: "El patró hauria de coincidir: `/{0}/`",
mustBeAnInteger: "Ha de ser un enter",
notAValidOption: "No és una opció vàlida",
selectAnOption: "Selecciona una opció",
remove: "Elimina",
addValue: "Afegeix valor",
languages: "Idiomes"
},
cs: {
shouldBeEqual: "{0} by měl být roven {1}",
shouldBeDifferent: "{0} by měl být odlišný od {1}",
shouldMatchPattern: "Vzor by měl odpovídat: `/{0}/`",
mustBeAnInteger: "Musí být celé číslo",
notAValidOption: "Není platná možnost",
selectAnOption: "Vyberte možnost",
remove: "Odstranit",
addValue: "Přidat hodnotu",
languages: "Jazyky"
},
da: {
shouldBeEqual: "{0} bør være lig med {1}",
shouldBeDifferent: "{0} bør være forskellig fra {1}",
shouldMatchPattern: "Mønsteret bør matche: `/{0}/`",
mustBeAnInteger: "Skal være et heltal",
notAValidOption: "Ikke en gyldig mulighed",
selectAnOption: "Vælg en mulighed",
remove: "Fjern",
addValue: "Tilføj værdi",
languages: "Sprog"
},
de: {
shouldBeEqual: "{0} sollte gleich {1} sein",
shouldBeDifferent: "{0} sollte sich von {1} unterscheiden",
shouldMatchPattern: "Muster sollte übereinstimmen: `/{0}/`",
mustBeAnInteger: "Muss eine ganze Zahl sein",
notAValidOption: "Keine gültige Option",
selectAnOption: "Wählen Sie eine Option",
remove: "Entfernen",
addValue: "Wert hinzufügen",
languages: "Sprachen"
},
el: {
shouldBeEqual: "Το {0} πρέπει να είναι ίσο με {1}",
shouldBeDifferent: "Το {0} πρέπει να διαφέρει από το {1}",
shouldMatchPattern: "Το πρότυπο πρέπει να ταιριάζει: `/{0}/`",
mustBeAnInteger: "Πρέπει να είναι ακέραιος",
notAValidOption: "Δεν είναι μια έγκυρη επιλογή",
selectAnOption: "Επιλέξτε μια επιλογή",
remove: "Αφαίρεση",
addValue: "Προσθήκη τιμής",
languages: "Γλώσσες"
},
es: {
shouldBeEqual: "{0} debería ser igual a {1}",
shouldBeDifferent: "{0} debería ser diferente a {1}",
shouldMatchPattern: "El patrón debería coincidir: `/{0}/`",
mustBeAnInteger: "Debe ser un número entero",
notAValidOption: "No es una opción válida",
selectAnOption: "Selecciona una opción",
remove: "Eliminar",
addValue: "Añadir valor",
languages: "Idiomas"
},
fa: {
shouldBeEqual: "{0} باید برابر باشد با {1}",
shouldBeDifferent: "{0} باید متفاوت باشد از {1}",
shouldMatchPattern: "الگو باید مطابقت داشته باشد: `/{0}/`",
mustBeAnInteger: "باید یک عدد صحیح باشد",
notAValidOption: "یک گزینه معتبر نیست",
selectAnOption: "یک گزینه انتخاب کنید",
remove: "حذف",
addValue: "افزودن مقدار",
languages: "زبان‌ها"
},
fi: {
shouldBeEqual: "{0} pitäisi olla yhtä suuri kuin {1}",
shouldBeDifferent: "{0} pitäisi olla erilainen kuin {1}",
shouldMatchPattern: "Mallin tulisi vastata: `/{0}/`",
mustBeAnInteger: "On oltava kokonaisluku",
notAValidOption: "Ei ole kelvollinen vaihtoehto",
selectAnOption: "Valitse vaihtoehto",
remove: "Poista",
addValue: "Lisää arvo",
languages: "Kielet"
},
fr: {
shouldBeEqual: "{0} devrait être égal à {1}",
shouldBeDifferent: "{0} devrait être différent de {1}",
shouldMatchPattern: "Le motif devrait correspondre: `/{0}/`",
mustBeAnInteger: "Doit être un entier",
notAValidOption: "Pas une option valide",
selectAnOption: "Sélectionnez une option",
remove: "Supprimer",
addValue: "Ajouter une valeur",
languages: "Langues"
},
hu: {
shouldBeEqual: "{0} egyenlő kell legyen {1}-vel",
shouldBeDifferent: "{0} különbözőnek kell lennie, mint {1}",
shouldMatchPattern: "A mintának egyeznie kell: `/{0}/`",
mustBeAnInteger: "Egész számnak kell lennie",
notAValidOption: "Nem érvényes opció",
selectAnOption: "Válasszon egy lehetőséget",
remove: "Eltávolítás",
addValue: "Érték hozzáadása",
languages: "Nyelvek"
},
it: {
shouldBeEqual: "{0} dovrebbe essere uguale a {1}",
shouldBeDifferent: "{0} dovrebbe essere diverso da {1}",
shouldMatchPattern: "Il modello dovrebbe corrispondere: `/{0}/`",
mustBeAnInteger: "Deve essere un numero intero",
notAValidOption: "Non è un'opzione valida",
selectAnOption: "Seleziona un'opzione",
remove: "Rimuovi",
addValue: "Aggiungi valore",
languages: "Lingue"
},
ja: {
shouldBeEqual: "{0} は {1} と等しい必要があります",
shouldBeDifferent: "{0} は {1} と異なる必要があります",
shouldMatchPattern: "パターンは一致する必要があります: `/{0}/`",
mustBeAnInteger: "整数である必要があります",
notAValidOption: "有効なオプションではありません",
selectAnOption: "オプションを選択",
remove: "削除",
addValue: "値を追加",
languages: "言語"
},
lt: {
shouldBeEqual: "{0} turėtų būti lygus {1}",
shouldBeDifferent: "{0} turėtų skirtis nuo {1}",
shouldMatchPattern: "Šablonas turėtų atitikti: `/{0}/`",
mustBeAnInteger: "Turi būti sveikasis skaičius",
notAValidOption: "Netinkama parinktis",
selectAnOption: "Pasirinkite parinktį",
remove: "Pašalinti",
addValue: "Pridėti reikšmę",
languages: "Kalbos"
},
lv: {
shouldBeEqual: "{0} jābūt vienādam ar {1}",
shouldBeDifferent: "{0} jābūt atšķirīgam no {1}",
shouldMatchPattern: "Mustrim jāsakrīt: `/{0}/`",
mustBeAnInteger: "Jābūt veselam skaitlim",
notAValidOption: "Nav derīga opcija",
selectAnOption: "Izvēlieties opciju",
remove: "Noņemt",
addValue: "Pievienot vērtību",
languages: "Valodas"
},
nl: {
shouldBeEqual: "{0} moet gelijk zijn aan {1}",
shouldBeDifferent: "{0} moet verschillen van {1}",
shouldMatchPattern: "Patroon moet overeenkomen: `/{0}/`",
mustBeAnInteger: "Moet een geheel getal zijn",
notAValidOption: "Geen geldige optie",
selectAnOption: "Selecteer een optie",
remove: "Verwijderen",
addValue: "Waarde toevoegen",
languages: "Talen"
},
no: {
shouldBeEqual: "{0} skal være lik {1}",
shouldBeDifferent: "{0} skal være forskjellig fra {1}",
shouldMatchPattern: "Mønsteret skal matche: `/{0}/`",
mustBeAnInteger: "Må være et heltall",
notAValidOption: "Ikke et gyldig alternativ",
selectAnOption: "Velg et alternativ",
remove: "Fjern",
addValue: "Legg til verdi",
languages: "Språk"
},
pl: {
shouldBeEqual: "{0} powinno być równe {1}",
shouldBeDifferent: "{0} powinno być różne od {1}",
shouldMatchPattern: "Wzór pow inien pasować: `/{0}/`",
mustBeAnInteger: "Musi być liczbą całkowitą",
notAValidOption: "Nieprawidłowa opcja",
selectAnOption: "Wybierz opcję",
remove: "Usuń",
addValue: "Dodaj wartość",
languages: "Języki"
},
"pt-BR": {
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"
},
ru: {
shouldBeEqual: "{0} должно быть равно {1}",
shouldBeDifferent: "{0} должно отличаться от {1}",
shouldMatchPattern: "Шаблон должен соответствовать: `/{0}/`",
mustBeAnInteger: "Должно быть целым числом",
notAValidOption: "Недопустимый вариант",
selectAnOption: "Выберите вариант",
remove: "Удалить",
addValue: "Добавить значение",
languages: "Языки"
},
sk: {
shouldBeEqual: "{0} by mal byť rovnaký ako {1}",
shouldBeDifferent: "{0} by mal byť odlišný od {1}",
shouldMatchPattern: "Vzor by mal zodpovedať: `/{0}/`",
mustBeAnInteger: "Musí byť celé číslo",
notAValidOption: "Nie je platná možnosť",
selectAnOption: "Vyberte možnosť",
remove: "Odstrániť",
addValue: "Pridať hodnotu",
languages: "Jazyky"
},
sv: {
shouldBeEqual: "{0} bör vara lika med {1}",
shouldBeDifferent: "{0} bör vara annorlunda än {1}",
shouldMatchPattern: "Mönstret bör matcha: `/{0}/`",
mustBeAnInteger: "Måste vara ett heltal",
notAValidOption: "Inte ett giltigt alternativ",
selectAnOption: "Välj ett alternativ",
remove: "Ta bort",
addValue: "Lägg till värde",
languages: "Språk"
},
th: {
shouldBeEqual: "{0} ควรเท่ากับ {1}",
shouldBeDifferent: "{0} ควรแตกต่างจาก {1}",
shouldMatchPattern: "รูปแบบควรตรงกับ: `/{0}/`",
mustBeAnInteger: "ต้องเป็นจำนวนเต็ม",
notAValidOption: "ไม่ใช่ตัวเลือกที่ถูกต้อง",
selectAnOption: "เลือกตัวเลือก",
remove: "ลบ",
addValue: "เพิ่มค่า",
languages: "ภาษา"
},
tr: {
shouldBeEqual: "{0} {1} eşit olmalıdır",
shouldBeDifferent: "{0} {1} farklı olmalıdır",
shouldMatchPattern: "Desen eşleşmelidir: `/{0}/`",
mustBeAnInteger: "Tam sayı olmalıdır",
notAValidOption: "Geçerli bir seçenek değil",
selectAnOption: "Bir seçenek seçin",
remove: "Kaldır",
addValue: "Değer ekle",
languages: "Diller"
},
uk: {
shouldBeEqual: "{0} повинно бути рівним {1}",
shouldBeDifferent: "{0} повинно відрізнятися від {1}",
shouldMatchPattern: "Шаблон повинен відповідати: `/{0}/`",
mustBeAnInteger: "Повинно бути цілим числом",
notAValidOption: "Не є дійсною опцією",
selectAnOption: "Виберіть опцію",
remove: "Видалити",
addValue: "Додати значення",
languages: "Мови"
},
"zh-CN": {
shouldBeEqual: "{0} 应该等于 {1}",
shouldBeDifferent: "{0} 应该不同于 {1}",
shouldMatchPattern: "模式应匹配: `/{0}/`",
mustBeAnInteger: "必须是整数",
notAValidOption: "不是有效选项",
selectAnOption: "选择一个选项",
remove: "移除",
addValue: "添加值",
languages: "语言"
}
/* spell-checker: enable */
};
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",
Record<"newPasswordSameAsOld" | "passwordConfirmNotMatch", string>
> = {
en: {
newPasswordSameAsOld: "New password must be different from the old one",
passwordConfirmNotMatch: "Password confirmation does not match"
},
/* spell-checker: disable */
ar: {
newPasswordSameAsOld: "يجب أن تكون كلمة المرور الجديدة مختلفة عن القديمة",
passwordConfirmNotMatch: "تأكيد كلمة المرور لا يتطابق"
},
ca: {
newPasswordSameAsOld: "La nova contrasenya ha de ser diferent de l'anterior",
passwordConfirmNotMatch: "La confirmació de la contrasenya no coincideix"
},
cs: {
newPasswordSameAsOld: "Nové heslo musí být odlišné od starého",
passwordConfirmNotMatch: "Potvrzení hesla se neshoduje"
},
da: {
newPasswordSameAsOld: "Det nye kodeord skal være forskelligt fra det gamle",
passwordConfirmNotMatch: "Adgangskodebekræftelse matcher ikke"
},
de: {
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ää"
},
fr: {
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
},
hu: {
newPasswordSameAsOld: "Az új jelszónak különböznie kell az előzőtől",
passwordConfirmNotMatch: "A jelszó megerősítése nem egyezik"
},
it: {
newPasswordSameAsOld:
"La nuova password deve essere diversa da quella precedente",
passwordConfirmNotMatch: "La conferma della password non corrisponde"
},
ja: {
newPasswordSameAsOld: "新しいパスワードは古いパスワードと異なる必要があります",
passwordConfirmNotMatch: "パスワード確認が一致しません"
},
lt: {
newPasswordSameAsOld: "Naujas slaptažodis turi skirtis nuo seno",
passwordConfirmNotMatch: "Slaptažodžio patvirtinimas neatitinka"
},
lv: {
newPasswordSameAsOld: "Jaunajam parolam jābūt atšķirīgam no vecā",
passwordConfirmNotMatch: "Paroles apstiprināšana neatbilst"
},
nl: {
newPasswordSameAsOld: "Het nieuwe wachtwoord moet verschillend zijn van het oude",
passwordConfirmNotMatch: "Wachtwoordbevestiging komt niet overeen"
},
no: {
newPasswordSameAsOld: "Det nye passordet må være forskjellig fra det gamle",
passwordConfirmNotMatch: "Passordbekreftelsen stemmer ikke"
},
pl: {
newPasswordSameAsOld: "Nowe hasło musi być inne niż stare",
passwordConfirmNotMatch: "Potwierdzenie hasła nie pasuje"
},
"pt-BR": {
newPasswordSameAsOld: "A nova senha deve ser diferente da antiga",
passwordConfirmNotMatch: "A confirmação da senha não corresponde"
},
ru: {
newPasswordSameAsOld: "Новый пароль должен отличаться от старого",
passwordConfirmNotMatch: "Подтверждение пароля не совпадает"
},
sk: {
newPasswordSameAsOld: "Nové heslo musí byť odlišné od starého",
passwordConfirmNotMatch: "Potvrdenie hesla sa nezhoduje"
},
sv: {
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();
}

View File

@ -1,11 +1,7 @@
import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import chokidar from "chokidar";
import * as runExclusive from "run-exclusive";
import { Deferred } from "evt/tools/Deferred";
import chalk from "chalk";
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
fs.rmSync("node_modules", { recursive: true, force: true });
fs.rmSync("dist", { recursive: true, force: true });
@ -23,35 +19,7 @@ run("yarn install", { cwd: join("..", "keycloakify-starter") });
run(`npx ts-node --skipProject ${join("scripts", "link-in-app.ts")} keycloakify-starter`);
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
const runYarnBuild = runExclusive.build(async () => {
console.log(chalk.green("Running `yarn build`"));
const dCompleted = new Deferred<void>();
const child = child_process.spawn("yarn", ["build"], {
env: process.env
});
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", () => dCompleted.resolve());
await dCompleted.pr;
console.log("\n\n");
});
console.log(chalk.green("Watching for changes in src/"));
chokidar.watch("src", { ignoreInitial: true }).on("all", async () => {
await waitForDebounce();
runYarnBuild();
});
startRebuildOnSrcChange();
function run(command: string, options?: { cwd: string }) {
console.log(`$ ${command}`);

View File

@ -0,0 +1,31 @@
import * as child_process from "child_process";
import * as fs from "fs";
import { join } from "path";
import { startRebuildOnSrcChange } from "./startRebuildOnSrcChange";
run("yarn build");
run(`node ${join("dist", "bin", "main.js")} copy-keycloak-resources-to-public`, {
env: {
...process.env,
PUBLIC_DIR_PATH: join(".storybook", "static")
}
});
{
const child = child_process.spawn("npx", ["start-storybook", "-p", "6006"]);
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", process.exit.bind(process));
}
startRebuildOnSrcChange();
function run(command: string, options?: { env?: NodeJS.ProcessEnv }) {
console.log(`$ ${command}`);
child_process.execSync(command, { stdio: "inherit", ...options });
}

View File

@ -0,0 +1,36 @@
import * as child_process from "child_process";
import { waitForDebounceFactory } from "powerhooks/tools/waitForDebounce";
import chokidar from "chokidar";
import * as runExclusive from "run-exclusive";
import { Deferred } from "evt/tools/Deferred";
import chalk from "chalk";
export function startRebuildOnSrcChange() {
const { waitForDebounce } = waitForDebounceFactory({ delay: 400 });
const runYarnBuild = runExclusive.build(async () => {
console.log(chalk.green("Running `yarn build`"));
const dCompleted = new Deferred<void>();
const child = child_process.spawn("yarn", ["build"]);
child.stdout.on("data", data => process.stdout.write(data));
child.stderr.on("data", data => process.stderr.write(data));
child.on("exit", () => dCompleted.resolve());
await dCompleted.pr;
console.log("\n\n");
});
console.log(chalk.green("Watching for changes in src/"));
chokidar.watch("src", { ignoreInitial: true }).on("all", async () => {
await waitForDebounce();
runYarnBuild();
});
}

View File

@ -5,7 +5,7 @@ import {
import { assert } from "tsafe/assert";
/**
* This is an equivalent of process.env.PUBLIC_URL thay you can use in Webpack projects.
* This is an equivalent of process.env.PUBLIC_URL that you can use in Webpack projects.
* This works both in your main app and in your Keycloak theme.
*/
export const PUBLIC_URL = (() => {

View File

@ -1,9 +1,8 @@
import { lazy, Suspense } from "react";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { I18n } from "keycloakify/account/i18n";
import type { KcContext } from "./kcContext";
import { assert, type Equals } from "tsafe/assert";
import FederatedIdentity from "./pages/FederatedIdentity";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
const Password = lazy(() => import("keycloakify/account/pages/Password"));
const Account = lazy(() => import("keycloakify/account/pages/Account"));
@ -11,6 +10,7 @@ const Sessions = lazy(() => import("keycloakify/account/pages/Sessions"));
const Totp = lazy(() => import("keycloakify/account/pages/Totp"));
const Applications = lazy(() => import("keycloakify/account/pages/Applications"));
const Log = lazy(() => import("keycloakify/account/pages/Log"));
const FederatedIdentity = lazy(() => import("keycloakify/account/pages/FederatedIdentity"));
export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props;

View File

@ -1,6 +1,24 @@
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { ThemeType, AccountThemePageId } from "keycloakify/bin/shared/constants";
export type ExtendKcContext<
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
> = ValueOf<{
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
KcContext,
{ pageId: PageId }
> extends never
? KcContext.Common &
KcContextExtraProperties & {
pageId: PageId;
} & KcContextExtraPropertiesPerPage[PageId]
: Extract<KcContext, { pageId: PageId }> &
KcContextExtraProperties &
KcContextExtraPropertiesPerPage[PageId];
}>;
export type KcContext =
| KcContext.Password

View File

@ -0,0 +1,82 @@
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
import type { AccountThemePageId } from "keycloakify/bin/shared/constants";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { exclude } from "tsafe/exclude";
export function createGetKcContextMock<
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<
`${string}.ftl`,
Record<string, unknown>
>
>(params: {
kcContextExtraProperties: KcContextExtraProperties;
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
overridesPerPage?: {
[PageId in
| AccountThemePageId
| keyof KcContextExtraPropertiesPerPage]?: DeepPartial<
Extract<
ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>,
{ pageId: PageId }
>
>;
};
}) {
const {
kcContextExtraProperties,
kcContextExtraPropertiesPerPage,
overrides: overrides_global,
overridesPerPage: overridesPerPage_global
} = params;
type KcContext = ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>;
function getKcContextMock<
PageId extends AccountThemePageId | keyof KcContextExtraPropertiesPerPage
>(params: {
pageId: PageId;
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
// NOTE: We choose to have a return type less precise than Extract<KcContext, { pageId: PageId }> {
// because we want to be able to use the mock just as the real KcContext.
}): KcContext {
const { pageId, overrides } = params;
const kcContextMock = structuredCloneButFunctions(
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
...kcContextCommonMock,
pageId
}
);
[
kcContextExtraProperties,
kcContextExtraPropertiesPerPage[pageId],
overrides_global,
overridesPerPage_global?.[pageId],
overrides
]
.filter(exclude(undefined))
.forEach(overrides =>
deepAssign({
target: kcContextMock,
source: overrides
})
);
// @ts-expect-error
return kcContextMock;
}
return { getKcContextMock };
}

View File

@ -0,0 +1,2 @@
export type { ExtendKcContext, KcContext } from "./KcContext";
export { createGetKcContextMock } from "./getKcContextMock";

View File

@ -1,4 +1,4 @@
import "minimal-polyfills/Object.fromEntries";
import "keycloakify/tools/Object.fromEntries";
import { resources_common, keycloak_resources } from "keycloakify/bin/shared/constants";
import { id } from "tsafe/id";
import type { KcContext } from "./KcContext";
@ -40,98 +40,33 @@ export const kcContextCommonMock: KcContext.Common = {
locale: {
supported: [
/* spell-checker: disable */
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
label: "Deutsch",
languageTag: "de"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
label: "Norsk",
languageTag: "no"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
label: "Русский",
languageTag: "ru"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
label: "Svenska",
languageTag: "sv"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
label: "Português (Brasil)",
languageTag: "pt-BR"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
label: "Lietuvių",
languageTag: "lt"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
label: "English",
languageTag: "en"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
label: "Italiano",
languageTag: "it"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
label: "Français",
languageTag: "fr"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
label: "中文简体",
languageTag: "zh-CN"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
label: "Español",
languageTag: "es"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
label: "Čeština",
languageTag: "cs"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
label: "日本語",
languageTag: "ja"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
label: "Slovenčina",
languageTag: "sk"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
label: "Polski",
languageTag: "pl"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
label: "Català",
languageTag: "ca"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
label: "Nederlands",
languageTag: "nl"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
label: "Türkçe",
languageTag: "tr"
}
["de", "Deutsch"],
["no", "Norsk"],
["ru", "Русский"],
["sv", "Svenska"],
["pt-BR", "Português (Brasil)"],
["lt", "Lietuvių"],
["en", "English"],
["it", "Italiano"],
["fr", "Français"],
["zh-CN", "中文简体"],
["es", "Español"],
["cs", "Čeština"],
["ja", "日本語"],
["sk", "Slovenčina"],
["pl", "Polski"],
["ca", "Català"],
["nl", "Nederlands"],
["tr", "Türkçe"]
/* spell-checker: enable */
],
].map(
([languageTag, label]) =>
({
languageTag,
label,
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
}) as const
),
currentLanguageTag: "en"
},
features: {

View File

@ -1,14 +1,12 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { type TemplateProps } from "keycloakify/account/TemplateProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import { createUseInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useSetClassName } from "keycloakify/tools/useSetClassName";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
import { assert } from "keycloakify/tools/assert";
const { useInsertLinkTags } = createUseInsertLinkTags();
import { clsx } from "keycloakify/tools/clsx";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useSetClassName } from "keycloakify/tools/useSetClassName";
import type { TemplateProps } from "keycloakify/account/TemplateProps";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
export default function Template(props: TemplateProps<KcContext, I18n>) {
const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
@ -46,6 +44,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
componentOrHookName: "Template",
hrefs: !doUseDefaultCss
? []
: [
@ -74,7 +73,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<li>
<div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link">
{labelBySupportedLanguageTag[currentLanguageTag]}
</a>

View File

@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
export type TemplateProps<

View File

@ -1,9 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import "keycloakify/tools/Object.fromEntries";
import { useEffect, useState, useRef } from "react";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContext } from "../kcContext/KcContext";
import type { KcContext } from "../KcContext";
import { Reflect } from "tsafe/Reflect";
export const fallbackLanguageTag = "en";
@ -16,7 +17,7 @@ export type KcContextLike = {
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type MessageKey = keyof typeof fallbackMessages;
export type GenericI18n<MessageKey extends string> = {
/**
@ -106,12 +107,10 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
...createI18nTranslationFunctions({
fallbackMessages: {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
messages: {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any
}),
@ -137,7 +136,10 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
return i18n ?? null;
}
return { useI18n };
return {
useI18n,
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
};
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
@ -240,30 +242,3 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
}) as string
};
}
const keycloakifyExtraMessages = {
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
newPasswordSameAsOld: "New password must be different from the old one",
passwordConfirmNotMatch: "Password confirmation does not match"
},
fr: {
/* spell-checker: disable */
shouldBeEqual: "{0} doit être égal à {1}",
shouldBeDifferent: "{0} doit être différent de {1}",
shouldMatchPattern: "Dois respecter le schéma: `/{0}/`",
mustBeAnInteger: "Doit être un nombre entier",
notAValidOption: "N'est pas une option valide",
logoutConfirmTitle: "Déconnexion",
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
doLogout: "Se déconnecter",
newPasswordSameAsOld: "Le nouveau mot de passe doit être différent de l'ancien",
passwordConfirmNotMatch: "La confirmation du mot de passe ne correspond pas"
/* spell-checker: enable */
}
};

View File

@ -1 +1,2 @@
export type { I18n } from "./i18n";
export { createUseI18n } from "./i18n";

View File

@ -1,10 +1,4 @@
import Fallback from "keycloakify/account/Fallback";
export default Fallback;
export { getKcContext } from "keycloakify/account/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/account/kcContext/createGetKcContext";
export type { AccountThemePageId as PageId } from "keycloakify/bin/shared/constants";
export { createUseI18n } from "keycloakify/account/i18n/i18n";
export type { ExtendKcContext } from "keycloakify/account/KcContext";
export type { PageProps } from "keycloakify/account/pages/PageProps";
export { createGetKcContextMock } from "keycloakify/account/KcContext";
export { createUseI18n } from "keycloakify/account/i18n";

View File

@ -1,134 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { isStorybook } from "keycloakify/lib/isStorybook";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { symToStr } from "tsafe/symToStr";
import {
kcContextMocks,
kcContextCommonMock
} from "keycloakify/account/kcContext/kcContextMocks";
export function createGetKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
warn_that_mock_is_enbaled: {
if (isStorybook) {
break warn_that_mock_is_enbaled;
}
console.log(
`%cKeycloakify: ${symToStr({
mockPageId
})} set to ${mockPageId}.`,
"background: red; color: yellow; font-size: medium"
);
}
const kcContextDefaultMock = kcContextMocks.find(
({ pageId }) => pageId === mockPageId
);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(
({ pageId }) => pageId === mockPageId
);
if (mockDataPick !== undefined) {
deepAssign({
target: out,
source: mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
target: out,
source: storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
target: kcContext,
source:
kcContextDefaultMock !== undefined
? kcContextDefaultMock
: { pageId: mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
target: kcContext,
source: partialKcContextCustomMock
});
}
if (mockProperties !== undefined) {
deepAssign({
target: kcContext.properties,
source: mockProperties
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { kcContext: undefined as any };
}
if (realKcContext.themeType !== "account") {
return { kcContext: undefined as any };
}
return { kcContext: realKcContext as any };
}
return { getKcContext };
}

View File

@ -1,23 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const { getKcContext } = createGetKcContext({
mockData
});
const { kcContext } = getKcContext({ mockPageId });
return { kcContext };
}

View File

@ -1,15 +0,0 @@
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
import type { KcContext } from "./KcContext";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -1 +0,0 @@
export type { KcContext } from "./KcContext";

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Account(props: PageProps<Extract<KcContext, { pageId: "account.ftl" }>, I18n>) {

View File

@ -1,13 +1,9 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
function isArrayWithEmptyObject(variable: any): boolean {
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
}
export default function Applications(props: PageProps<Extract<KcContext, { pageId: "applications.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;
@ -136,3 +132,7 @@ export default function Applications(props: PageProps<Extract<KcContext, { pageI
</Template>
);
}
function isArrayWithEmptyObject(variable: any): boolean {
return Array.isArray(variable) && variable.length === 1 && typeof variable[0] === "object" && Object.keys(variable[0]).length === 0;
}

View File

@ -1,6 +1,6 @@
import { PageProps } from "keycloakify/account";
import { I18n } from "keycloakify/account/i18n";
import { KcContext } from "keycloakify/account/kcContext";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function FederatedIdentity(props: PageProps<Extract<KcContext, { pageId: "federatedIdentity.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;

View File

@ -1,8 +1,8 @@
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import { Key } from "react";
import type { Key } from "react";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { I18n } from "../i18n";
import type { KcContext } from "../KcContext";
export default function Log(props: PageProps<Extract<KcContext, { pageId: "log.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;

View File

@ -1,11 +1,11 @@
import type { I18n } from "keycloakify/account/i18n";
import type { TemplateProps, ClassKey } from "keycloakify/account/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { KcContext } from "keycloakify/account/kcContext";
import type { I18n } from "../i18n";
import type { KcContext } from "../KcContext";
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
export type PageProps<NarrowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: NarowedKcContext;
kcContext: NarrowedKcContext;
i18n: I18nExtended;
doUseDefaultCss: boolean;
classes?: Partial<Record<ClassKey, string>>;

View File

@ -1,8 +1,8 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Password(props: PageProps<Extract<KcContext, { pageId: "password.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Sessions(props: PageProps<Extract<KcContext, { pageId: "sessions.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/account/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Totp(props: PageProps<Extract<KcContext, { pageId: "totp.ftl" }>, I18n>) {

View File

@ -8,9 +8,6 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
const buildOptions = readBuildOptions({ cliCommandOptions });
await copyKeycloakResourcesToPublic({
buildOptions: {
...buildOptions,
publicDirPath: buildOptions.reactAppRootDirPath
}
buildOptions
});
}

View File

@ -288,19 +288,19 @@ function decodeHtmlEntities(htmlStr){
are_same_path(path, [])
) || (
<#-- attributesByName adds a lot of noise to the output and is not needed -->
key == "attributesByName" &&
(
are_same_path(path, ["profile"]) ||
are_same_path(path, ["register"])
)
) || (
key == "attributes" &&
are_same_path(path, ["profile"])
) || (
<#-- We already have the attributes in profile speedup the rendering by filtering it out from the register object -->
(key == "attributes" || key == "attributesByName") &&
are_same_path(path, ["register"])
)
>
<#local out_seq += ["/*" + path?join(".") + "." + key + " excluded*/"]>
<#continue>
</#if>
USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2
<#-- https://github.com/keycloakify/keycloakify/discussions/406 -->
<#if (

View File

@ -21,6 +21,7 @@ export type BuildOptionsLike = {
urlPathname: string | undefined;
reactAppBuildDirPath: string;
assetsDirPath: string;
kcContextExclusionsFtlCode: string | undefined;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();
@ -140,6 +141,10 @@ export function generateFtlFilesCodeFactory(params: {
.replace(
"lOCALIZATION_REALM_OVERRIDES_USER_PROFILE_PROPERTY_KEY_aaGLsPgGIdeeX",
nameOfTheLocalizationRealmOverridesUserProfileProperty
)
.replace(
"USER_DEFINED_EXCLUSIONS_eKsaY4ZsZ4eMr2",
buildOptions.kcContextExclusionsFtlCode ?? ""
);
const ftlObjectToJsCodeDeclaringAnObjectPlaceholder =
'{ "x": "vIdLqMeOed9sdLdIdOxdK0d" }';

View File

@ -10,7 +10,7 @@ import {
import { downloadKeycloakDefaultTheme } from "../../shared/downloadKeycloakDefaultTheme";
import { transformCodebase } from "../../tools/transformCodebase";
type BuildOptionsLike = {
export type BuildOptionsLike = {
cacheDirPath: string;
npmWorkspaceRootDirPath: string;
keycloakifyBuildDirPath: string;

View File

@ -3,7 +3,10 @@ import * as fs from "fs";
import { join as pathJoin, resolve as pathResolve } from "path";
import { replaceImportsInJsCode } from "../replacers/replaceImportsInJsCode";
import { replaceImportsInCssCode } from "../replacers/replaceImportsInCssCode";
import { generateFtlFilesCodeFactory } from "../generateFtl";
import {
generateFtlFilesCodeFactory,
type BuildOptionsLike as BuildOptionsLike_kcContextExclusionsFtlCode
} from "../generateFtl";
import {
type ThemeType,
lastKeycloakVersionWithAccountV1,
@ -16,11 +19,17 @@ import {
import { isInside } from "../../tools/isInside";
import type { BuildOptions } from "../../shared/buildOptions";
import { assert, type Equals } from "tsafe/assert";
import { downloadKeycloakStaticResources } from "../../shared/downloadKeycloakStaticResources";
import {
downloadKeycloakStaticResources,
type BuildOptionsLike as BuildOptionsLike_downloadKeycloakStaticResources
} from "../../shared/downloadKeycloakStaticResources";
import { readFieldNameUsage } from "./readFieldNameUsage";
import { readExtraPagesNames } from "./readExtraPageNames";
import { generateMessageProperties } from "./generateMessageProperties";
import { bringInAccountV1 } from "./bringInAccountV1";
import {
bringInAccountV1,
type BuildOptionsLike as BuildOptionsLike_bringInAccountV1
} from "./bringInAccountV1";
import { getThemeSrcDirPath } from "../../shared/getThemeSrcDirPath";
import { rmSync } from "../../tools/fs.rmSync";
import { readThisNpmPackageVersion } from "../../tools/readThisNpmPackageVersion";
@ -30,19 +39,18 @@ import {
} from "../../shared/metaInfKeycloakThemes";
import { objectEntries } from "tsafe/objectEntries";
export type BuildOptionsLike = {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined;
themeVersion: string;
loginThemeResourcesFromKeycloakVersion: string;
reactAppBuildDirPath: string;
cacheDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
npmWorkspaceRootDirPath: string;
reactAppRootDirPath: string;
keycloakifyBuildDirPath: string;
};
export type BuildOptionsLike = BuildOptionsLike_kcContextExclusionsFtlCode &
BuildOptionsLike_downloadKeycloakStaticResources &
BuildOptionsLike_bringInAccountV1 & {
bundler: "vite" | "webpack";
extraThemeProperties: string[] | undefined;
loginThemeResourcesFromKeycloakVersion: string;
reactAppBuildDirPath: string;
assetsDirPath: string;
urlPathname: string | undefined;
reactAppRootDirPath: string;
keycloakifyBuildDirPath: string;
};
assert<BuildOptions extends BuildOptionsLike ? true : false>();

View File

@ -21,7 +21,7 @@ export function readExtraPagesNames(params: {
}).filter(filePath => /\.(ts|tsx|js|jsx)$/.test(filePath));
const candidateFilePaths = filePaths.filter(filePath =>
/kcContext\.[^.]+$/.test(filePath)
/[kK]cContext\.[^.]+$/.test(filePath)
);
if (candidateFilePaths.length === 0) {

View File

@ -30,6 +30,7 @@ export type BuildOptions = {
urlPathname: string | undefined;
assetsDirPath: string;
npmWorkspaceRootDirPath: string;
kcContextExclusionsFtlCode: string | undefined;
};
export type UserProvidedBuildOptions = {
@ -39,6 +40,7 @@ export type UserProvidedBuildOptions = {
loginThemeResourcesFromKeycloakVersion?: string;
keycloakifyBuildDirPath?: string;
themeName?: string | string[];
kcContextExclusionsFtlCode?: string;
};
export type ResolvedViteConfig = {
@ -302,6 +304,7 @@ export function readBuildOptions(params: {
return pathJoin(reactAppBuildDirPath, resolvedViteConfig.assetsDir);
})(),
npmWorkspaceRootDirPath
npmWorkspaceRootDirPath,
kcContextExclusionsFtlCode: userProvidedBuildOptions.kcContextExclusionsFtlCode
};
}

View File

@ -14,6 +14,8 @@ export function getNpmWorkspaceRootDirPath(params: {
pathJoin(...[reactAppRootDirPath, ...Array(depth).fill("..")])
);
assert(cwd !== pathSep, "NPM workspace not found");
try {
child_process.execSync("npm config get", {
cwd,
@ -21,48 +23,46 @@ export function getNpmWorkspaceRootDirPath(params: {
});
} catch (error) {
if (String(error).includes("ENOWORKSPACES")) {
assert(cwd !== pathSep, "NPM workspace not found");
return callee(depth + 1);
}
throw error;
}
const { isExpectedDependencyFound } = (() => {
const packageJsonFilePath = pathJoin(cwd, "package.json");
const packageJsonFilePath = pathJoin(cwd, "package.json");
assert(fs.existsSync(packageJsonFilePath));
if (!fs.existsSync(packageJsonFilePath)) {
return callee(depth + 1);
}
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
assert(fs.existsSync(packageJsonFilePath));
let isExpectedDependencyFound = false;
const parsedPackageJson = JSON.parse(
fs.readFileSync(packageJsonFilePath).toString("utf8")
);
for (const dependenciesOrDevDependencies of [
"dependencies",
"devDependencies"
] as const) {
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
let isExpectedDependencyFound = false;
if (dependencies === undefined) {
continue;
}
for (const dependenciesOrDevDependencies of [
"dependencies",
"devDependencies"
] as const) {
const dependencies = parsedPackageJson[dependenciesOrDevDependencies];
assert(dependencies instanceof Object);
if (dependencies[dependencyExpected] === undefined) {
continue;
}
isExpectedDependencyFound = true;
if (dependencies === undefined) {
continue;
}
return { isExpectedDependencyFound };
})();
assert(dependencies instanceof Object);
if (!isExpectedDependencyFound) {
if (dependencies[dependencyExpected] === undefined) {
continue;
}
isExpectedDependencyFound = true;
}
if (!isExpectedDependencyFound && parsedPackageJson.name !== dependencyExpected) {
return callee(depth + 1);
}

View File

@ -1,9 +1,9 @@
import { lazy, Suspense } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { assert, type Equals } from "tsafe/assert";
import type { I18n } from "./i18n";
import type { KcContext } from "./kcContext";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
const Login = lazy(() => import("keycloakify/login/pages/Login"));

View File

@ -3,14 +3,28 @@ import type {
LoginThemePageId,
nameOfTheLocalizationRealmOverridesUserProfileProperty
} from "keycloakify/bin/shared/constants";
import type { ExtractAfterStartingWith } from "keycloakify/tools/ExtractAfterStartingWith";
import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
import type { MessageKey } from "../i18n/i18n";
type ExtractAfterStartingWith<
Prefix extends string,
StrEnum
> = StrEnum extends `${Prefix}${infer U}` ? U : never;
export type ExtendKcContext<
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<string, Record<string, unknown>>
> = ValueOf<{
[PageId in keyof KcContextExtraPropertiesPerPage | KcContext["pageId"]]: Extract<
KcContext,
{ pageId: PageId }
> extends never
? KcContext.Common &
KcContextExtraProperties & {
pageId: PageId;
} & KcContextExtraPropertiesPerPage[PageId]
: Extract<KcContext, { pageId: PageId }> &
KcContextExtraProperties &
KcContextExtraPropertiesPerPage[PageId];
}>;
/** Take theses type definition with a grain of salt.
* Some values might be undefined on some pages.
@ -138,12 +152,12 @@ export declare namespace KcContext {
getFirstError: (...fieldNames: string[]) => string;
};
properties: Record<string, string | undefined>;
authenticationSession?: {
authSessionId: string;
tabId: string;
ssoLoginInOtherTabsUrl: string;
};
properties: {};
__localizationRealmOverridesUserProfile?: Record<string, string>;
};
@ -585,7 +599,7 @@ export declare namespace KcContext {
}
export type UserProfile = {
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
html5DataAnnotations?: Record<string, string>;
};
@ -683,31 +697,31 @@ export type Attribute = {
| "photo";
};
export type Validators = Partial<{
length: Validators.DoIgnoreEmpty & Validators.Range;
integer: Validators.DoIgnoreEmpty & Validators.Range;
email: Validators.DoIgnoreEmpty;
pattern: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string };
options: Validators.Options;
multivalued: Validators.DoIgnoreEmpty & Validators.Range;
export type Validators = {
length?: Validators.DoIgnoreEmpty & Validators.Range;
integer?: Validators.DoIgnoreEmpty & Validators.Range;
email?: Validators.DoIgnoreEmpty;
pattern?: Validators.DoIgnoreEmpty & Validators.ErrorMessage & { pattern: string };
options?: Validators.Options;
multivalued?: Validators.DoIgnoreEmpty & Validators.Range;
// NOTE: Following are the validators for which we don't implement client side validation yet
// or for which the validation can't be performed on the client side.
/*
double: Validators.DoIgnoreEmpty & Validators.Range;
"up-immutable-attribute": {};
"up-attribute-required-by-metadata-value": {};
"up-username-has-value": {};
"up-duplicate-username": {};
"up-username-mutation": {};
"up-email-exists-as-username": {};
"up-blank-attribute-value": Validators.ErrorMessage & { "fail-on-null": boolean; };
"up-duplicate-email": {};
"local-date": Validators.DoIgnoreEmpty;
"person-name-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
uri: Validators.DoIgnoreEmpty;
"username-prohibited-characters": Validators.DoIgnoreEmpty & Validators.ErrorMessage;
double?: Validators.DoIgnoreEmpty & Validators.Range;
"up-immutable-attribute"?: {};
"up-attribute-required-by-metadata-value"?: {};
"up-username-has-value"?: {};
"up-duplicate-username"?: {};
"up-username-mutation"?: {};
"up-email-exists-as-username"?: {};
"up-blank-attribute-value"?: Validators.ErrorMessage & { "fail-on-null": boolean; };
"up-duplicate-email"?: {};
"local-date"?: Validators.DoIgnoreEmpty;
"person-name-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage;
uri?: Validators.DoIgnoreEmpty;
"username-prohibited-characters"?: Validators.DoIgnoreEmpty & Validators.ErrorMessage;
*/
}>;
};
export declare namespace Validators {
export type DoIgnoreEmpty = {

View File

@ -0,0 +1,82 @@
import type { ExtendKcContext, KcContext as KcContextBase } from "./KcContext";
import type { LoginThemePageId } from "keycloakify/bin/shared/constants";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { exclude } from "tsafe/exclude";
export function createGetKcContextMock<
KcContextExtraProperties extends { properties?: Record<string, string | undefined> },
KcContextExtraPropertiesPerPage extends Record<
`${string}.ftl`,
Record<string, unknown>
>
>(params: {
kcContextExtraProperties: KcContextExtraProperties;
kcContextExtraPropertiesPerPage: KcContextExtraPropertiesPerPage;
overrides?: DeepPartial<KcContextExtraProperties & KcContextBase.Common>;
overridesPerPage?: {
[PageId in
| LoginThemePageId
| keyof KcContextExtraPropertiesPerPage]?: DeepPartial<
Extract<
ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>,
{ pageId: PageId }
>
>;
};
}) {
const {
kcContextExtraProperties,
kcContextExtraPropertiesPerPage,
overrides: overrides_global,
overridesPerPage: overridesPerPage_global
} = params;
type KcContext = ExtendKcContext<
KcContextExtraProperties,
KcContextExtraPropertiesPerPage
>;
function getKcContextMock<
PageId extends LoginThemePageId | keyof KcContextExtraPropertiesPerPage
>(params: {
pageId: PageId;
overrides?: DeepPartial<Extract<KcContext, { pageId: PageId }>>;
// NOTE: We choose to have a return type less precise than Extract<KcContext, { pageId: PageId }> {
// because we want to be able to use the mock just as the real KcContext.
}): KcContext {
const { pageId, overrides } = params;
const kcContextMock = structuredCloneButFunctions(
kcContextMocks.find(kcContextMock => kcContextMock.pageId === pageId) ?? {
...kcContextCommonMock,
pageId
}
);
[
kcContextExtraProperties,
kcContextExtraPropertiesPerPage[pageId],
overrides_global,
overridesPerPage_global?.[pageId],
overrides
]
.filter(exclude(undefined))
.forEach(overrides =>
deepAssign({
target: kcContextMock,
source: overrides
})
);
// @ts-expect-error
return kcContextMock;
}
return { getKcContextMock };
}

View File

@ -0,0 +1,8 @@
export type {
ExtendKcContext,
KcContext,
Attribute,
PasswordPolicies,
Validators
} from "./KcContext";
export { createGetKcContextMock } from "./getKcContextMock";

View File

@ -1,4 +1,4 @@
import "minimal-polyfills/Object.fromEntries";
import "keycloakify/tools/Object.fromEntries";
import type { KcContext, Attribute } from "./KcContext";
import {
resources_common,
@ -9,71 +9,72 @@ import { id } from "tsafe/id";
import { assert, type Equals } from "tsafe/assert";
import { BASE_URL } from "keycloakify/lib/BASE_URL";
const attributes: Attribute[] = [
{
validators: {
length: {
"ignore.empty.value": true,
min: "3",
max: "255"
}
},
displayName: "${username}",
annotations: {},
required: true,
autocomplete: "username",
readOnly: false,
name: "username",
value: "xxxx"
},
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
const attributesByName = Object.fromEntries(
id<Attribute[]>([
{
validators: {
length: {
"ignore.empty.value": true,
min: "3",
max: "255"
}
},
email: {
"ignore.empty.value": true
displayName: "${username}",
annotations: {},
required: true,
autocomplete: "username",
readOnly: false,
name: "username"
},
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
},
email: {
"ignore.empty.value": true
},
pattern: {
"ignore.empty.value": true,
pattern: "gmail\\.com$"
}
},
pattern: {
"ignore.empty.value": true,
pattern: "gmail\\.com$"
}
displayName: "${email}",
annotations: {},
required: true,
autocomplete: "email",
readOnly: false,
name: "email"
},
displayName: "${email}",
annotations: {},
required: true,
autocomplete: "email",
readOnly: false,
name: "email"
},
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
},
displayName: "${firstName}",
annotations: {},
required: true,
readOnly: false,
name: "firstName"
},
displayName: "${firstName}",
annotations: {},
required: true,
readOnly: false,
name: "firstName"
},
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
},
displayName: "${lastName}",
annotations: {},
required: true,
readOnly: false,
name: "lastName"
}
];
{
validators: {
length: {
max: "255",
"ignore.empty.value": true
}
},
displayName: "${lastName}",
annotations: {},
required: true,
readOnly: false,
name: "lastName"
}
]).map(attribute => [attribute.name, attribute])
);
const resourcesPath = `${BASE_URL}${keycloak_resources}/login/resources`;
@ -86,12 +87,9 @@ export const kcContextCommonMock: KcContext.Common = {
loginAction: "#",
resourcesPath,
resourcesCommonPath: `${resourcesPath}/${resources_common}`,
loginRestartFlowUrl:
"/auth/realms/myrealm/login-actions/restart?client_id=account&tab_id=HoAx28ja4xg",
loginUrl:
"/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg",
ssoLoginInOtherTabsUrl:
"/auth/realms/myrealm/login-actions/switch?client_id=account&tab_id=HoAx28ja4xg"
loginRestartFlowUrl: "#",
loginUrl: "#",
ssoLoginInOtherTabsUrl: "#"
},
realm: {
name: "myrealm",
@ -112,98 +110,34 @@ export const kcContextCommonMock: KcContext.Common = {
locale: {
supported: [
/* spell-checker: disable */
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=de",
label: "Deutsch",
languageTag: "de"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=no",
label: "Norsk",
languageTag: "no"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ru",
label: "Русский",
languageTag: "ru"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sv",
label: "Svenska",
languageTag: "sv"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pt-BR",
label: "Português (Brasil)",
languageTag: "pt-BR"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=lt",
label: "Lietuvių",
languageTag: "lt"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=en",
label: "English",
languageTag: "en"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=it",
label: "Italiano",
languageTag: "it"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=fr",
label: "Français",
languageTag: "fr"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=zh-CN",
label: "中文简体",
languageTag: "zh-CN"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=es",
label: "Español",
languageTag: "es"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=cs",
label: "Čeština",
languageTag: "cs"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ja",
label: "日本語",
languageTag: "ja"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=sk",
label: "Slovenčina",
languageTag: "sk"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=pl",
label: "Polski",
languageTag: "pl"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=ca",
label: "Català",
languageTag: "ca"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=nl",
label: "Nederlands",
languageTag: "nl"
},
{
url: "/auth/realms/myrealm/login-actions/authenticate?client_id=account&tab_id=HoAx28ja4xg&execution=ee6c2834-46a4-4a20-a1b6-f6d6f6451b36&kc_locale=tr",
label: "Türkçe",
languageTag: "tr"
}
["de", "Deutsch"],
["no", "Norsk"],
["ru", "Русский"],
["sv", "Svenska"],
["pt-BR", "Português (Brasil)"],
["lt", "Lietuvių"],
["en", "English"],
["it", "Italiano"],
["fr", "Français"],
["zh-CN", "中文简体"],
["es", "Español"],
["cs", "Čeština"],
["ja", "日本語"],
["sk", "Slovenčina"],
["pl", "Polski"],
["ca", "Català"],
["nl", "Nederlands"],
["tr", "Türkçe"]
/* spell-checker: enable */
],
].map(
([languageTag, label]) =>
({
languageTag,
label,
url: "https://gist.github.com/garronej/52baaca1bb925f2296ab32741e062b8e"
}) as const
),
currentLanguageTag: "en"
},
auth: {
@ -223,13 +157,10 @@ export const kcContextCommonMock: KcContext.Common = {
const loginUrl = {
...kcContextCommonMock.url,
loginResetCredentialsUrl:
"/auth/realms/myrealm/login-actions/reset-credentials?client_id=account&tab_id=HoAx28ja4xg",
registrationUrl:
"/auth/realms/myrealm/login-actions/registration?client_id=account&tab_id=HoAx28ja4xg",
oauth2DeviceVerificationAction: "/auth/realms/myrealm/device",
oauthAction:
"/auth/realms/myrealm/login-actions/consent?client_id=account&tab_id=HoAx28ja4xg"
loginResetCredentialsUrl: "#",
registrationUrl: "#",
oauth2DeviceVerificationAction: "#",
oauthAction: "#"
};
export const kcContextMocks = [
@ -257,15 +188,14 @@ export const kcContextMocks = [
...kcContextCommonMock,
url: {
...loginUrl,
registrationAction:
"http://localhost:8080/auth/realms/myrealm/login-actions/registration?session_code=gwZdUeO7pbYpFTRxiIxRg_QtzMbtFTKrNu6XW_f8asM&execution=12146ce0-b139-4bbd-b25b-0eccfee6577e&client_id=account&tab_id=uS8lYfebLa0"
registrationAction: "#"
},
isAppInitiatedAction: false,
passwordRequired: true,
recaptchaRequired: false,
pageId: "register.ftl",
profile: {
attributes
attributesByName
},
scripts: [
//"https://www.google.com/recaptcha/api.js"
@ -416,7 +346,7 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "login-update-profile.ftl",
profile: {
attributes
attributesByName
}
}),
id<KcContext.LoginIdpLinkConfirm>({
@ -472,14 +402,16 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "idp-review-user-profile.ftl",
profile: {
attributes
attributesByName
}
}),
id<KcContext.UpdateEmail>({
...kcContextCommonMock,
pageId: "update-email.ftl",
profile: {
attributes: attributes.filter(attribute => attribute.name === "email")
attributesByName: {
email: attributesByName["email"]
}
}
}),
id<KcContext.SelectAuthenticator>({
@ -506,7 +438,7 @@ export const kcContextMocks = [
...kcContextCommonMock,
pageId: "saml-post-form.ftl",
samlPost: {
url: ""
url: "#"
}
}),
id<KcContext.LoginPageExpired>({

View File

@ -1,17 +1,14 @@
import { useEffect } from "react";
import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx";
import { type TemplateProps } from "keycloakify/login/TemplateProps";
import type { TemplateProps } from "keycloakify/login/TemplateProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { createUseInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { useInsertLinkTags } from "keycloakify/tools/useInsertLinkTags";
import { useSetClassName } from "keycloakify/tools/useSetClassName";
import type { KcContext } from "./kcContext";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
const { useInsertLinkTags } = createUseInsertLinkTags();
const { useInsertScriptTags } = createUseInsertScriptTags();
export default function Template(props: TemplateProps<KcContext, I18n>) {
const {
displayInfo = false,
@ -63,6 +60,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
}, []);
const { areAllStyleSheetsLoaded } = useInsertLinkTags({
componentOrHookName: "Template",
hrefs: !doUseDefaultCss
? []
: [
@ -75,6 +73,7 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
});
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "Template",
scriptTags: [
{
type: "module",
@ -130,11 +129,10 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div className={getClassName("kcLocaleMainClass")} id="kc-locale">
<div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div id="kc-locale-dropdown" className={clsx("menu-button-links", getClassName("kcLocaleDropDownClass"))}>
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<button
tabIndex={1}
id="kc-current-locale-link"
aria-label={msgStr("languages" as any)}
aria-label={msgStr("languages")}
aria-haspopup="true"
aria-expanded="false"
aria-controls="language-switch1"
@ -248,7 +246,6 @@ export default function Template(props: TemplateProps<KcContext, I18n>) {
<div className={getClassName("kcFormGroupClass")}>
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a
href="#"
id="try-another-way"

View File

@ -1,5 +1,5 @@
import type { ReactNode } from "react";
import type { KcContext } from "./kcContext";
import type { KcContext } from "./KcContext";
import type { I18n } from "./i18n";
export type TemplateProps<

View File

@ -1,8 +1,14 @@
import { useEffect, useReducer, Fragment } from "react";
import type { ClassKey } from "keycloakify/login/TemplateProps";
import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
import type { Attribute } from "keycloakify/login/kcContext/KcContext";
import { assert } from "tsafe/assert";
import type { ClassKey } from "keycloakify/login/TemplateProps";
import {
useUserProfileForm,
getButtonToDisplayForMultivaluedAttributeField,
type KcContextLike,
type FormAction,
type FormFieldError
} from "keycloakify/login/lib/useUserProfileForm";
import type { Attribute } from "keycloakify/login/KcContext";
import type { I18n } from "./i18n";
export type UserProfileFormFieldsProps = {
@ -413,92 +419,34 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
const { msg } = i18n;
const hasRemove = (() => {
if (values.length === 1) {
return false;
}
const { hasAdd, hasRemove } = getButtonToDisplayForMultivaluedAttributeField({ attribute, values, fieldIndex });
const minCount = (() => {
const { multivalued } = attribute.validators;
if (multivalued === undefined) {
return undefined;
}
const minStr = multivalued.min;
if (minStr === undefined) {
return undefined;
}
return parseInt(`${minStr}`);
})();
if (minCount === undefined) {
return true;
}
if (values.length === minCount) {
return false;
}
return true;
})();
const hasAdd = (() => {
if (fieldIndex + 1 !== values.length) {
return false;
}
const maxCount = (() => {
const { multivalued } = attribute.validators;
if (multivalued === undefined) {
return undefined;
}
const maxStr = multivalued.max;
if (maxStr === undefined) {
return undefined;
}
return parseInt(`${maxStr}`);
})();
if (maxCount === undefined) {
return false;
}
if (values.length === maxCount) {
return false;
}
return true;
})();
const idPostfix = `-${attribute.name}-${fieldIndex + 1}`;
return (
<>
{hasRemove && (
<button
id={`kc-remove-${attribute.name}-${fieldIndex + 1}`}
type="button"
className="pf-c-button pf-m-inline pf-m-link"
onClick={() =>
dispatchFormAction({
action: "update",
name: attribute.name,
valueOrValues: values.filter((_, i) => i !== fieldIndex)
})
}
>
{msg("remove")}
{hasRemove ? <>&nbsp;|&nbsp;</> : null}
</button>
<>
<button
id={`kc-remove${idPostfix}`}
type="button"
className="pf-c-button pf-m-inline pf-m-link"
onClick={() =>
dispatchFormAction({
action: "update",
name: attribute.name,
valueOrValues: values.filter((_, i) => i !== fieldIndex)
})
}
>
{msg("remove")}
</button>
{hasAdd ? <>&nbsp;|&nbsp;</> : null}
</>
)}
{hasAdd && (
<button
id="kc-add-titles-1"
id={`kc-add${idPostfix}`}
type="button"
className="pf-c-button pf-m-inline pf-m-link"
onClick={() =>
@ -580,7 +528,7 @@ function InputTagSelects(props: InputFiledByTypeProps) {
className={classInput}
aria-invalid={props.displayableErrors.length !== 0}
disabled={attribute.readOnly}
checked={valueOrValues.includes(option)}
checked={valueOrValues instanceof Array ? valueOrValues.includes(option) : valueOrValues === option}
onChange={event =>
formValidationDispatch({
action: "update",

View File

@ -1,9 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
import "keycloakify/tools/Object.fromEntries";
import { useEffect, useState, useRef } from "react";
import { assert } from "tsafe/assert";
import fallbackMessages from "./baseMessages/en";
import { getMessages } from "./baseMessages";
import { assert } from "tsafe/assert";
import type { KcContext } from "../kcContext/KcContext";
import type { KcContext } from "../KcContext";
import { Reflect } from "tsafe/Reflect";
export const fallbackLanguageTag = "en";
@ -17,7 +18,7 @@ export type KcContextLike = {
assert<KcContext extends KcContextLike ? true : false>();
export type MessageKey = keyof typeof fallbackMessages | keyof (typeof keycloakifyExtraMessages)[typeof fallbackLanguageTag];
export type MessageKey = keyof typeof fallbackMessages;
export type GenericI18n<MessageKey extends string> = {
/**
@ -107,12 +108,10 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
...createI18nTranslationFunctions({
fallbackMessages: {
...fallbackMessages,
...(keycloakifyExtraMessages[fallbackLanguageTag] ?? {}),
...(extraMessages[fallbackLanguageTag] ?? {})
} as any,
messages: {
...(await getMessages(currentLanguageTag)),
...((keycloakifyExtraMessages as any)[currentLanguageTag] ?? {}),
...(extraMessages[currentLanguageTag] ?? {})
} as any,
__localizationRealmOverridesUserProfile: kcContext.__localizationRealmOverridesUserProfile
@ -139,7 +138,10 @@ export function createUseI18n<ExtraMessageKey extends string = never>(extraMessa
return i18n ?? null;
}
return { useI18n };
return {
useI18n,
ofTypeI18n: Reflect<GenericI18n<MessageKey | ExtraMessageKey>>()
};
}
function createI18nTranslationFunctions<MessageKey extends string>(params: {
@ -258,32 +260,3 @@ function createI18nTranslationFunctions<MessageKey extends string>(params: {
}) as string
};
}
const keycloakifyExtraMessages = {
en: {
shouldBeEqual: "{0} should be equal to {1}",
shouldBeDifferent: "{0} should be different to {1}",
shouldMatchPattern: "Pattern should match: `/{0}/`",
mustBeAnInteger: "Must be an integer",
notAValidOption: "Not a valid option",
selectAnOption: "Select an option",
remove: "Remove",
addValue: "Add value"
},
fr: {
/* spell-checker: disable */
shouldBeEqual: "{0} doit être égal à {1}",
shouldBeDifferent: "{0} doit être différent de {1}",
shouldMatchPattern: "Dois respecter le schéma: `/{0}/`",
mustBeAnInteger: "Doit être un nombre entier",
notAValidOption: "N'est pas une option valide",
logoutConfirmTitle: "Déconnexion",
logoutConfirmHeader: "Êtes-vous sûr(e) de vouloir vous déconnecter ?",
doLogout: "Se déconnecter",
selectAnOption: "Sélectionner une option",
remove: "Supprimer",
addValue: "Ajouter une valeur"
/* spell-checker: enable */
}
};

View File

@ -1 +1,2 @@
export type { I18n } from "./i18n";
export type { I18n, MessageKey } from "./i18n";
export { createUseI18n, fallbackLanguageTag } from "./i18n";

View File

@ -1,11 +1,5 @@
import Fallback from "keycloakify/login/Fallback";
export default Fallback;
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { getKcContext } from "keycloakify/login/kcContext/getKcContext";
export { createGetKcContext } from "keycloakify/login/kcContext/createGetKcContext";
export type { LoginThemePageId as PageId } from "keycloakify/bin/shared/constants";
export { createUseI18n } from "keycloakify/login/i18n/i18n";
export type { ExtendKcContext, Attribute } from "keycloakify/login/KcContext";
export type { PageProps } from "keycloakify/login/pages/PageProps";
export { createGetKcContextMock } from "keycloakify/login/KcContext";
export { useDownloadTerms } from "keycloakify/login/lib/useDownloadTerms";
export { createUseI18n } from "keycloakify/login/i18n";

View File

@ -1,199 +0,0 @@
import type { KcContext, Attribute } from "./KcContext";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import { deepAssign } from "keycloakify/tools/deepAssign";
import { isStorybook } from "keycloakify/lib/isStorybook";
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
import { symToStr } from "tsafe/symToStr";
export function createGetKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
mockProperties?: Record<string, string>;
}) {
const { mockData, mockProperties } = params ?? {};
function getKcContext<
PageId extends
| ExtendKcContext<KcContextExtension>["pageId"]
| undefined = undefined
>(params?: {
mockPageId?: PageId;
storyPartialKcContext?: DeepPartial<
Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>
>;
}): {
kcContext: PageId extends undefined
? ExtendKcContext<KcContextExtension> | undefined
: Extract<ExtendKcContext<KcContextExtension>, { pageId: PageId }>;
} {
const { mockPageId, storyPartialKcContext } = params ?? {};
const realKcContext = getKcContextFromWindow<KcContextExtension>();
if (mockPageId !== undefined && realKcContext === undefined) {
//TODO maybe trow if no mock fo custom page
warn_that_mock_is_enabled: {
if (isStorybook) {
break warn_that_mock_is_enabled;
}
console.log(
`%cKeycloakify: ${symToStr({
mockPageId
})} set to ${mockPageId}.`,
"background: red; color: yellow; font-size: medium"
);
}
const kcContextDefaultMock = kcContextMocks.find(
({ pageId }) => pageId === mockPageId
);
const partialKcContextCustomMock = (() => {
const out: DeepPartial<ExtendKcContext<KcContextExtension>> = {};
const mockDataPick = mockData?.find(
({ pageId }) => pageId === mockPageId
);
if (mockDataPick !== undefined) {
deepAssign({
target: out,
source: mockDataPick
});
}
if (storyPartialKcContext !== undefined) {
deepAssign({
target: out,
source: storyPartialKcContext
});
}
return Object.keys(out).length === 0 ? undefined : out;
})();
if (
kcContextDefaultMock === undefined &&
partialKcContextCustomMock === undefined
) {
console.warn(
[
`WARNING: You declared the non build in page ${mockPageId} but you didn't `,
`provide mock data needed to debug the page outside of Keycloak as you are trying to do now.`,
`Please check the documentation of the getKcContext function`
].join("\n")
);
}
const kcContext: any = {};
deepAssign({
target: kcContext,
source:
kcContextDefaultMock !== undefined
? kcContextDefaultMock
: { pageId: mockPageId, ...kcContextCommonMock }
});
if (partialKcContextCustomMock !== undefined) {
deepAssign({
target: kcContext,
source: partialKcContextCustomMock
});
if ("profile" in partialKcContextCustomMock) {
assert(
kcContextDefaultMock !== undefined &&
"profile" in kcContextDefaultMock
);
const { attributes } = kcContextDefaultMock.profile;
id<KcContext.Register>(kcContext).profile.attributes = [];
const partialAttributes = [
...((
partialKcContextCustomMock as DeepPartial<KcContext.Register>
).profile?.attributes ?? [])
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(
({ name }) => name === attribute.name
);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
target: augmentedAttribute,
source: attribute
});
if (partialAttribute !== undefined) {
partialAttributes.splice(
partialAttributes.indexOf(partialAttribute),
1
);
deepAssign({
target: augmentedAttribute,
source: partialAttribute
});
}
id<KcContext.Register>(kcContext).profile.attributes.push(
augmentedAttribute
);
});
partialAttributes
.map(partialAttribute => ({
validators: {},
...partialAttribute
}))
.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(
name !== undefined,
"If you define a mock attribute it must have at least a name"
);
id<KcContext.Register>(kcContext).profile.attributes.push(
partialAttribute as any
);
});
}
}
if (mockProperties !== undefined) {
deepAssign({
target: kcContext.properties,
source: mockProperties
});
}
return { kcContext };
}
if (realKcContext === undefined) {
return { kcContext: undefined as any };
}
if (realKcContext.themeType !== "login") {
return { kcContext: undefined as any };
}
return { kcContext: realKcContext as any };
}
return { getKcContext };
}

View File

@ -1,23 +0,0 @@
import type { DeepPartial } from "keycloakify/tools/DeepPartial";
import type { ExtendKcContext } from "./getKcContextFromWindow";
import { createGetKcContext } from "./createGetKcContext";
/** NOTE: We now recommend using createGetKcContext instead of this function to make storybook integration easier
* See: https://github.com/keycloakify/keycloakify-starter/blob/main/src/keycloak-theme/account/kcContext.ts
*/
export function getKcContext<
KcContextExtension extends { pageId: string } = never
>(params?: {
mockPageId?: ExtendKcContext<KcContextExtension>["pageId"];
mockData?: readonly DeepPartial<ExtendKcContext<KcContextExtension>>[];
}): { kcContext: ExtendKcContext<KcContextExtension> | undefined } {
const { mockPageId, mockData } = params ?? {};
const { getKcContext } = createGetKcContext<KcContextExtension>({
mockData
});
const { kcContext } = getKcContext({ mockPageId });
return { kcContext };
}

View File

@ -1,15 +0,0 @@
import type { KcContext } from "./KcContext";
import type { AndByDiscriminatingKey } from "keycloakify/tools/AndByDiscriminatingKey";
import { nameOfTheGlobal } from "keycloakify/bin/shared/constants";
export type ExtendKcContext<KcContextExtension extends { pageId: string }> = [
KcContextExtension
] extends [never]
? KcContext
: AndByDiscriminatingKey<"pageId", KcContextExtension & KcContext.Common, KcContext>;
export function getKcContextFromWindow<
KcContextExtension extends { pageId: string } = never
>(): ExtendKcContext<KcContextExtension> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[nameOfTheGlobal];
}

View File

@ -1 +0,0 @@
export type { KcContext } from "./KcContext";

View File

@ -1,14 +1,11 @@
import { useEffect } from "react";
import { memoize } from "keycloakify/tools/memoize";
import { fallbackLanguageTag } from "keycloakify/login/i18n/i18n";
import { useConst } from "keycloakify/tools/useConst";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
import { fallbackLanguageTag } from "keycloakify/login/i18n";
import { assert } from "tsafe/assert";
import {
createStatefulObservable,
useRerenderOnChange
} from "keycloakify/tools/StatefulObservable";
import { KcContext } from "../kcContext";
import { useOnFistMount } from "keycloakify/tools/useOnFirstMount";
import { KcContext } from "../KcContext";
const obsTermsMarkdown = createStatefulObservable<string | undefined>(() => undefined);
@ -27,29 +24,18 @@ export function useDownloadTerms(params: {
kcContext: KcContextLike;
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
}) {
const { kcContext } = params;
const { kcContext, downloadTermMarkdown } = params;
const { downloadTermMarkdownMemoized } = (function useClosure() {
const { downloadTermMarkdown } = params;
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
const downloadTermMarkdownMemoized = useConst(() =>
memoize((currentLanguageTag: string) =>
downloadTermMarkdownConst({ currentLanguageTag })
)
);
return { downloadTermMarkdownMemoized };
})();
useEffect(() => {
useOnFistMount(async () => {
if (kcContext.pageId === "terms.ftl" || kcContext.termsAcceptanceRequired) {
downloadTermMarkdownMemoized(
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
).then(thermMarkdown => (obsTermsMarkdown.current = thermMarkdown));
const termsMarkdown = await downloadTermMarkdown({
currentLanguageTag:
kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag
});
obsTermsMarkdown.current = termsMarkdown;
}
}, []);
});
}
export function useTermsMarkdown() {

View File

@ -3,6 +3,7 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
export const { useGetClassName } = createUseClassName<ClassKey>({
defaultClasses: {
kcHtmlClass: "login-pf",
kcBodyClass: undefined,
kcHeaderWrapperClass: undefined,
kcLocaleWrapperClass: undefined,
@ -54,7 +55,6 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
kcLogoLink: "http://www.keycloak.org",
kcContainerClass: "container-fluid",
kcSelectAuthListItemTitle: "select-auth-box-paragraph",
kcHtmlClass: "login-pf",
kcLoginOTPListItemTitleClass: "pf-c-tile__title",
"kcLogoIdP-openshift-v4": "pf-icon pf-icon-openshift",
kcWebAuthnUnknownIcon: "pficon pficon-key unknown-transport-class",

View File

@ -1,14 +1,14 @@
import "keycloakify/tools/Array.prototype.every";
import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
import { assert, type Equals } from "tsafe/assert";
import { id } from "tsafe/id";
import type { MessageKey } from "keycloakify/login/i18n/i18n";
import type { Attribute, Validators } from "keycloakify/login/kcContext/KcContext";
import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
import { emailRegexp } from "keycloakify/tools/emailRegExp";
import type { KcContext, PasswordPolicies } from "keycloakify/login/kcContext/KcContext";
import { assert, type Equals } from "tsafe/assert";
import { formatNumber } from "keycloakify/tools/formatNumber";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { KcContext, PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
import type { MessageKey } from "keycloakify/login/i18n";
import type { I18n } from "../i18n";
export type FormFieldError = {
@ -67,7 +67,7 @@ export type FormAction =
export type KcContextLike = {
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
profile: {
attributes: Attribute[];
attributesByName: Record<string, Attribute>;
html5DataAnnotations?: Record<string, string>;
};
passwordRequired?: boolean;
@ -102,12 +102,11 @@ namespace internal {
};
}
const { useInsertScriptTags } = createUseInsertScriptTags();
export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
const { kcContext, i18n, doMakeUserConfirmPassword } = params;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "useUserProfileForm",
scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
.filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
.map(key => ({
@ -136,7 +135,11 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
const attributes = (() => {
retrocompat_patch: {
if ("profile" in kcContext && "attributes" in kcContext.profile && kcContext.profile.attributes.length !== 0) {
if (
"profile" in kcContext &&
"attributesByName" in kcContext.profile &&
Object.keys(kcContext.profile.attributesByName).length !== 0
) {
break retrocompat_patch;
}
@ -216,7 +219,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
assert(false, "Unable to mock user profile from the current kcContext");
}
return kcContext.profile.attributes.map(attribute_pre_group_patch => {
return Object.values(kcContext.profile.attributesByName).map(attribute_pre_group_patch => {
if (typeof attribute_pre_group_patch.group === "string" && attribute_pre_group_patch.group !== "") {
const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations, ...rest } =
attribute_pre_group_patch as Attribute & {
@ -242,7 +245,7 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
})();
for (const attribute of attributes) {
syntheticAttributes.push(attribute);
syntheticAttributes.push(structuredCloneButFunctions(attribute));
add_password_and_password_confirm: {
if (!kcContext.passwordRequired) {
@ -284,6 +287,21 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
}
}
// NOTE: Consistency patch
syntheticAttributes.forEach(attribute => {
if (getIsMultivaluedSingleField({ attribute })) {
attribute.multivalued = true;
}
if (attribute.multivalued) {
attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
delete attribute.value;
} else {
attribute.value ??= attribute.values?.[0];
delete attribute.values;
}
});
return syntheticAttributes;
})();
@ -299,10 +317,10 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
break handle_multi_valued_attribute;
}
const values = attribute.values ?? [""];
const values = attribute.values?.length ? attribute.values : [""];
apply_validator_min_range: {
if (attribute.annotations.inputType?.startsWith("multiselect")) {
if (getIsMultivaluedSingleField({ attribute })) {
break apply_validator_min_range;
}
@ -349,7 +367,8 @@ export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTy
attributeName: attribute.name,
formFieldStates: initialFormFieldState
}),
hasLostFocusAtLeastOnce: valueOrValues instanceof Array ? valueOrValues.map(() => false) : false,
hasLostFocusAtLeastOnce:
valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
valueOrValues: valueOrValues
}))
};
@ -543,7 +562,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
server_side_error: {
if (attribute.multivalued) {
const defaultValues = attribute.values ?? [""];
const defaultValues = attribute.values?.length ? attribute.values : [""];
assert(valueOrValues instanceof Array);
@ -595,7 +614,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
break handle_multi_valued_multi_fields;
}
if (attribute.annotations.inputType?.startsWith("multiselect")) {
if (getIsMultivaluedSingleField({ attribute })) {
break handle_multi_valued_multi_fields;
}
@ -674,7 +693,7 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
break handle_multi_valued_single_field;
}
if (!attribute.annotations.inputType?.startsWith("multiselect")) {
if (!getIsMultivaluedSingleField({ attribute })) {
break handle_multi_valued_single_field;
}
@ -913,6 +932,10 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
return valueOrValues;
})();
if (usernameValue === "") {
break check_password_policy_x;
}
if (value !== usernameValue) {
break check_password_policy_x;
}
@ -950,6 +973,10 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
{
const emailValue = emailFormFieldState.valueOrValues;
if (emailValue === "") {
break check_password_policy_x;
}
if (value !== emailValue) {
break check_password_policy_x;
}
@ -1239,3 +1266,79 @@ function useGetErrors(params: { kcContext: Pick<KcContextLike, "messagesPerField
return { getErrors };
}
function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
const { attribute } = params;
return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
}
export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
const { attribute, values, fieldIndex } = params;
const hasRemove = (() => {
if (values.length === 1) {
return false;
}
const minCount = (() => {
const { multivalued } = attribute.validators;
if (multivalued === undefined) {
return undefined;
}
const minStr = multivalued.min;
if (minStr === undefined) {
return undefined;
}
return parseInt(`${minStr}`);
})();
if (minCount === undefined) {
return true;
}
if (values.length === minCount) {
return false;
}
return true;
})();
const hasAdd = (() => {
if (fieldIndex + 1 !== values.length) {
return false;
}
const maxCount = (() => {
const { multivalued } = attribute.validators;
if (multivalued === undefined) {
return undefined;
}
const maxStr = multivalued.max;
if (maxStr === undefined) {
return undefined;
}
return parseInt(`${maxStr}`);
})();
if (maxCount === undefined) {
return false;
}
if (values.length === maxCount) {
return false;
}
return true;
})();
return { hasRemove, hasAdd };
}

View File

@ -1,6 +1,6 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Code(props: PageProps<Extract<KcContext, { pageId: "code.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function DeleteAccountConfirm(props: PageProps<Extract<KcContext, { pageId: "delete-account-confirm.ftl" }>, I18n>) {

View File

@ -1,8 +1,8 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function DeleteCredential(props: PageProps<Extract<KcContext, { pageId: "delete-credential.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;

View File

@ -1,5 +1,5 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Error(props: PageProps<Extract<KcContext, { pageId: "error.ftl" }>, I18n>) {

View File

@ -1,6 +1,6 @@
import { useEffect } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function FrontchannelLogout(props: PageProps<Extract<KcContext, { pageId: "frontchannel-logout.ftl" }>, I18n>) {

View File

@ -1,10 +1,10 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
type IdpReviewUserProfileProps = PageProps<Extract<KcContext, { pageId: "idp-review-user-profile.ftl" }>, I18n> & {

View File

@ -1,6 +1,6 @@
import { assert } from "keycloakify/tools/assert";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Info(props: PageProps<Extract<KcContext, { pageId: "info.ftl" }>, I18n>) {

View File

@ -3,7 +3,7 @@ import { assert } from "tsafe/assert";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Login(props: PageProps<Extract<KcContext, { pageId: "login.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pageId: "login-config-totp.ftl" }>, I18n>) {
@ -25,7 +25,7 @@ export default function LoginConfigTotp(props: PageProps<Extract<KcContext, { pa
<ul id="kc-totp-supported-apps">
{totp.supportedApplications.map(app => (
<li>{advancedMsg(app)}</li>
<li key={app}>{advancedMsg(app)}</li>
))}
</ul>
</li>

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginIdpLinkConfirm(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-confirm.ftl" }>, I18n>) {

View File

@ -1,6 +1,6 @@
import type { KcContext } from "keycloakify/login/kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { I18n } from "keycloakify/login/i18n";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginIdpLinkEmail(props: PageProps<Extract<KcContext, { pageId: "login-idp-link-email.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;

View File

@ -1,8 +1,8 @@
import { clsx } from "keycloakify/tools/clsx";
import { I18n } from "../i18n";
import { KcContext } from "../kcContext";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { PageProps } from "./PageProps";
import { PageProps } from "keycloakify/login/pages/PageProps";
import { I18n } from "../i18n";
import { KcContext } from "../KcContext";
export default function LoginOauth2DeviceVerifyUserCode(
props: PageProps<Extract<KcContext, { pageId: "login-oauth2-device-verify-user-code.ftl" }>, I18n>

View File

@ -1,8 +1,8 @@
import { clsx } from "keycloakify/tools/clsx";
import { PageProps } from "./PageProps";
import { KcContext } from "../kcContext";
import { I18n } from "../i18n";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { PageProps } from "keycloakify/login/pages/PageProps";
import { KcContext } from "../KcContext";
import { I18n } from "../i18n";
export default function LoginOauthGrant(props: PageProps<Extract<KcContext, { pageId: "login-oauth-grant.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, classes, Template } = props;

View File

@ -1,8 +1,8 @@
import { Fragment } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginOtp(props: PageProps<Extract<KcContext, { pageId: "login-otp.ftl" }>, I18n>) {

View File

@ -1,5 +1,5 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginPageExpired(props: PageProps<Extract<KcContext, { pageId: "login-page-expired.ftl" }>, I18n>) {

View File

@ -1,9 +1,9 @@
import { useState, useEffect, useReducer } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { assert } from "tsafe/assert";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginPassword(props: PageProps<Extract<KcContext, { pageId: "login-password.ftl" }>, I18n>) {

View File

@ -1,13 +1,11 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { KcContext } from "../kcContext";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
const { useInsertScriptTags } = createUseInsertScriptTags();
export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-config.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -21,6 +19,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "LoginRecoveryAuthnCodeConfig",
scriptTags: [
{
type: "text/javascript",
@ -31,7 +30,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
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";
tmpTextarea.value = tmpTextarea.value + codes[i].innerText + "\\n";
}
document.body.appendChild(tmpTextarea);
tmpTextarea.select();
@ -65,7 +64,7 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
for (var i = 0; i < recoveryCodes.length; i++) {
var recoveryCodeLiElement = recoveryCodes[i].innerText;
recoveryCodeList += recoveryCodeLiElement + "\r\n";
recoveryCodeList += recoveryCodeLiElement + "\\r\\n";
}
return recoveryCodeList;
@ -84,9 +83,9 @@ export default function LoginRecoveryAuthnCodeConfig(props: PageProps<Extract<Kc
};
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-header")}\\n\\n" +
recoveryCodeList + "\\n" +
"${msgStr("recovery-codes-download-file-description")}\\n\\n" +
"${msgStr("recovery-codes-download-file-date")} " + formatCurrentDateTime();
}

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginRecoveryAuthnCodeInput(props: PageProps<Extract<KcContext, { pageId: "login-recovery-authn-code-input.ftl" }>, I18n>) {

View File

@ -1,8 +1,8 @@
import { Fragment } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginResetOtp(props: PageProps<Extract<KcContext, { pageId: "login-reset-otp.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginResetPassword(props: PageProps<Extract<KcContext, { pageId: "login-reset-password.ftl" }>, I18n>) {

View File

@ -1,9 +1,9 @@
import { useEffect, useReducer } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { assert } from "tsafe/assert";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginUpdatePassword(props: PageProps<Extract<KcContext, { pageId: "login-update-password.ftl" }>, I18n>) {

View File

@ -1,11 +1,11 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
type LoginUpdateProfileProps = PageProps<Extract<KcContext, { pageId: "login-update-profile.ftl" }>, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;

View File

@ -1,8 +1,8 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginUsername(props: PageProps<Extract<KcContext, { pageId: "login-username.ftl" }>, I18n>) {

View File

@ -1,5 +1,5 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginVerifyEmail(props: PageProps<Extract<KcContext, { pageId: "login-verify-email.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LoginX509Info(props: PageProps<Extract<KcContext, { pageId: "login-x509-info.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "logout-confirm.ftl" }>, I18n>) {

View File

@ -1,7 +1,7 @@
import type { I18n } from "keycloakify/login/i18n";
import { type TemplateProps, type ClassKey } from "keycloakify/login/TemplateProps";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { KcContext } from "keycloakify/account/kcContext";
import type { KcContext } from "keycloakify/account/KcContext";
export type PageProps<NarowedKcContext = KcContext, I18nExtended extends I18n = I18n> = {
Template: LazyOrNot<(props: TemplateProps<any, any>) => JSX.Element | null>;

View File

@ -1,12 +1,12 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { Markdown } from "keycloakify/tools/Markdown";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import { Markdown } from "keycloakify/tools/Markdown";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
type RegisterProps = PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n> & {

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function SamlPostForm(props: PageProps<Extract<KcContext, { pageId: "saml-post-form.ftl" }>, I18n>) {

View File

@ -1,8 +1,8 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "keycloakify/login/kcContext";
import type { I18n } from "keycloakify/login/i18n";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function SelectAuthenticator(props: PageProps<Extract<KcContext, { pageId: "select-authenticator.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;

View File

@ -1,9 +1,9 @@
import { clsx } from "keycloakify/tools/clsx";
import { Markdown } from "keycloakify/tools/Markdown";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { useTermsMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl" }>, I18n>) {

View File

@ -1,10 +1,10 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
type UpdateEmailProps = PageProps<Extract<KcContext, { pageId: "update-email.ftl" }>, I18n> & {

View File

@ -1,14 +1,12 @@
import { useEffect, Fragment } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { assert } from "tsafe/assert";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { KcContext } from "../kcContext";
import { clsx } from "keycloakify/tools/clsx";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
const { useInsertScriptTags } = createUseInsertScriptTags();
export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext, { pageId: "webauthn-authenticate.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -31,6 +29,7 @@ export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext,
const { msg, msgStr, advancedMsg } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "WebauthnAuthenticate",
scriptTags: [
{
type: "text/javascript",

View File

@ -1,7 +1,7 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { clsx } from "keycloakify/tools/clsx";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
export default function WebauthnError(props: PageProps<Extract<KcContext, { pageId: "webauthn-error.ftl" }>, I18n>) {

View File

@ -1,14 +1,12 @@
import { useEffect } from "react";
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { assert } from "tsafe/assert";
import { createUseInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { KcContext } from "../kcContext";
import { clsx } from "keycloakify/tools/clsx";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
const { useInsertScriptTags } = createUseInsertScriptTags();
export default function WebauthnRegister(props: PageProps<Extract<KcContext, { pageId: "webauthn-register.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@ -35,6 +33,7 @@ export default function WebauthnRegister(props: PageProps<Extract<KcContext, { p
const { msg, msgStr } = i18n;
const { insertScriptTags } = useInsertScriptTags({
componentOrHookName: "WebauthnRegister",
scriptTags: [
{
type: "text/javascript",

View File

@ -1,31 +0,0 @@
export type AndByDiscriminatingKey<
DiscriminatingKey extends string,
U1 extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> = AndByDiscriminatingKey.Tf1<DiscriminatingKey, U1, U1, U2>;
export declare namespace AndByDiscriminatingKey {
export type Tf1<
DiscriminatingKey extends string,
U1,
U1Again extends Record<DiscriminatingKey, string>,
U2 extends Record<DiscriminatingKey, string>
> =
U1 extends Pick<U2, DiscriminatingKey>
? Tf2<DiscriminatingKey, U1, U2, U1Again>
: U1Again[DiscriminatingKey] & U2[DiscriminatingKey] extends never
? U1 | U2
: U1;
export type Tf2<
DiscriminatingKey extends string,
SingletonU1 extends Record<DiscriminatingKey, string>,
U2,
U1 extends Record<DiscriminatingKey, string>
> =
U2 extends Pick<SingletonU1, DiscriminatingKey>
? U2 & SingletonU1
: U2 extends Pick<U1, DiscriminatingKey>
? never
: U2;
}

View File

@ -0,0 +1,4 @@
export type ExtractAfterStartingWith<
Prefix extends string,
StrEnum
> = StrEnum extends `${Prefix}${infer U}` ? U : never;

View File

@ -0,0 +1,23 @@
if (!(Object as any).fromEntries) {
Object.defineProperty(Object, "fromEntries", {
value: function (entries: any) {
if (!entries || !entries[Symbol.iterator]) {
throw new Error(
"Object.fromEntries() requires a single iterable argument"
);
}
const o: any = {};
Object.keys(entries).forEach(key => {
const [k, v] = entries[key];
o[k] = v;
});
return o;
}
});
}
export {};

View File

@ -5,15 +5,15 @@ import type { StatefulObservable } from "../StatefulObservable";
/**
* Equivalent of https://docs.evt.land/api/react-hooks
* */
export function useRerenderOnChange($: StatefulObservable<unknown>): void {
export function useRerenderOnChange(obs: StatefulObservable<unknown>): void {
//NOTE: We use function in case the state is a function
const [, setCurrent] = useState(() => $.current);
const [, setCurrent] = useState(() => obs.current);
useObservable(
({ registerSubscription }) => {
const subscription = $.subscribe(current => setCurrent(() => current));
const subscription = obs.subscribe(current => setCurrent(() => current));
registerSubscription(subscription);
},
[$]
[obs]
);
}

Some files were not shown because too many files have changed in this diff Show More