Commit for saving
This commit is contained in:
parent
1539980f66
commit
3d0736e72b
|
@ -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(
|
||||||
|
|
|
@ -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": [
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import KcApp from "./KcApp";
|
|
||||||
export * from "./KcApp";
|
|
||||||
export default KcApp;
|
|
|
@ -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>;
|
|
|
@ -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>
|
||||||
|
|
80
src/keycloakTheme/KcApp.tsx
Normal file
80
src/keycloakTheme/KcApp.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
237
src/keycloakTheme/Template.tsx
Normal file
237
src/keycloakTheme/Template.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
88
src/keycloakTheme/kcContext.ts
Normal file
88
src/keycloakTheme/kcContext.ts
Normal 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>;
|
22
src/keycloakTheme/pages/MyExtraPage1.tsx
Normal file
22
src/keycloakTheme/pages/MyExtraPage1.tsx
Normal 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> }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
25
src/keycloakTheme/pages/MyExtraPage2.tsx
Normal file
25
src/keycloakTheme/pages/MyExtraPage2.tsx
Normal 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> }
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
|
82
src/keycloakTheme/pages/Terms.tsx
Normal file
82
src/keycloakTheme/pages/Terms.tsx
Normal 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" />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in a new issue