diff --git a/README.md b/README.md
index 8381213..034c88a 100644
--- a/README.md
+++ b/README.md
@@ -87,7 +87,7 @@ and remove unnecessary files.
```bash
rm -r src/App
-rm src/keycloak-theme/valuesTransferredOverUrl.ts
+rm src/keycloak-theme/login/valuesTransferredOverUrl.ts
mv src/keycloak-theme/* src/
rm -r src/keycloak-theme
diff --git a/package.json b/package.json
index a568c2d..281e663 100755
--- a/package.json
+++ b/package.json
@@ -20,6 +20,10 @@
"extraLoginPages": [
"my-extra-page-1.ftl",
"my-extra-page-2.ftl"
+ ],
+ "extraAccountPages": [
+ "my-extra-page-1.ftl",
+ "my-extra-page-2.ftl"
]
},
"author": "u/garronej",
diff --git a/src/App/App.tsx b/src/App/App.tsx
index 68f1393..dccb8af 100644
--- a/src/App/App.tsx
+++ b/src/App/App.tsx
@@ -5,9 +5,12 @@ import { createOidcClientProvider, useOidcClient } from "./oidc";
import { addFooToQueryParams, addBarToQueryParams } from "../keycloak-theme/login/valuesTransferredOverUrl";
import jwt_decode from "jwt-decode";
+const keycloakUrl = "https://auth.code.gouv.fr"
+const keycloakRealm = "keycloakify";
+
const { OidcClientProvider } = createOidcClientProvider({
- url: "https://auth.code.gouv.fr/auth",
- realm: "keycloakify",
+ url: `${keycloakUrl}/auth`,
+ realm: keycloakRealm,
clientId: "starter",
//This function will be called just before redirecting,
//it should return the current langue.
@@ -38,18 +41,20 @@ function ContextualizedApp() {
return (
- {
- oidcClient.isUserLoggedIn ?
- <>
- You are authenticated !
- {JSON.stringify(jwt_decode(oidcClient.getAccessToken()), null, 2)}
-
- >
- :
- <>
-
- >
- }
+ {
+ oidcClient.isUserLoggedIn ?
+ <>
+ You are authenticated !
+ {/* On older Keycloak version its /auth/realms instead of /realms */}
+ Link to your Keycloak account
+ {JSON.stringify(jwt_decode(oidcClient.getAccessToken()), null, 2)}
+
+ >
+ :
+ <>
+
+ >
+ }
Hello world
diff --git a/src/index.tsx b/src/index.tsx
index ba55bfe..fa7baab 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,9 +1,11 @@
import { createRoot } from "react-dom/client";
import { StrictMode, lazy, Suspense } from "react";
import { kcContext as kcLoginThemeContext } from "./keycloak-theme/login/kcContext";
+import { kcContext as kcAccountThemeContext } from "./keycloak-theme/account/kcContext";
-const App = lazy(() => import("./App"));
const KcLoginThemeApp = lazy(() => import("./keycloak-theme/login/KcApp"));
+const KcAccountThemeApp = lazy(() => import("./keycloak-theme/account/KcApp"));
+const App = lazy(() => import("./App"));
createRoot(document.getElementById("root")!).render(
@@ -14,6 +16,10 @@ createRoot(document.getElementById("root")!).render(
return ;
}
+ if( kcAccountThemeContext !== undefined ){
+ return ;
+ }
+
return ;
})()}
diff --git a/src/keycloak-theme/account/KcApp.css b/src/keycloak-theme/account/KcApp.css
new file mode 100644
index 0000000..1f110d9
--- /dev/null
+++ b/src/keycloak-theme/account/KcApp.css
@@ -0,0 +1,5 @@
+
+
+.my-root-class {
+ background: url(./assets/background.svg) no-repeat center center fixed;
+}
\ No newline at end of file
diff --git a/src/keycloak-theme/account/KcApp.tsx b/src/keycloak-theme/account/KcApp.tsx
new file mode 100644
index 0000000..d6d9e46
--- /dev/null
+++ b/src/keycloak-theme/account/KcApp.tsx
@@ -0,0 +1,41 @@
+import "./KcApp.css";
+import { lazy, Suspense } from "react";
+import Fallback, { type PageProps } from "keycloakify/account";
+import type { KcContext } from "./kcContext";
+import { useI18n } from "./i18n";
+
+const Template = lazy(() => import("./Template"));
+const DefaultTemplate = lazy(() => import("keycloakify/account/Template"));
+
+const Password = lazy(() => import("./pages/Password"));
+const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
+const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
+
+const classes: PageProps["classes"] = {
+ "kcBodyClass": "my-root-class"
+};
+
+export default function App(props: { kcContext: KcContext; }) {
+
+ const { kcContext } = props;
+
+ const i18n = useI18n({ kcContext });
+
+ if (i18n === null) {
+ return null;
+ }
+
+ return (
+
+ {(() => {
+ switch (kcContext.pageId) {
+ case "password.ftl": return ;
+ case "my-extra-page-1.ftl": return ;
+ case "my-extra-page-2.ftl": return ;
+ default: return ;
+ }
+ })()}
+
+ );
+
+}
diff --git a/src/keycloak-theme/account/Template.tsx b/src/keycloak-theme/account/Template.tsx
new file mode 100644
index 0000000..283178b
--- /dev/null
+++ b/src/keycloak-theme/account/Template.tsx
@@ -0,0 +1,132 @@
+// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
+
+import { clsx } from "keycloakify/tools/clsx";
+import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
+import { type TemplateProps } from "keycloakify/account/TemplateProps";
+import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
+import type { KcContext } from "./kcContext";
+import type { I18n } from "./i18n";
+import { assert } from "keycloakify/tools/assert";
+
+export default function Template(props: TemplateProps) {
+ const { kcContext, i18n, doUseDefaultCss, active, classes, children } = props;
+
+ const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
+
+ const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
+
+ const { locale, url, features, realm, message, referrer } = kcContext;
+
+ const { isReady } = usePrepareTemplate({
+ "doFetchDefaultThemeResources": doUseDefaultCss,
+ url,
+ "stylesCommon": ["node_modules/patternfly/dist/css/patternfly.min.css", "node_modules/patternfly/dist/css/patternfly-additions.min.css"],
+ "styles": ["css/account.css"],
+ "htmlClassName": undefined,
+ "bodyClassName": clsx("admin-console", "user", getClassName("kcBodyClass"))
+ });
+
+ if (!isReady) {
+ return null;
+ }
+
+ return (
+ <>
+
+
+
+
+
+
+ {message !== undefined && (
+
+ {message.type === "success" && }
+ {message.type === "error" && }
+ {message.summary}
+
+ )}
+
+ {children}
+
+
+ >
+ );
+}
diff --git a/src/keycloak-theme/account/assets/background.svg b/src/keycloak-theme/account/assets/background.svg
new file mode 100644
index 0000000..0e1cada
--- /dev/null
+++ b/src/keycloak-theme/account/assets/background.svg
@@ -0,0 +1,132 @@
+
diff --git a/src/keycloak-theme/account/i18n.ts b/src/keycloak-theme/account/i18n.ts
new file mode 100644
index 0000000..45f75c4
--- /dev/null
+++ b/src/keycloak-theme/account/i18n.ts
@@ -0,0 +1,6 @@
+import { createUseI18n } from "keycloakify/account";
+
+//NOTE: See src/login/i18n.ts for instructions on customization of i18n messages.
+export const { useI18n } = createUseI18n({});
+
+export type I18n = NonNullable>;
diff --git a/src/keycloak-theme/account/kcContext.ts b/src/keycloak-theme/account/kcContext.ts
new file mode 100644
index 0000000..f105263
--- /dev/null
+++ b/src/keycloak-theme/account/kcContext.ts
@@ -0,0 +1,17 @@
+import { getKcContext } from "keycloakify/account";
+
+export type KcContextExtension =
+ | { pageId: "my-extra-page-1.ftl"; }
+ | { pageId: "my-extra-page-2.ftl"; someCustomValue: string; };
+
+export const { kcContext } = getKcContext({
+ //mockPageId: "password.ftl",
+ mockData: [
+ {
+ pageId: "my-extra-page-2.ftl",
+ someCustomValue: "foo bar"
+ }
+ ]
+});
+
+export type KcContext = NonNullable;
\ No newline at end of file
diff --git a/src/keycloak-theme/account/pages/MyExtraPage1.tsx b/src/keycloak-theme/account/pages/MyExtraPage1.tsx
new file mode 100644
index 0000000..649e4cb
--- /dev/null
+++ b/src/keycloak-theme/account/pages/MyExtraPage1.tsx
@@ -0,0 +1,15 @@
+import type { PageProps } from "keycloakify/account/pages/PageProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function MyExtraPage1(props: PageProps, I18n>) {
+
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+
+ return (
+
+ Hello world 1
+
+ );
+
+}
diff --git a/src/keycloak-theme/account/pages/MyExtraPage2.tsx b/src/keycloak-theme/account/pages/MyExtraPage2.tsx
new file mode 100644
index 0000000..dc90e84
--- /dev/null
+++ b/src/keycloak-theme/account/pages/MyExtraPage2.tsx
@@ -0,0 +1,18 @@
+import type { PageProps } from "keycloakify/account/pages/PageProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function MyExtraPage1(props: PageProps, I18n>) {
+
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+
+ // someCustomValue is declared by you in ../kcContext.ts
+ console.log(`TODO: Do something with: ${kcContext.someCustomValue}`);
+
+ return (
+
+ Hello world 2
+
+ );
+
+}
diff --git a/src/keycloak-theme/account/pages/Password.tsx b/src/keycloak-theme/account/pages/Password.tsx
new file mode 100644
index 0000000..c92b4d1
--- /dev/null
+++ b/src/keycloak-theme/account/pages/Password.tsx
@@ -0,0 +1,105 @@
+import { clsx } from "keycloakify/tools/clsx";
+import type { PageProps } from "keycloakify/account/pages/PageProps";
+import { useGetClassName } from "keycloakify/account/lib/useGetClassName";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function LogoutConfirm(props: PageProps, I18n>) {
+ const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
+
+ const { getClassName } = useGetClassName({
+ doUseDefaultCss,
+ "classes": {
+ ...classes,
+ "kcBodyClass": clsx(classes?.kcBodyClass, "password")
+ }
+ });
+
+ const { url, password, account } = kcContext;
+
+ const { msg } = i18n;
+
+ return (
+
+
+
+
{msg("changePasswordHtmlTitle")}
+
+
+ ${msg("allFieldsRequired")}
+
+
+
+
+
+ );
+}
diff --git a/src/keycloak-theme/login/Template.tsx b/src/keycloak-theme/login/Template.tsx
index 1fd077a..3e732e4 100644
--- a/src/keycloak-theme/login/Template.tsx
+++ b/src/keycloak-theme/login/Template.tsx
@@ -1,9 +1,10 @@
-// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/Template.tsx
+// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/login/Template.tsx
+
import { assert } from "keycloakify/tools/assert";
import { clsx } from "keycloakify/tools/clsx";
import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
-import { type TemplateProps, defaultTemplateClasses } from "keycloakify/login/TemplateProps";
-import { useGetClassName } from "keycloakify/lib/useGetClassName";
+import { type TemplateProps } from "keycloakify/login/TemplateProps";
+import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "./kcContext";
import type { I18n } from "./i18n";
@@ -24,10 +25,7 @@ export default function Template(props: TemplateProps) {
children
} = props;
- const { getClassName } = useGetClassName({
- "defaultClasses": !doUseDefaultCss ? undefined : defaultTemplateClasses,
- classes
- });
+ const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
diff --git a/src/keycloak-theme/login/pages/Login.tsx b/src/keycloak-theme/login/pages/Login.tsx
index f89c259..4d2de20 100644
--- a/src/keycloak-theme/login/pages/Login.tsx
+++ b/src/keycloak-theme/login/pages/Login.tsx
@@ -1,8 +1,9 @@
+// ejected using 'npx eject-keycloak-page'
import { useState, type FormEventHandler } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
-import { type PageProps, defaultClasses } from "keycloakify/login/pages/PageProps";
-import { useGetClassName } from "keycloakify/lib/useGetClassName";
+import type { PageProps } from "keycloakify/login/pages/PageProps";
+import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
@@ -10,7 +11,7 @@ export default function Login(props: PageProps