diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/KcApp/KcApp.tsx | 45 | ||||
| -rw-r--r-- | src/KcApp/MyExtraPage1.tsx | 14 | ||||
| -rw-r--r-- | src/KcApp/MyExtraPage2.tsx | 16 | ||||
| -rw-r--r-- | src/KcApp/Terms.tsx | 94 | ||||
| -rw-r--r-- | src/KcApp/index.ts | 3 | ||||
| -rw-r--r-- | src/KcApp/kcContext.ts | 79 | ||||
| -rw-r--r-- | src/index.tsx | 8 | ||||
| -rw-r--r-- | src/keycloakTheme/KcApp.css (renamed from src/KcApp/KcApp.css) | 0 | ||||
| -rw-r--r-- | src/keycloakTheme/KcApp.tsx | 80 | ||||
| -rw-r--r-- | src/keycloakTheme/Template.tsx | 237 | ||||
| -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.ts | 88 | ||||
| -rw-r--r-- | src/keycloakTheme/pages/MyExtraPage1.tsx | 22 | ||||
| -rw-r--r-- | src/keycloakTheme/pages/MyExtraPage2.tsx | 25 | ||||
| -rw-r--r-- | src/keycloakTheme/pages/Register.tsx (renamed from src/KcApp/Register.tsx) | 106 | ||||
| -rw-r--r-- | src/keycloakTheme/pages/Terms.tsx | 82 |
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" /> + </> + } + /> + ); +} |