Commit for saving

This commit is contained in:
garronej 2023-02-26 12:32:22 +01:00
parent 1539980f66
commit 3d0736e72b
21 changed files with 604 additions and 319 deletions

View file

@ -19,7 +19,7 @@ yarn
yarn keycloak # Build the theme one time (some assets will be copied to yarn keycloak # Build the theme one time (some assets will be copied to
# public/keycloak_static, they are needed to dev your page outside of Keycloak) # public/keycloak_static, they are needed to dev your page outside of Keycloak)
yarn start # See the Hello World app yarn start # See the Hello World app
# Uncomment line 15 of src/KcApp/kcContext, reload https://localhost:3000 # Uncomment line 15 of src/keycloakTheme/kcContext, reload https://localhost:3000
# You can now develop your Login pages. # You can now develop your Login pages.
# Think your theme is ready? Run # Think your theme is ready? Run
@ -89,14 +89,15 @@ and remove unnecessary file.
```bash ```bash
rm -r src/App rm -r src/App
rm src/KcApp/index.ts rm src/keycloakTheme/index.ts
mv src/KcApp/* src/ mv src/keycloakTheme/* src/
rm -r src/keycloakTheme
cat << EOF > src/index.tsx cat << EOF > src/index.tsx
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { StrictMode } from "react"; import { StrictMode } from "react";
import { kcContext } from "./kcContext"; import { kcContext } from "./kcContext";
import KcApp from "KcApp"; import KcApp from "./KcApp";
if( kcContext === undefined ){ if( kcContext === undefined ){
throw new Error( throw new Error(

View file

@ -33,13 +33,16 @@
"@types/react": "18.0.9", "@types/react": "18.0.9",
"@types/react-dom": "18.0.4", "@types/react-dom": "18.0.4",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"typescript": "^4.7.3" "typescript": "^4.9.5"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
"react-app", "react-app",
"react-app/jest" "react-app/jest"
] ],
"rules": {
"react-hooks/exhaustive-deps": "off"
}
}, },
"browserslist": { "browserslist": {
"production": [ "production": [

View file

@ -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>
);
}

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,3 +0,0 @@
import KcApp from "./KcApp";
export * from "./KcApp";
export default KcApp;

View file

@ -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>;

View file

@ -1,13 +1,9 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { StrictMode, lazy, Suspense } from "react"; import { StrictMode, lazy, Suspense } from "react";
import { kcContext } from "./KcApp/kcContext"; import { kcContext } from "./keycloakTheme/kcContext";
const App = lazy(() => import("./App")); const App = lazy(() => import("./App"));
const KcApp = lazy(() => import("./KcApp")); const KcApp = lazy(() => import("./keycloakTheme/KcApp"));
if (kcContext !== undefined) {
console.log(kcContext);
}
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>;

View file

@ -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> }
/>
);
}

View file

@ -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> }
/>
);
}

View file

@ -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 // 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. // 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 // 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 // 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 { 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 { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n; 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 ( return (
<Template <Template
{...{ kcContext, i18n, ...props }} {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
doFetchDefaultThemeResources={true}
headerNode={msg("registerTitle")} headerNode={msg("registerTitle")}
formNode={ formNode={
<form id="kc-register-form" className={clsx(props.kcFormClass)} action={url.registrationAction} method="post"> <form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("firstName", props.kcFormGroupErrorClass))}> <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("firstName", kcProps.kcFormGroupErrorClass))}>
<div className={clsx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="firstName" className={clsx(props.kcLabelClass)}> <label htmlFor="firstName" className={clsx(kcProps.kcLabelClass)}>
{msg("firstName")} {msg("firstName")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="firstName" id="firstName"
className={clsx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="firstName" name="firstName"
defaultValue={register.formData.firstName ?? ""} defaultValue={register.formData.firstName ?? ""}
/> />
</div> </div>
</div> </div>
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("lastName", props.kcFormGroupErrorClass))}> <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("lastName", kcProps.kcFormGroupErrorClass))}>
<div className={clsx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="lastName" className={clsx(props.kcLabelClass)}> <label htmlFor="lastName" className={clsx(kcProps.kcLabelClass)}>
{msg("lastName")} {msg("lastName")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="lastName" id="lastName"
className={clsx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="lastName" name="lastName"
defaultValue={register.formData.lastName ?? ""} defaultValue={register.formData.lastName ?? ""}
/> />
</div> </div>
</div> </div>
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("email", props.kcFormGroupErrorClass))}> <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("email", kcProps.kcFormGroupErrorClass))}>
<div className={clsx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="email" className={clsx(props.kcLabelClass)}> <label htmlFor="email" className={clsx(kcProps.kcLabelClass)}>
{msg("email")} {msg("email")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="email" id="email"
className={clsx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="email" name="email"
defaultValue={register.formData.email ?? ""} defaultValue={register.formData.email ?? ""}
autoComplete="email" autoComplete="email"
@ -79,17 +75,17 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
</div> </div>
</div> </div>
{!realm.registrationEmailAsUsername && ( {!realm.registrationEmailAsUsername && (
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("username", props.kcFormGroupErrorClass))}> <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("username", kcProps.kcFormGroupErrorClass))}>
<div className={clsx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="username" className={clsx(props.kcLabelClass)}> <label htmlFor="username" className={clsx(kcProps.kcLabelClass)}>
{msg("username")} {msg("username")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="text" type="text"
id="username" id="username"
className={clsx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="username" name="username"
defaultValue={register.formData.username ?? ""} defaultValue={register.formData.username ?? ""}
autoComplete="username" autoComplete="username"
@ -99,17 +95,19 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
)} )}
{passwordRequired && ( {passwordRequired && (
<> <>
<div className={clsx(props.kcFormGroupClass, messagesPerField.printIfExists("password", props.kcFormGroupErrorClass))}> <div
<div className={clsx(props.kcLabelWrapperClass)}> className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}
<label htmlFor="password" className={clsx(props.kcLabelClass)}> >
<div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password" className={clsx(kcProps.kcLabelClass)}>
{msg("password")} {msg("password")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input <input
type="password" type="password"
id="password" id="password"
className={clsx(props.kcInputClass)} className={clsx(kcProps.kcInputClass)}
name="password" name="password"
autoComplete="new-password" autoComplete="new-password"
/> />
@ -118,40 +116,45 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
<div <div
className={clsx( className={clsx(
props.kcFormGroupClass, kcProps.kcFormGroupClass,
messagesPerField.printIfExists("password-confirm", props.kcFormGroupErrorClass) messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass)
)} )}
> >
<div className={clsx(props.kcLabelWrapperClass)}> <div className={clsx(kcProps.kcLabelWrapperClass)}>
<label htmlFor="password-confirm" className={clsx(props.kcLabelClass)}> <label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}>
{msg("passwordConfirm")} {msg("passwordConfirm")}
</label> </label>
</div> </div>
<div className={clsx(props.kcInputWrapperClass)}> <div className={clsx(kcProps.kcInputWrapperClass)}>
<input type="password" id="password-confirm" className={clsx(props.kcInputClass)} name="password-confirm" /> <input type="password" id="password-confirm" className={clsx(kcProps.kcInputClass)} name="password-confirm" />
</div> </div>
</div> </div>
</> </>
)} )}
{recaptchaRequired && ( {recaptchaRequired && (
<div className="form-group"> <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 className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div> </div>
</div> </div>
)} )}
<div className={clsx(props.kcFormGroupClass)}> <div className={clsx(kcProps.kcFormGroupClass)}>
<div id="kc-form-options" className={clsx(props.kcFormOptionsClass)}> <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={clsx(props.kcFormOptionsWrapperClass)}> <div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
<span> <span>
<a href={url.loginUrl}>{msg("backToLogin")}</a> <a href={url.loginUrl}>{msg("backToLogin")}</a>
</span> </span>
</div> </div>
</div> </div>
<div id="kc-form-buttons" className={clsx(props.kcFormButtonsClass)}> <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input <input
className={clsx(props.kcButtonClass, props.kcButtonPrimaryClass, props.kcButtonBlockClass, props.kcButtonLargeClass)} className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit" type="submit"
value={msgStr("doRegister")} value={msgStr("doRegister")}
/> />
@ -161,6 +164,5 @@ const Register = memo(({ kcContext, i18n, ...props }: { kcContext: KcContext_Reg
} }
/> />
); );
}); }
export default Register;

View file

@ -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" />
</>
}
/>
);
}

View file

@ -8810,10 +8810,10 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript@^4.7.3: typescript@^4.9.5:
version "4.7.4" version "4.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
unbox-primitive@^1.0.2: unbox-primitive@^1.0.2:
version "1.0.2" version "1.0.2"