aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorGravatar garronej <[email protected]>2023-02-26 12:32:22 +0100
committerGravatar garronej <[email protected]>2023-02-26 12:32:22 +0100
commit3d0736e72b8af581819c3255af6fb36b0198a40a (patch)
tree4731116a3932f08580cb5d6722be678880014854 /src
parentFix build (diff)
Commit for saving
Diffstat (limited to 'src')
-rw-r--r--src/KcApp/KcApp.tsx45
-rw-r--r--src/KcApp/MyExtraPage1.tsx14
-rw-r--r--src/KcApp/MyExtraPage2.tsx16
-rw-r--r--src/KcApp/Terms.tsx94
-rw-r--r--src/KcApp/index.ts3
-rw-r--r--src/KcApp/kcContext.ts79
-rw-r--r--src/index.tsx8
-rw-r--r--src/keycloakTheme/KcApp.css (renamed from src/KcApp/KcApp.css)0
-rw-r--r--src/keycloakTheme/KcApp.tsx80
-rw-r--r--src/keycloakTheme/Template.tsx237
-rw-r--r--src/keycloakTheme/assets/tos_en.md (renamed from src/KcApp/tos_en.md)0
-rw-r--r--src/keycloakTheme/assets/tos_fr.md (renamed from src/KcApp/tos_fr.md)0
-rw-r--r--src/keycloakTheme/i18n.ts (renamed from src/KcApp/i18n.ts)0
-rw-r--r--src/keycloakTheme/kcContext.ts88
-rw-r--r--src/keycloakTheme/pages/MyExtraPage1.tsx22
-rw-r--r--src/keycloakTheme/pages/MyExtraPage2.tsx25
-rw-r--r--src/keycloakTheme/pages/Register.tsx (renamed from src/KcApp/Register.tsx)106
-rw-r--r--src/keycloakTheme/pages/Terms.tsx82
18 files changed, 590 insertions, 309 deletions
diff --git a/src/KcApp/KcApp.tsx b/src/KcApp/KcApp.tsx
deleted file mode 100644
index 0cd7747..0000000
--- a/src/KcApp/KcApp.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import "./KcApp.css";
-import { lazy, Suspense } from "react";
-import type { KcContext } from "./kcContext";
-import KcAppBase, { defaultKcProps } from "keycloakify";
-import { useI18n } from "./i18n";
-
-const Register = lazy(() => import("./Register"));
-const Terms = lazy(() => import("./Terms"));
-const MyExtraPage1 = lazy(() => import("./MyExtraPage1"));
-const MyExtraPage2 = lazy(() => import("./MyExtraPage2"));
-
-export type Props = {
- kcContext: KcContext;
-};
-
-export default function KcApp({ kcContext }: Props) {
- const i18n = useI18n({ kcContext });
-
- //NOTE: Locales not yet downloaded
- if (i18n === null) {
- return null;
- }
-
- const props = {
- i18n,
- ...defaultKcProps,
- // NOTE: The classes are defined in ./KcApp.css
- "kcHeaderWrapperClass": "my-color my-font",
- };
-
- return (
- <Suspense>
- {(() => {
- switch (kcContext.pageId) {
- case "register.ftl": return <Register {...{ kcContext, ...props }} />;
- case "terms.ftl": return <Terms {...{ kcContext, ...props }} />;
- case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...props }} />;
- case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...props }} />;
- default: return <KcAppBase {...{ kcContext, ...props }} />;
- }
- })()}
- </Suspense>
- );
-
-}
diff --git a/src/KcApp/MyExtraPage1.tsx b/src/KcApp/MyExtraPage1.tsx
deleted file mode 100644
index 1650449..0000000
--- a/src/KcApp/MyExtraPage1.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { memo } from "react";
-import type { KcProps } from "keycloakify";
-import type { KcContext } from "./kcContext";
-import type { I18n } from "./i18n";
-
-type KcContext_MyExtraPage1 = Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>;
-
-const MyExtraPage1 = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_MyExtraPage1; i18n: I18n; } & KcProps) => {
-
- return <>It is up to you to implement this page</>
-
-});
-
-export default MyExtraPage1; \ No newline at end of file
diff --git a/src/KcApp/MyExtraPage2.tsx b/src/KcApp/MyExtraPage2.tsx
deleted file mode 100644
index ecdd333..0000000
--- a/src/KcApp/MyExtraPage2.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { memo } from "react";
-import type { KcProps } from "keycloakify";
-import type { KcContext } from "./kcContext";
-import type { I18n } from "./i18n";
-
-type KcContext_MyExtraPage2 = Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>;
-
-const MyExtraPage2 = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_MyExtraPage2; i18n: I18n; } & KcProps) => {
-
- console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
-
- return <>It is up to you to implement this page</>
-
-});
-
-export default MyExtraPage2; \ No newline at end of file
diff --git a/src/KcApp/Terms.tsx b/src/KcApp/Terms.tsx
deleted file mode 100644
index 59ed62c..0000000
--- a/src/KcApp/Terms.tsx
+++ /dev/null
@@ -1,94 +0,0 @@
-import { memo } from "react";
-import Template from "keycloakify/lib/components/Template";
-import type { KcProps } from "keycloakify";
-import { useDownloadTerms } from "keycloakify";
-import type { KcContext } from "./kcContext";
-import type { I18n } from "./i18n";
-import { evtTermMarkdown } from "keycloakify/lib/components/Terms";
-import { useRerenderOnStateChange } from "evt/hooks";
-import tos_en_url from "./tos_en.md";
-import tos_fr_url from "./tos_fr.md";
-import { clsx } from "keycloakify/lib/tools/clsx";
-
-/**
- * NOTE: Yo do not need to do all this to put your own Terms and conditions
- * this is if you want component level customization.
- * If the default works for you you can just use the useDownloadTerms hook
- * in the KcApp.tsx
- * Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
- */
-
-type KcContext_Terms = Extract<KcContext, { pageId: "terms.ftl" }>;
-
-const Terms = memo(
- ({
- kcContext,
- i18n,
- ...props
- }: { kcContext: KcContext_Terms; i18n: I18n } & KcProps) => {
- const { url } = kcContext;
-
- useDownloadTerms({
- kcContext,
- "downloadTermMarkdown": async ({ currentLanguageTag }) => {
-
- const markdownString = await fetch((() => {
- switch (currentLanguageTag) {
- case "fr": return tos_fr_url;
- default: return tos_en_url;
- }
- })()).then(response => response.text());
-
- return markdownString;
- },
- });
-
- useRerenderOnStateChange(evtTermMarkdown);
-
- if (evtTermMarkdown.state === undefined) {
- return null;
- }
-
- const { msg, msgStr } = i18n;
-
- return (
- <Template
- {...{ kcContext, i18n, ...props }}
- doFetchDefaultThemeResources={true}
- displayMessage={false}
- headerNode={msg("termsTitle")}
- formNode={
- <>
- <div id="kc-terms-text">{evtTermMarkdown.state}</div>
- <form className="form-actions" action={url.loginAction} method="POST">
- <input
- className={clsx(
- props.kcButtonClass,
- props.kcButtonClass,
- props.kcButtonClass,
- props.kcButtonPrimaryClass,
- props.kcButtonLargeClass
- )}
- name="accept"
- id="kc-accept"
- type="submit"
- value={msgStr("doAccept")}
- />
- <input
- className={clsx(props.kcButtonClass, props.kcButtonDefaultClass, props.kcButtonLargeClass)}
- name="cancel"
- id="kc-decline"
- type="submit"
- value={msgStr("doDecline")}
- />
- </form>
- <div className="clearfix" />
- </>
- }
- />
- );
-
- },
-);
-
-export default Terms;
diff --git a/src/KcApp/index.ts b/src/KcApp/index.ts
deleted file mode 100644
index 974ad0c..0000000
--- a/src/KcApp/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import KcApp from "./KcApp";
-export * from "./KcApp";
-export default KcApp;
diff --git a/src/KcApp/kcContext.ts b/src/KcApp/kcContext.ts
deleted file mode 100644
index a5adc68..0000000
--- a/src/KcApp/kcContext.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import { getKcContext } from "keycloakify/lib/getKcContext";
-
-export const { kcContext } = getKcContext<
- // NOTE: A 'keycloakify' field must be added
- // in the package.json to generate theses pages
- // https://docs.keycloakify.dev/build-options#keycloakify.extrapages
- | { pageId: "my-extra-page-1.ftl"; }
- | { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
- // NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
- // but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
- // keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
- | { pageId: "register.ftl"; authorizedMailDomains: string[]; }
->({
- // Uncomment to test the login page for development.
- //"mockPageId": "login.ftl",
- "mockData": [
- {
- "pageId": "login.ftl",
- "locale": {
- //When we test the login page we do it in french
- "currentLanguageTag": "fr",
- },
- },
- {
- "pageId": "my-extra-page-2.ftl",
- "someCustomValue": "foo bar baz"
- },
- {
- "pageId": "register.ftl",
- "authorizedMailDomains": [
- "example.com",
- "another-example.com",
- "*.yet-another-example.com",
- "*.example.com",
- "hello-world.com"
- ]
- },
- {
- //NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
- "pageId": "register-user-profile.ftl",
- "locale": {
- "currentLanguageTag": "fr"
- },
- "profile": {
- "attributes": [
- {
- "validators": {
- "pattern": {
- "pattern": "^[a-zA-Z0-9]+$",
- "ignore.empty.value": true,
- // eslint-disable-next-line no-template-curly-in-string
- "error-message": "${alphanumericalCharsOnly}",
- },
- },
- //NOTE: To override the default mock value
- "value": undefined,
- "name": "username"
- },
- {
- "validators": {
- "options": {
- "options": ["male", "female", "non-binary", "transgender", "intersex", "non_communicated"]
- }
- },
- // eslint-disable-next-line no-template-curly-in-string
- "displayName": "${gender}",
- "annotations": {},
- "required": true,
- "groupAnnotations": {},
- "readOnly": false,
- "name": "gender"
- }
- ]
- }
- }
- ]
-});
-
-export type KcContext = NonNullable<typeof kcContext>; \ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index 8438b67..d671dba 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,13 +1,9 @@
import { createRoot } from "react-dom/client";
import { StrictMode, lazy, Suspense } from "react";
-import { kcContext } from "./KcApp/kcContext";
+import { kcContext } from "./keycloakTheme/kcContext";
const App = lazy(() => import("./App"));
-const KcApp = lazy(() => import("./KcApp"));
-
-if (kcContext !== undefined) {
- console.log(kcContext);
-}
+const KcApp = lazy(() => import("./keycloakTheme/KcApp"));
createRoot(document.getElementById("root")!).render(
<StrictMode>
diff --git a/src/KcApp/KcApp.css b/src/keycloakTheme/KcApp.css
index a8fdf42..a8fdf42 100644
--- a/src/KcApp/KcApp.css
+++ b/src/keycloakTheme/KcApp.css
diff --git a/src/keycloakTheme/KcApp.tsx b/src/keycloakTheme/KcApp.tsx
new file mode 100644
index 0000000..aa804ce
--- /dev/null
+++ b/src/keycloakTheme/KcApp.tsx
@@ -0,0 +1,80 @@
+import "./KcApp.css";
+import { lazy, Suspense } from "react";
+import type { KcContext } from "./kcContext";
+import { useI18n, type I18n } from "./i18n";
+import Fallback, { defaultKcProps, type PageProps } from "keycloakify";
+import Template from "./Template";
+import { KcContextBase } from "keycloakify/lib/getKcContext";
+import type { I18nBase } from "keycloakify/lib/i18n";
+import type { TemplateProps } from "keycloakify";
+
+const Login = lazy(()=> import("keycloakify/lib/pages/Login"));
+const Register = lazy(() => import("./pages/Register"));
+const Terms = lazy(() => import("./pages/Terms"));
+const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
+const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
+
+type Props = {
+ kcContext: KcContext;
+};
+
+export default function App({ kcContext }: Props) {
+ const i18n = useI18n({ kcContext });
+
+ //NOTE: Locales not yet downloaded
+ if (i18n === null) {
+ return null;
+ }
+
+ const props = {
+ i18n,
+ Template,
+ ...defaultKcProps,
+ // NOTE: The classes are defined in ./KcApp.css
+ "kcHeaderWrapperClass": "my-color my-font"
+ } satisfies Omit<PageProps<any, I18n>, "kcContext">;
+
+
+
+ return (
+ <Suspense>
+ {(() => {
+ switch (kcContext.pageId) {
+ case "login.ftl": return <Login {...{kcContext, ...props }} />;
+ case "register.ftl": return <Register {...{ kcContext, ...props }} />;
+ case "terms.ftl": return <Terms {...{ kcContext, ...props }} />;
+ case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...props }} />;
+ case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...props }} />;
+ default:
+
+ //console.log(xxxx);
+
+ //const x: KcContextBase = kcContext;
+ //console.log(Template2, x);
+
+ //const y: I18nBase = i18n;
+
+ //const zz: TemplateProps<KcContextBase, I18nBase> = null as any as TemplateProps<KcContext, I18n>;
+ //const z: TemplateProps<KcContextBase, I18nBase> = null as any as TemplateProps<typeof kcContext, I18n>;
+ type XX = typeof kcContext;
+ const Template2: (props: TemplateProps<KcContextBase, I18nBase>) => JSX.Element | null= null as any as (( props: TemplateProps<XX, I18n>)=> JSX.Element | null);
+
+
+ //const Template3= (props: TemplateProps<typeof kcContext, I18n>)=> <Template {...props}/>;
+
+ /*
+ const xxxx: PageProps<KcContextBase, I18nBase> = {
+ "kcContext": kcContext,
+ ...defaultKcProps,
+ "Template": Template3,
+ "i18n": i18n
+ };
+ */
+
+ return <Fallback {...{ kcContext, ...props }} Template={Template3} />;
+ }
+ })()}
+ </Suspense>
+ );
+
+}
diff --git a/src/keycloakTheme/Template.tsx b/src/keycloakTheme/Template.tsx
new file mode 100644
index 0000000..59644f8
--- /dev/null
+++ b/src/keycloakTheme/Template.tsx
@@ -0,0 +1,237 @@
+// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/shared/Template.tsx
+import { useReducer, useEffect } from "react";
+// You can replace all relative imports by cherry picking files from the keycloakify module.
+// For example, the following import:
+// import { headInsert } from "./tools/headInsert";
+// becomes:
+import { headInsert } from "keycloakify/lib/tools/headInsert";
+import { assert } from "keycloakify/lib/tools/assert";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import { pathJoin } from "keycloakify/bin/tools/pathJoin";
+import type { TemplateProps } from "keycloakify/lib/KcProps";
+//import type { KcContextBase } from "keycloakify/lib/getKcContext";
+import type { KcContext } from "./kcContext";
+// Here Instead of KcContextBase.Common you should provide your own context
+import type { I18n } from "./i18n";
+
+export default function Template(props: TemplateProps<KcContext, I18n>) {
+ const {
+ displayInfo = false,
+ displayMessage = true,
+ displayRequiredFields = false,
+ displayWide = false,
+ showAnotherWayIfPresent = true,
+ headerNode,
+ showUsernameNode = null,
+ formNode,
+ infoNode = null,
+ kcContext,
+ i18n,
+ doFetchDefaultThemeResources
+ } = props;
+
+ const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
+
+ const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
+
+ const [isExtraCssLoaded, setExtraCssLoaded] = useReducer(() => true, false);
+
+ useEffect(() => {
+ if (!doFetchDefaultThemeResources) {
+ setExtraCssLoaded();
+ return;
+ }
+
+ let isUnmounted = false;
+ const cleanups: (() => void)[] = [];
+
+ const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
+
+ Promise.all(
+ [
+ ...toArr(props.stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
+ ...toArr(props.styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
+ ]
+ .reverse()
+ .map(href =>
+ headInsert({
+ "type": "css",
+ href,
+ "position": "prepend"
+ })
+ )
+ ).then(() => {
+ if (isUnmounted) {
+ return;
+ }
+
+ setExtraCssLoaded();
+ });
+
+ toArr(props.scripts).forEach(relativePath =>
+ headInsert({
+ "type": "javascript",
+ "src": pathJoin(url.resourcesPath, relativePath)
+ })
+ );
+
+ if (props.kcHtmlClass !== undefined) {
+ const htmlClassList = document.getElementsByTagName("html")[0].classList;
+
+ const tokens = clsx(props.kcHtmlClass).split(" ");
+
+ htmlClassList.add(...tokens);
+
+ cleanups.push(() => htmlClassList.remove(...tokens));
+ }
+
+ return () => {
+ isUnmounted = true;
+
+ cleanups.forEach(f => f());
+ };
+ }, [props.kcHtmlClass]);
+
+ if (!isExtraCssLoaded) {
+ return null;
+ }
+
+ return (
+ <div className={clsx(props.kcLoginClass)}>
+ <div id="kc-header" className={clsx(props.kcHeaderClass)}>
+ <div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}>
+ {msg("loginTitleHtml", realm.displayNameHtml)}
+ </div>
+ </div>
+
+ <div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}>
+ <header className={clsx(props.kcFormHeaderClass)}>
+ {realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
+ <div id="kc-locale">
+ <div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}>
+ <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>
+ <ul>
+ {locale.supported.map(({ languageTag }) => (
+ <li key={languageTag} className="kc-dropdown-item">
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ <a href="#" onClick={()=> changeLocale(languageTag)}>
+ {labelBySupportedLanguageTag[languageTag]}
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ </div>
+ </div>
+ )}
+ {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
+ displayRequiredFields ? (
+ <div className={clsx(props.kcContentWrapperClass)}>
+ <div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
+ <span className="subtitle">
+ <span className="required">*</span>
+ {msg("requiredFields")}
+ </span>
+ </div>
+ <div className="col-md-10">
+ <h1 id="kc-page-title">{headerNode}</h1>
+ </div>
+ </div>
+ ) : (
+ <h1 id="kc-page-title">{headerNode}</h1>
+ )
+ ) : displayRequiredFields ? (
+ <div className={clsx(props.kcContentWrapperClass)}>
+ <div className={clsx(props.kcLabelWrapperClass, "subtitle")}>
+ <span className="subtitle">
+ <span className="required">*</span> {msg("requiredFields")}
+ </span>
+ </div>
+ <div className="col-md-10">
+ {showUsernameNode}
+ <div className={clsx(props.kcFormGroupClass)}>
+ <div id="kc-username">
+ <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
+ <a id="reset-login" href={url.loginRestartFlowUrl}>
+ <div className="kc-login-tooltip">
+ <i className={clsx(props.kcResetFlowIcon)}></i>
+ <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
+ </div>
+ </a>
+ </div>
+ </div>
+ </div>
+ </div>
+ ) : (
+ <>
+ {showUsernameNode}
+ <div className={clsx(props.kcFormGroupClass)}>
+ <div id="kc-username">
+ <label id="kc-attempted-username">{auth?.attemptedUsername}</label>
+ <a id="reset-login" href={url.loginRestartFlowUrl}>
+ <div className="kc-login-tooltip">
+ <i className={clsx(props.kcResetFlowIcon)}></i>
+ <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
+ </div>
+ </a>
+ </div>
+ </div>
+ </>
+ )}
+ </header>
+ <div id="kc-content">
+ <div id="kc-content-wrapper">
+ {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
+ {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
+ <div className={clsx("alert", `alert-${message.type}`)}>
+ {message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>}
+ {message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>}
+ {message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>}
+ {message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>}
+ <span
+ className="kc-feedback-text"
+ dangerouslySetInnerHTML={{
+ "__html": message.summary
+ }}
+ />
+ </div>
+ )}
+ {formNode}
+ {auth !== undefined && auth.showTryAnotherWayLink && showAnotherWayIfPresent && (
+ <form
+ id="kc-select-try-another-way-form"
+ action={url.loginAction}
+ method="post"
+ className={clsx(displayWide && props.kcContentWrapperClass)}
+ >
+ <div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}>
+ <div className={clsx(props.kcFormGroupClass)}>
+ <input type="hidden" name="tryAnotherWay" value="on" />
+ {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
+ <a href="#" id="try-another-way" onClick={()=>{
+ document.forms["kc-select-try-another-way-form" as never].submit();
+ return false;
+ }}>
+ {msg("doTryAnotherWay")}
+ </a>
+ </div>
+ </div>
+ </form>
+ )}
+ {displayInfo && (
+ <div id="kc-info" className={clsx(props.kcSignUpClass)}>
+ <div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}>
+ {infoNode}
+ </div>
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/src/KcApp/tos_en.md b/src/keycloakTheme/assets/tos_en.md
index 9436328..9436328 100644
--- a/src/KcApp/tos_en.md
+++ b/src/keycloakTheme/assets/tos_en.md
diff --git a/src/KcApp/tos_fr.md b/src/keycloakTheme/assets/tos_fr.md
index 0621cd3..0621cd3 100644
--- a/src/KcApp/tos_fr.md
+++ b/src/keycloakTheme/assets/tos_fr.md
diff --git a/src/KcApp/i18n.ts b/src/keycloakTheme/i18n.ts
index e4d55ea..e4d55ea 100644
--- a/src/KcApp/i18n.ts
+++ b/src/keycloakTheme/i18n.ts
diff --git a/src/keycloakTheme/kcContext.ts b/src/keycloakTheme/kcContext.ts
new file mode 100644
index 0000000..47e956b
--- /dev/null
+++ b/src/keycloakTheme/kcContext.ts
@@ -0,0 +1,88 @@
+import { getKcContext } from "keycloakify/lib/kcContext";
+
+//NOTE: In most of the cases you do not need to overload the KcContext, you can
+// just call getKcContext(...) without type arguments.
+// You want to overload the KcContext only if:
+// - You have custom plugins that add some values to the context (like https://github.com/micedre/keycloak-mail-whitelisting that adds authorizedMailDomains)
+// - You want to add support for extra pages that are not yey featured by default, see: https://docs.keycloakify.dev/contributing#adding-support-for-a-new-page
+export const { kcContext } = getKcContext<
+ // NOTE: A 'keycloakify' field must be added
+ // in the package.json to generate theses extra pages
+ // https://docs.keycloakify.dev/build-options#keycloakify.extrapages
+ | { pageId: "my-extra-page-1.ftl"; }
+ | { pageId: "my-extra-page-2.ftl"; someCustomValue: string; }
+ // NOTE: register.ftl is deprecated in favor of register-user-profile.ftl
+ // but let's say we use it anyway and have this plugin enabled: https://github.com/micedre/keycloak-mail-whitelisting
+ // keycloak-mail-whitelisting define the non standard ftl global authorizedMailDomains, we declare it here.
+ | { pageId: "register.ftl"; authorizedMailDomains: string[]; }
+>({
+ // Uncomment to test the login page for development.
+ //"mockPageId": "login.ftl",
+ mockData: [
+ {
+ pageId: "login.ftl",
+ locale: {
+ //When we test the login page we do it in french
+ currentLanguageTag: "fr",
+ },
+ //Uncomment the following line for hiding the Alert message
+ //"message": undefined
+ //Uncomment the following line for showing an Error message
+ //message: { type: "error", summary: "This is an error" }
+ },
+ {
+ pageId: "my-extra-page-2.ftl",
+ someCustomValue: "foo bar baz"
+ },
+ {
+ pageId: "register.ftl",
+ authorizedMailDomains: [
+ "example.com",
+ "another-example.com",
+ "*.yet-another-example.com",
+ "*.example.com",
+ "hello-world.com"
+ ]
+ },
+ {
+ //NOTE: You will either use register.ftl (legacy) or register-user-profile.ftl, not both
+ pageId: "register-user-profile.ftl",
+ locale: {
+ currentLanguageTag: "fr"
+ },
+ profile: {
+ attributes: [
+ {
+ validators: {
+ pattern: {
+ pattern: "^[a-zA-Z0-9]+$",
+ "ignore.empty.value": true,
+ // eslint-disable-next-line no-template-curly-in-string
+ "error-message": "${alphanumericalCharsOnly}",
+ },
+ },
+ //NOTE: To override the default mock value
+ value: undefined,
+ name: "username"
+ },
+ {
+ validators: {
+ options: {
+ options: ["male", "female", "non-binary", "transgender", "intersex", "non_communicated"]
+ }
+ },
+ // eslint-disable-next-line no-template-curly-in-string
+ displayName: "${gender}",
+ annotations: {},
+ required: true,
+ groupAnnotations: {},
+ readOnly: false,
+ name: "gender"
+ }
+ ]
+ }
+ }
+ ]
+});
+
+export type KcContext = NonNullable<typeof kcContext>; \ No newline at end of file
diff --git a/src/keycloakTheme/pages/MyExtraPage1.tsx b/src/keycloakTheme/pages/MyExtraPage1.tsx
new file mode 100644
index 0000000..82d54d1
--- /dev/null
+++ b/src/keycloakTheme/pages/MyExtraPage1.tsx
@@ -0,0 +1,22 @@
+import type { PageProps } from "keycloakify";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-1.ftl"; }>, I18n>) {
+
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ headerNode={<>Header <i>text</i></>}
+ formNode={
+ <form>
+ {/*...*/}
+ </form>
+ }
+ infoNode={<span>footer</span> }
+ />
+ );
+
+}
diff --git a/src/keycloakTheme/pages/MyExtraPage2.tsx b/src/keycloakTheme/pages/MyExtraPage2.tsx
new file mode 100644
index 0000000..a0d00f8
--- /dev/null
+++ b/src/keycloakTheme/pages/MyExtraPage2.tsx
@@ -0,0 +1,25 @@
+import type { PageProps } from "keycloakify";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function MyExtraPage1(props: PageProps<Extract<KcContext, { pageId: "my-extra-page-2.ftl"; }>, I18n>) {
+
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ // someCustomValue is declared by you in ../kcContext.ts
+ console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ headerNode={<>Header <i>text</i></>}
+ formNode={
+ <form>
+ {/*...*/}
+ </form>
+ }
+ infoNode={<span>footer</span> }
+ />
+ );
+
+}
diff --git a/src/KcApp/Register.tsx b/src/keycloakTheme/pages/Register.tsx
index 2b0ad8c..ea6639d 100644
--- a/src/KcApp/Register.tsx
+++ b/src/keycloakTheme/pages/Register.tsx
@@ -1,77 +1,73 @@
-// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/components/Register.tsx
+// This is a copy paste from https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/Register.tsx
// It is now up to us to implement a special behavior to leverage the non standard authorizedMailDomains
// provided by the plugin: https://github.com/micedre/keycloak-mail-whitelisting installed on our keycloak server.
// Note that it is no longer recommended to use register.ftl, it's best to use register-user-profile.ftl
// See: https://docs.keycloakify.dev/realtime-input-validation
-
-import { memo } from "react";
-import Template from "keycloakify/lib/components/Template";
-import type { KcProps } from "keycloakify";
-import type { KcContext } from "./kcContext";
import { clsx } from "keycloakify/lib/tools/clsx";
-import type { I18n } from "./i18n";
+import type { PageProps } from "keycloakify";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
-type KcContext_Register = Extract<KcContext, { pageId: "register.ftl"; }>;
+export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl"; }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
-const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Register; i18n: I18n; } & KcProps) => {
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n;
- console.log(`NOTE: It is up to you do do something meaningful with ${kcContext.authorizedMailDomains}`)
+ console.log(`NOTE: It is up to you do do something meaningful with ${kcContext.authorizedMailDomains}`);
return (
<Template
- {...{ kcContext, i18n, ...props }}
- doFetchDefaultThemeResources={true}
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
headerNode={msg("registerTitle")}
formNode={
- <form id="kc-register-form" className={clsx(props.kcFormClass)} action={url.registrationAction} method="post">
- <div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="firstName" className={clsx(props.kcLabelClass)}>
+ <form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
+ <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}>
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}>
{msg("firstName")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="firstName"
- className={clsx(props.kcInputClass)}
+ className={clsx(kcProps.kcInputClass)}
name="firstName"
defaultValue={register.formData.firstName ?? ""}
/>
</div>
</div>
- <div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="lastName" className={clsx(props.kcLabelClass)}>
+ <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}>
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="lastName" className={clsx(kcProps.kcLabelClass)}>
{msg("lastName")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="lastName"
- className={clsx(props.kcInputClass)}
+ className={clsx(kcProps.kcInputClass)}
name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
- <div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="email" className={clsx(props.kcLabelClass)}>
+ <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}>
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="email" className={clsx(kcProps.kcLabelClass)}>
{msg("email")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="email"
- className={clsx(props.kcInputClass)}
+ className={clsx(kcProps.kcInputClass)}
name="email"
defaultValue={register.formData.email ?? ""}
autoComplete="email"
@@ -79,17 +75,17 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
</div>
</div>
{!realm.registrationEmailAsUsername && (
- <div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="username" className={clsx(props.kcLabelClass)}>
+ <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}>
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
{msg("username")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<input
type="text"
id="username"
- className={clsx(props.kcInputClass)}
+ className={clsx(kcProps.kcInputClass)}
name="username"
defaultValue={register.formData.username ?? ""}
autoComplete="username"
@@ -99,17 +95,19 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
)}
{passwordRequired && (
<>
- <div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="password" className={clsx(props.kcLabelClass)}>
+ <div
+ className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}
+ >
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
{msg("password")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<input
type="password"
id="password"
- className={clsx(props.kcInputClass)}
+ className={clsx(kcProps.kcInputClass)}
name="password"
autoComplete="new-password"
/>
@@ -118,40 +116,45 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
<div
className={clsx(
- props.kcFormGroupClass,
- messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass)
+ kcProps.kcFormGroupClass,
+ messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass)
)}
>
- <div className={clsx(props.kcLabelWrapperClass)}>
- <label htmlFor="password-confirm" className={clsx(props.kcLabelClass)}>
+ <div className={clsx(kcProps.kcLabelWrapperClass)}>
+ <label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
{msg("passwordConfirm")}
</label>
</div>
- <div className={clsx(props.kcInputWrapperClass)}>
- <input type="password" id="password-confirm" className={clsx(props.kcInputClass)} name="password-confirm" />
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
+ <input type="password" id="password-confirm" className={clsx(kcProps.kcInputClass)} name="password-confirm" />
</div>
</div>
</>
)}
{recaptchaRequired && (
<div className="form-group">
- <div className={clsx(props.kcInputWrapperClass)}>
+ <div className={clsx(kcProps.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div>
</div>
)}
- <div className={clsx(props.kcFormGroupClass)}>
- <div id="kc-form-options" className={clsx(props.kcFormOptionsClass)}>
- <div className={clsx(props.kcFormOptionsWrapperClass)}>
+ <div className={clsx(kcProps.kcFormGroupClass)}>
+ <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
+ <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
- <div id="kc-form-buttons" className={clsx(props.kcFormButtonsClass)}>
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input
- className={clsx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)}
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
type="submit"
value={msgStr("doRegister")}
/>
@@ -161,6 +164,5 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
}
/>
);
-});
+}
-export default Register;
diff --git a/src/keycloakTheme/pages/Terms.tsx b/src/keycloakTheme/pages/Terms.tsx
new file mode 100644
index 0000000..342602e
--- /dev/null
+++ b/src/keycloakTheme/pages/Terms.tsx
@@ -0,0 +1,82 @@
+/**
+ * NOTE: Yo do not need to do all this to put your own Terms and conditions
+ * this is if you want component level customization.
+ * If the default works for you you can just use the useDownloadTerms hook
+ * in the KcApp.tsx
+ * Example: https://github.com/garronej/keycloakify-starter/blob/a20c21b2aae7c6dc6dbea294f3d321955ddf9355/src/KcApp/KcApp.tsx#L14-L30
+ */
+
+import { clsx } from "keycloakify/lib/tools/clsx";
+import { useRerenderOnStateChange } from "evt/hooks";
+import { Markdown } from "keycloakify/lib/tools/Markdown";
+import { evtTermMarkdown, useDownloadTerms } from "keycloakify/lib/pages/Terms";
+import tos_en_url from "../assets/tos_en.md";
+import tos_fr_url from "../assets/tos_fr.md";
+import type { KcContext } from "../kcContext";
+import type { PageProps } from "keycloakify/lib/KcProps";
+import type { I18n } from "../i18n";
+
+export default function Terms(props: PageProps<Extract<KcContext, { pageId: "terms.ftl"; }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { msg, msgStr } = i18n;
+
+ useDownloadTerms({
+ kcContext,
+ "downloadTermMarkdown": async ({ currentLanguageTag }) => {
+
+ const markdownString = await fetch((() => {
+ switch (currentLanguageTag) {
+ case "fr": return tos_fr_url;
+ default: return tos_en_url;
+ }
+ })()).then(response => response.text());
+
+ return markdownString;
+ },
+ });
+
+ useRerenderOnStateChange(evtTermMarkdown);
+
+ const { url } = kcContext;
+
+ if (evtTermMarkdown.state === undefined) {
+ return null;
+ }
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ displayMessage={false}
+ headerNode={msg("termsTitle")}
+ formNode={
+ <>
+ <div id="kc-terms-text">{evtTermMarkdown.state && <Markdown>{evtTermMarkdown.state}</Markdown>}</div>
+ <form className="form-actions" action={url.loginAction} method="POST">
+ <input
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonClass,
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonLargeClass
+ )}
+ name="accept"
+ id="kc-accept"
+ type="submit"
+ value={msgStr("doAccept")}
+ />
+ <input
+ className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
+ name="cancel"
+ id="kc-decline"
+ type="submit"
+ value={msgStr("doDecline")}
+ />
+ </form>
+ <div className="clearfix" />
+ </>
+ }
+ />
+ );
+}