aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/IdpReviewUserProfile.tsx2
-rw-r--r--src/keycloak-theme/pages/Info.stories.tsx7
-rw-r--r--src/keycloak-theme/pages/Login.stories.tsx24
-rw-r--r--src/keycloak-theme/pages/LoginConfigTotp.stories.tsx12
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/LoginOtp.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/LoginPageExpired.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/LoginPassword.stories.tsx10
-rw-r--r--src/keycloak-theme/pages/LoginResetPassword.stories.tsx10
-rw-r--r--src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/LoginUsername.stories.tsx17
-rw-r--r--src/keycloak-theme/pages/LoginUsername.tsx158
-rw-r--r--src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/LoginVerifyEmail.tsx32
-rw-r--r--src/keycloak-theme/pages/LogoutConfirm.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/LogoutConfirm.tsx58
-rw-r--r--src/keycloak-theme/pages/MyExtraPage1.stories.tsx8
-rw-r--r--src/keycloak-theme/pages/MyExtraPage2.stories.tsx10
-rw-r--r--src/keycloak-theme/pages/Register.stories.tsx12
-rw-r--r--src/keycloak-theme/pages/RegisterUserProfile.stories.tsx9
-rw-r--r--src/keycloak-theme/pages/RegisterUserProfile.tsx2
-rw-r--r--src/keycloak-theme/pages/Terms.stories.tsx5
-rw-r--r--src/keycloak-theme/pages/UpdateUserProfile.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/UpdateUserProfile.tsx67
-rw-r--r--src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx16
-rw-r--r--src/keycloak-theme/pages/WebauthnAuthenticate.tsx193
28 files changed, 671 insertions, 77 deletions
diff --git a/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx b/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
index e3d6915..660216c 100644
--- a/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
+++ b/src/keycloak-theme/pages/IdpReviewUserProfile.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('idp-review-user-profile.ftl');
export default {
kind: 'Page',
@@ -11,7 +13,5 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('idp-review-user-profile.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/IdpReviewUserProfile.tsx b/src/keycloak-theme/pages/IdpReviewUserProfile.tsx
index ec73693..2e4f789 100644
--- a/src/keycloak-theme/pages/IdpReviewUserProfile.tsx
+++ b/src/keycloak-theme/pages/IdpReviewUserProfile.tsx
@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { clsx } from "keycloakify/lib/tools/clsx";
-import { UserProfileFormFields } from "./shared/UserProfileCommons";
+import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
import type { PageProps } from "keycloakify";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
diff --git a/src/keycloak-theme/pages/Info.stories.tsx b/src/keycloak-theme/pages/Info.stories.tsx
index e6bacc5..4fd483f 100644
--- a/src/keycloak-theme/pages/Info.stories.tsx
+++ b/src/keycloak-theme/pages/Info.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('info.ftl');
export default {
kind: 'Page',
@@ -11,7 +13,6 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('info.ftl');
export const Default = bind({
messageHeader: 'Yo, get this:',
diff --git a/src/keycloak-theme/pages/Login.stories.tsx b/src/keycloak-theme/pages/Login.stories.tsx
index a2e9f8d..9cafd82 100644
--- a/src/keycloak-theme/pages/Login.stories.tsx
+++ b/src/keycloak-theme/pages/Login.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {socialProviders, template} from '../../../.storybook/util'
+import { socialProviders, template } from '../../../.storybook/util'
+
+const bind = template('login.ftl');
export default {
kind: 'Page',
@@ -11,17 +13,15 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login.ftl');
-
export const Default = bind({})
-export const WithoutPasswordField = bind({realm: {password: false}})
-export const WithoutRegistration = bind({realm: {registrationAllowed: false}})
-export const WithoutRememberMe = bind({realm: {rememberMe: false}})
-export const WithoutPasswordReset = bind({realm: {resetPasswordAllowed: false}})
-export const WithEmailAsUsername = bind({realm: {loginWithEmailAllowed: false}})
-export const WithPresetUsername = bind({login: {username: '[email protected]'}})
+export const WithoutPasswordField = bind({ realm: { password: false } })
+export const WithoutRegistration = bind({ realm: { registrationAllowed: false } })
+export const WithoutRememberMe = bind({ realm: { rememberMe: false } })
+export const WithoutPasswordReset = bind({ realm: { resetPasswordAllowed: false } })
+export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: false } })
+export const WithPresetUsername = bind({ login: { username: '[email protected]' } })
export const WithImmutablePresetUsername = bind({
- login: {username: '[email protected]'},
+ login: { username: '[email protected]' },
usernameEditDisabled: true
})
-export const WithSocialProviders = bind({social: {displayInfo: true, providers: socialProviders}})
+export const WithSocialProviders = bind({ social: { displayInfo: true, providers: socialProviders } })
diff --git a/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx b/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
index b0a673f..d798dd2 100644
--- a/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
+++ b/src/keycloak-theme/pages/LoginConfigTotp.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-config-totp.ftl');
export default {
kind: 'Page',
@@ -11,17 +13,15 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-config-totp.ftl');
-
export const Default = bind({})
-export const WithManualSetUp = bind({mode: 'manual'})
+export const WithManualSetUp = bind({ mode: 'manual' })
export const WithError = bind({
messagesPerField: {
get: (fieldName: string) => fieldName === 'totp' ? 'Invalid TOTP' : undefined,
exists: (fieldName: string) => fieldName === 'totp',
existsError: (fieldName: string) => fieldName === 'totp',
- printIfExists: <T, >(fieldName: string, x: T) => fieldName === 'totp' ? x : undefined
+ printIfExists: <T,>(fieldName: string, x: T) => fieldName === 'totp' ? x : undefined
}
})
diff --git a/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx b/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
index 55ff499..75617bb 100644
--- a/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
+++ b/src/keycloak-theme/pages/LoginIdpLinkConfirm.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-idp-link-confirm.ftl');
export default {
kind: 'Page',
@@ -11,7 +13,5 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-idp-link-confirm.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx b/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
index 98f280b..1d03385 100644
--- a/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
+++ b/src/keycloak-theme/pages/LoginIdpLinkEmail.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-idp-link-email.ftl');
export default {
kind: 'Page',
@@ -11,7 +13,5 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-idp-link-email.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginOtp.stories.tsx b/src/keycloak-theme/pages/LoginOtp.stories.tsx
index 21e90bb..f674379 100644
--- a/src/keycloak-theme/pages/LoginOtp.stories.tsx
+++ b/src/keycloak-theme/pages/LoginOtp.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-otp.ftl');
export default {
kind: 'Page',
@@ -11,6 +13,4 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-otp.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginPageExpired.stories.tsx b/src/keycloak-theme/pages/LoginPageExpired.stories.tsx
index c991a2f..f5de885 100644
--- a/src/keycloak-theme/pages/LoginPageExpired.stories.tsx
+++ b/src/keycloak-theme/pages/LoginPageExpired.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {socialProviders, template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-page-expired.ftl');
export default {
kind: 'Page',
@@ -11,6 +13,4 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-page-expired.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginPassword.stories.tsx b/src/keycloak-theme/pages/LoginPassword.stories.tsx
index 6a53efa..5106dfc 100644
--- a/src/keycloak-theme/pages/LoginPassword.stories.tsx
+++ b/src/keycloak-theme/pages/LoginPassword.stories.tsx
@@ -1,16 +1,16 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-password.ftl');
export default {
kind: 'Page',
- title: 'Theme/Pages/Login/Password Only',
+ title: 'Theme/Pages/Login/Password',
component: KcApp,
parameters: {
layout: 'fullscreen',
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-password.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginResetPassword.stories.tsx b/src/keycloak-theme/pages/LoginResetPassword.stories.tsx
index 9ea3736..c417dd8 100644
--- a/src/keycloak-theme/pages/LoginResetPassword.stories.tsx
+++ b/src/keycloak-theme/pages/LoginResetPassword.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-reset-password.ftl');
export default {
kind: 'Page',
@@ -11,7 +13,5 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-reset-password.ftl');
-
export const Default = bind({})
-export const WithEmailAsUsername = bind({realm: {loginWithEmailAllowed: true, registrationEmailAsUsername: true}})
+export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true } })
diff --git a/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx b/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx
index cf5177e..91f2d7d 100644
--- a/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx
+++ b/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-update-password.ftl');
export default {
kind: 'Page',
@@ -11,6 +13,4 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('login-update-password.ftl');
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginUsername.stories.tsx b/src/keycloak-theme/pages/LoginUsername.stories.tsx
new file mode 100644
index 0000000..21e63c0
--- /dev/null
+++ b/src/keycloak-theme/pages/LoginUsername.stories.tsx
@@ -0,0 +1,17 @@
+import { ComponentMeta } from '@storybook/react';
+import KcApp from '../KcApp';
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-username.ftl');
+
+export default {
+ kind: 'Page',
+ title: 'Theme/Pages/Login/Username',
+ component: KcApp,
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof KcApp>;
+
+export const Default = bind({})
+export const WithEmailAsUsername = bind({ realm: { loginWithEmailAllowed: true, registrationEmailAsUsername: true } })
diff --git a/src/keycloak-theme/pages/LoginUsername.tsx b/src/keycloak-theme/pages/LoginUsername.tsx
new file mode 100644
index 0000000..7ea5e0a
--- /dev/null
+++ b/src/keycloak-theme/pages/LoginUsername.tsx
@@ -0,0 +1,158 @@
+import React, { useState } from "react";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import { useConstCallback } from "keycloakify/lib/tools/useConstCallback";
+import type { FormEventHandler } from "react";
+import type { PageProps } from "keycloakify";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function LoginUsername(props: PageProps<Extract<KcContext, { pageId: "login-username.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { social, realm, url, usernameHidden, login, registrationDisabled } = kcContext;
+
+ const { msg, msgStr } = i18n;
+
+ const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
+
+ const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
+ e.preventDefault();
+
+ setIsLoginButtonDisabled(true);
+
+ const formElement = e.target as HTMLFormElement;
+
+ //NOTE: Even if we login with email Keycloak expect username and password in
+ //the POST request.
+ formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
+
+ formElement.submit();
+ });
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ displayInfo={social.displayInfo}
+ displayWide={realm.password && social.providers !== undefined}
+ headerNode={msg("doLogIn")}
+ formNode={
+ <div id="kc-form" className={clsx(realm.password && social.providers !== undefined && kcProps.kcContentWrapperClass)}>
+ <div
+ id="kc-form-wrapper"
+ className={clsx(
+ realm.password && social.providers && [kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass]
+ )}
+ >
+ {realm.password && (
+ <form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
+ <div className={clsx(kcProps.kcFormGroupClass)}>
+ {!usernameHidden &&
+ (() => {
+ const label = !realm.loginWithEmailAllowed
+ ? "username"
+ : realm.registrationEmailAsUsername
+ ? "email"
+ : "usernameOrEmail";
+
+ const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
+
+ return (
+ <>
+ <label htmlFor={autoCompleteHelper} className={clsx(kcProps.kcLabelClass)}>
+ {msg(label)}
+ </label>
+ <input
+ tabIndex={1}
+ id={autoCompleteHelper}
+ className={clsx(kcProps.kcInputClass)}
+ // NOTE: This is used by Google Chrome auto fill so we use it to tell
+ // the browser how to pre fill the form but before submit we put it back
+ // to username because it is what keycloak expects.
+ name={autoCompleteHelper}
+ defaultValue={login.username ?? ""}
+ type="text"
+ autoFocus={true}
+ autoComplete="off"
+ />
+ </>
+ );
+ })()}
+ </div>
+ <div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}>
+ <div id="kc-form-options">
+ {realm.rememberMe && !usernameHidden && (
+ <div className="checkbox">
+ <label>
+ <input
+ tabIndex={3}
+ id="rememberMe"
+ name="rememberMe"
+ type="checkbox"
+ {...(login.rememberMe
+ ? {
+ "checked": true
+ }
+ : {})}
+ />
+ {msg("rememberMe")}
+ </label>
+ </div>
+ )}
+ </div>
+ </div>
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
+ <input
+ tabIndex={4}
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
+ name="login"
+ id="kc-login"
+ type="submit"
+ value={msgStr("doLogIn")}
+ disabled={isLoginButtonDisabled}
+ />
+ </div>
+ </form>
+ )}
+ </div>
+ {realm.password && social.providers !== undefined && (
+ <div id="kc-social-providers" className={clsx(kcProps.kcFormSocialAccountContentClass, kcProps.kcFormSocialAccountClass)}>
+ <ul
+ className={clsx(
+ kcProps.kcFormSocialAccountListClass,
+ social.providers.length > 4 && kcProps.kcFormSocialAccountDoubleListClass
+ )}
+ >
+ {social.providers.map(p => (
+ <li key={p.providerId} className={clsx(kcProps.kcFormSocialAccountListLinkClass)}>
+ <a href={p.loginUrl} id={`zocial-${p.alias}`} className={clsx("zocial", p.providerId)}>
+ <span>{p.displayName}</span>
+ </a>
+ </li>
+ ))}
+ </ul>
+ </div>
+ )}
+ </div>
+ }
+ infoNode={
+ realm.password &&
+ realm.registrationAllowed &&
+ !registrationDisabled && (
+ <div id="kc-registration">
+ <span>
+ {msg("noAccount")}
+ <a tabIndex={6} href={url.registrationUrl}>
+ {msg("doRegister")}
+ </a>
+ </span>
+ </div>
+ )
+ }
+ />
+ );
+}
diff --git a/src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx b/src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
new file mode 100644
index 0000000..2bc48a1
--- /dev/null
+++ b/src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
@@ -0,0 +1,16 @@
+import { ComponentMeta } from '@storybook/react';
+import KcApp from '../KcApp';
+import { template } from '../../../.storybook/util'
+
+const bind = template('login-verify-email.ftl');
+
+export default {
+ kind: 'Page',
+ title: 'Theme/Pages/Login/Verify Email',
+ component: KcApp,
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof KcApp>;
+
+export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LoginVerifyEmail.tsx b/src/keycloak-theme/pages/LoginVerifyEmail.tsx
new file mode 100644
index 0000000..513ef3a
--- /dev/null
+++ b/src/keycloak-theme/pages/LoginVerifyEmail.tsx
@@ -0,0 +1,32 @@
+import React from "react";
+import type { PageProps } from "keycloakify";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function LoginVerifyEmail(props: PageProps<Extract<KcContext, { pageId: "login-verify-email.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { msg } = i18n;
+
+ const { url, user } = kcContext;
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ displayMessage={false}
+ headerNode={msg("emailVerifyTitle")}
+ formNode={
+ <>
+ <p className="instruction">{msg("emailVerifyInstruction1", user?.email)}</p>
+ <p className="instruction">
+ {msg("emailVerifyInstruction2")}
+ <br />
+ <a href={url.loginAction}>{msg("doClickHere")}</a>
+ &nbsp;
+ {msg("emailVerifyInstruction3")}
+ </p>
+ </>
+ }
+ />
+ );
+}
diff --git a/src/keycloak-theme/pages/LogoutConfirm.stories.tsx b/src/keycloak-theme/pages/LogoutConfirm.stories.tsx
new file mode 100644
index 0000000..e8fd2e5
--- /dev/null
+++ b/src/keycloak-theme/pages/LogoutConfirm.stories.tsx
@@ -0,0 +1,16 @@
+import { ComponentMeta } from '@storybook/react';
+import KcApp from '../KcApp';
+import { template } from '../../../.storybook/util'
+
+const bind = template('logout-confirm.ftl');
+
+export default {
+ kind: 'Page',
+ title: 'Theme/Pages/Login/Logout Confirmation',
+ component: KcApp,
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof KcApp>;
+
+export const Default = bind({})
diff --git a/src/keycloak-theme/pages/LogoutConfirm.tsx b/src/keycloak-theme/pages/LogoutConfirm.tsx
new file mode 100644
index 0000000..5347f64
--- /dev/null
+++ b/src/keycloak-theme/pages/LogoutConfirm.tsx
@@ -0,0 +1,58 @@
+import React from "react";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import type { PageProps } from "keycloakify/lib/KcProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function LogoutConfirm(props: PageProps<Extract<KcContext, { pageId: "logout-confirm.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { url, client, logoutConfirm } = kcContext;
+
+ const { msg, msgStr } = i18n;
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ displayMessage={false}
+ headerNode={msg("logoutConfirmTitle")}
+ formNode={
+ <>
+ <div id="kc-logout-confirm" className="content-area">
+ <p className="instruction">{msg("logoutConfirmHeader")}</p>
+ <form className="form-actions" action={url.logoutConfirmAction} method="POST">
+ <input type="hidden" name="session_code" value={logoutConfirm.code} />
+ <div className={clsx(kcProps.kcFormGroupClass)}>
+ <div id="kc-form-options">
+ <div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
+ </div>
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormGroupClass)}>
+ <input
+ tabIndex={4}
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
+ name="confirmLogout"
+ id="kc-logout"
+ type="submit"
+ value={msgStr("doLogout")}
+ />
+ </div>
+ </div>
+ </form>
+ <div id="kc-info-message">
+ {!logoutConfirm.skipLink && client.baseUrl && (
+ <p>
+ <a href={client.baseUrl} dangerouslySetInnerHTML={{ __html: msgStr("backToApplication") }} />
+ </p>
+ )}
+ </div>
+ </div>
+ </>
+ }
+ />
+ );
+}
diff --git a/src/keycloak-theme/pages/MyExtraPage1.stories.tsx b/src/keycloak-theme/pages/MyExtraPage1.stories.tsx
index ef3e198..d1e5055 100644
--- a/src/keycloak-theme/pages/MyExtraPage1.stories.tsx
+++ b/src/keycloak-theme/pages/MyExtraPage1.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('my-extra-page-1.ftl')
export default {
kind: 'Page',
@@ -11,6 +13,4 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('my-extra-page-1.ftl')
-
export const Default = bind({})
diff --git a/src/keycloak-theme/pages/MyExtraPage2.stories.tsx b/src/keycloak-theme/pages/MyExtraPage2.stories.tsx
index 196f5a0..462ba7a 100644
--- a/src/keycloak-theme/pages/MyExtraPage2.stories.tsx
+++ b/src/keycloak-theme/pages/MyExtraPage2.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('my-extra-page-2.ftl')
export default {
kind: 'Page',
@@ -11,8 +13,6 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('my-extra-page-2.ftl')
-
export const Default = bind({})
-export const WithCustomValue = bind({someCustomValue: 'Foo Bar Baz'})
+export const WithCustomValue = bind({ someCustomValue: 'Foo Bar Baz' })
diff --git a/src/keycloak-theme/pages/Register.stories.tsx b/src/keycloak-theme/pages/Register.stories.tsx
index 6947fff..ddc1872 100644
--- a/src/keycloak-theme/pages/Register.stories.tsx
+++ b/src/keycloak-theme/pages/Register.stories.tsx
@@ -1,6 +1,8 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
+
+const bind = template('register.ftl')
export default {
kind: 'Page',
@@ -11,8 +13,6 @@ export default {
},
} as ComponentMeta<typeof KcApp>;
-const bind = template('register.ftl')
-
export const Default = bind({})
export const WithFieldError = bind({
@@ -25,12 +25,12 @@ export const WithFieldError = bind({
existsError: (fieldName: string) => fieldName === "email",
exists: (fieldName: string) => fieldName === "email",
get: (fieldName: string) => fieldName === "email" ? "I don't like your email address" : undefined,
- printIfExists: <T, >(fieldName: string, x: T) => fieldName === "email" ? x : undefined,
+ printIfExists: <T,>(fieldName: string, x: T) => fieldName === "email" ? x : undefined,
}
})
export const WithEmailAsUsername = bind({
- realm: {registrationEmailAsUsername: true}
+ realm: { registrationEmailAsUsername: true }
})
export const WithoutPassword = bind({
diff --git a/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx b/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
index 3e34c39..adc6685 100644
--- a/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
+++ b/src/keycloak-theme/pages/RegisterUserProfile.stories.tsx
@@ -1,6 +1,6 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
const bind = template('register-user-profile.ftl')
@@ -8,11 +8,12 @@ export default {
kind: 'Page',
title: 'Theme/Pages/Register/Modern',
component: KcApp,
- parameters: {layout: 'fullscreen'},
+ parameters: { layout: 'fullscreen' },
} as ComponentMeta<typeof KcApp>;
export const Default = bind({})
+/*
export const WithFieldError = bind({
profile: {
attributes: [
@@ -77,3 +78,5 @@ export const WithImmutablePresets = bind({
]
}
})
+
+*/ \ No newline at end of file
diff --git a/src/keycloak-theme/pages/RegisterUserProfile.tsx b/src/keycloak-theme/pages/RegisterUserProfile.tsx
index 492e82b..e3b1f77 100644
--- a/src/keycloak-theme/pages/RegisterUserProfile.tsx
+++ b/src/keycloak-theme/pages/RegisterUserProfile.tsx
@@ -1,7 +1,7 @@
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
import { useState } from "react";
import { clsx } from "keycloakify/lib/tools/clsx";
-import { UserProfileFormFields } from "./shared/UserProfileCommons";
+import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
import type { PageProps } from "keycloakify/lib/KcProps";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
diff --git a/src/keycloak-theme/pages/Terms.stories.tsx b/src/keycloak-theme/pages/Terms.stories.tsx
index 5f5e238..28ff76c 100644
--- a/src/keycloak-theme/pages/Terms.stories.tsx
+++ b/src/keycloak-theme/pages/Terms.stories.tsx
@@ -1,8 +1,9 @@
-import {ComponentMeta} from '@storybook/react';
+import { ComponentMeta } from '@storybook/react';
import KcApp from '../KcApp';
-import {template} from '../../../.storybook/util'
+import { template } from '../../../.storybook/util'
const bind = template('terms.ftl');
+
export default {
kind: 'Page',
title: 'Theme/Pages/Actions/Terms',
diff --git a/src/keycloak-theme/pages/UpdateUserProfile.stories.tsx b/src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
new file mode 100644
index 0000000..8057558
--- /dev/null
+++ b/src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
@@ -0,0 +1,16 @@
+import { ComponentMeta } from '@storybook/react';
+import KcApp from '../KcApp';
+import { template } from '../../../.storybook/util'
+
+const bind = template('update-user-profile.ftl');
+
+export default {
+ kind: 'Page',
+ title: 'Theme/Pages/Actions/Update User Profile',
+ component: KcApp,
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof KcApp>;
+
+export const Default = bind({})
diff --git a/src/keycloak-theme/pages/UpdateUserProfile.tsx b/src/keycloak-theme/pages/UpdateUserProfile.tsx
new file mode 100644
index 0000000..6bfb1e2
--- /dev/null
+++ b/src/keycloak-theme/pages/UpdateUserProfile.tsx
@@ -0,0 +1,67 @@
+import React, { useState } from "react";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import { UserProfileFormFields } from "keycloakify/lib/pages/shared/UserProfileCommons";
+import type { PageProps } from "keycloakify/lib/KcProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function UpdateUserProfile(props: PageProps<Extract<KcContext, { pageId: "update-user-profile.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { msg, msgStr } = i18n;
+
+ const { url, isAppInitiatedAction } = kcContext;
+
+ const [isFomSubmittable, setIsFomSubmittable] = useState(false);
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ headerNode={msg("loginProfileTitle")}
+ formNode={
+ <form id="kc-update-profile-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post">
+ <UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
+
+ <div className={clsx(kcProps.kcFormGroupClass)}>
+ <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
+ <div className={clsx(kcProps.kcFormOptionsWrapperClass)}></div>
+ </div>
+
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
+ {isAppInitiatedAction ? (
+ <>
+ <input
+ className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)}
+ type="submit"
+ value={msgStr("doSubmit")}
+ />
+ <button
+ className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)}
+ type="submit"
+ name="cancel-aia"
+ value="true"
+ formNoValidate
+ >
+ {msg("doCancel")}
+ </button>
+ </>
+ ) : (
+ <input
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
+ type="submit"
+ defaultValue={msgStr("doSubmit")}
+ disabled={!isFomSubmittable}
+ />
+ )}
+ </div>
+ </div>
+ </form>
+ }
+ />
+ );
+}
diff --git a/src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx b/src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
new file mode 100644
index 0000000..62a84ee
--- /dev/null
+++ b/src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
@@ -0,0 +1,16 @@
+import { ComponentMeta } from '@storybook/react';
+import KcApp from '../KcApp';
+import { template } from '../../../.storybook/util'
+
+const bind = template('webauthn-authenticate.ftl');
+
+export default {
+ kind: 'Page',
+ title: 'Theme/Pages/Login/Webauthn',
+ component: KcApp,
+ parameters: {
+ layout: 'fullscreen',
+ },
+} as ComponentMeta<typeof KcApp>;
+
+export const Default = bind({})
diff --git a/src/keycloak-theme/pages/WebauthnAuthenticate.tsx b/src/keycloak-theme/pages/WebauthnAuthenticate.tsx
new file mode 100644
index 0000000..fd14dc8
--- /dev/null
+++ b/src/keycloak-theme/pages/WebauthnAuthenticate.tsx
@@ -0,0 +1,193 @@
+import React, { useRef, useState } from "react";
+import { clsx } from "keycloakify/lib/tools/clsx";
+import type { MessageKeyBase } from "keycloakify/lib/i18n";
+import { base64url } from "rfc4648";
+import { useConstCallback } from "keycloakify/lib/tools/useConstCallback";
+import type { PageProps } from "keycloakify/lib/KcProps";
+import type { KcContext } from "../kcContext";
+import type { I18n } from "../i18n";
+
+export default function WebauthnAuthenticate(props: PageProps<Extract<KcContext, { pageId: "webauthn-authenticate.ftl" }>, I18n>) {
+ const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
+
+ const { url } = kcContext;
+
+ const { msg, msgStr } = i18n;
+
+ const { authenticators, challenge, shouldDisplayAuthenticators, userVerification, rpId } = kcContext;
+ const createTimeout = Number(kcContext.createTimeout);
+ const isUserIdentified = kcContext.isUserIdentified == "true";
+
+ const webAuthnAuthenticate = useConstCallback(async () => {
+ if (!isUserIdentified) {
+ return;
+ }
+ const allowCredentials = authenticators.authenticators.map(
+ authenticator =>
+ ({
+ id: base64url.parse(authenticator.credentialId, { loose: true }),
+ type: "public-key"
+ } as PublicKeyCredentialDescriptor)
+ );
+ // Check if WebAuthn is supported by this browser
+ if (!window.PublicKeyCredential) {
+ setError(msgStr("webauthn-unsupported-browser-text"));
+ submitForm();
+ return;
+ }
+
+ const publicKey: PublicKeyCredentialRequestOptions = {
+ rpId,
+ challenge: base64url.parse(challenge, { loose: true })
+ };
+
+ if (createTimeout !== 0) {
+ publicKey.timeout = createTimeout * 1000;
+ }
+
+ if (allowCredentials.length) {
+ publicKey.allowCredentials = allowCredentials;
+ }
+
+ if (userVerification !== "not specified") {
+ publicKey.userVerification = userVerification;
+ }
+
+ try {
+ const resultRaw = await navigator.credentials.get({ publicKey });
+ if (!resultRaw || resultRaw.type != "public-key") return;
+ const result = resultRaw as PublicKeyCredential;
+ if (!("authenticatorData" in result.response)) return;
+ const response = result.response as AuthenticatorAssertionResponse;
+ const clientDataJSON = response.clientDataJSON;
+ const authenticatorData = response.authenticatorData;
+ const signature = response.signature;
+
+ setClientDataJSON(base64url.stringify(new Uint8Array(clientDataJSON), { pad: false }));
+ setAuthenticatorData(base64url.stringify(new Uint8Array(authenticatorData), { pad: false }));
+ setSignature(base64url.stringify(new Uint8Array(signature), { pad: false }));
+ setCredentialId(result.id);
+ setUserHandle(base64url.stringify(new Uint8Array(response.userHandle!), { pad: false }));
+ submitForm();
+ } catch (err) {
+ setError(String(err));
+ submitForm();
+ }
+ });
+
+ const webAuthForm = useRef<HTMLFormElement>(null);
+ const submitForm = useConstCallback(() => {
+ webAuthForm.current!.submit();
+ });
+
+ const [clientDataJSON, setClientDataJSON] = useState("");
+ const [authenticatorData, setAuthenticatorData] = useState("");
+ const [signature, setSignature] = useState("");
+ const [credentialId, setCredentialId] = useState("");
+ const [userHandle, setUserHandle] = useState("");
+ const [error, setError] = useState("");
+
+ return (
+ <Template
+ {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
+ headerNode={msg("webauthn-login-title")}
+ formNode={
+ <div id="kc-form-webauthn" className={clsx(kcProps.kcFormClass)}>
+ <form id="webauth" action={url.loginAction} ref={webAuthForm} method="post">
+ <input type="hidden" id="clientDataJSON" name="clientDataJSON" value={clientDataJSON} />
+ <input type="hidden" id="authenticatorData" name="authenticatorData" value={authenticatorData} />
+ <input type="hidden" id="signature" name="signature" value={signature} />
+ <input type="hidden" id="credentialId" name="credentialId" value={credentialId} />
+ <input type="hidden" id="userHandle" name="userHandle" value={userHandle} />
+ <input type="hidden" id="error" name="error" value={error} />
+ </form>
+ <div className={clsx(kcProps.kcFormGroupClass)}>
+ {authenticators &&
+ (() => (
+ <form id="authn_select" className={clsx(kcProps.kcFormClass)}>
+ {authenticators.authenticators.map(authenticator => (
+ <input
+ type="hidden"
+ name="authn_use_chk"
+ value={authenticator.credentialId}
+ key={authenticator.credentialId}
+ />
+ ))}
+ </form>
+ ))()}
+ {authenticators &&
+ shouldDisplayAuthenticators &&
+ (() => (
+ <>
+ {authenticators.authenticators.length > 1 && (
+ <p className={clsx(kcProps.kcSelectAuthListItemTitle)}>{msg("webauthn-available-authenticators")}</p>
+ )}
+ <div className={clsx(kcProps.kcFormClass)}>
+ {authenticators.authenticators.map(authenticator => (
+ <div id="kc-webauthn-authenticator" className={clsx(kcProps.kcSelectAuthListItemClass)}>
+ <div className={clsx(kcProps.kcSelectAuthListItemIconClass)}>
+ <i
+ className={clsx(
+ kcProps[authenticator.transports.iconClass] ?? kcProps.kcWebAuthnDefaultIcon,
+ kcProps.kcSelectAuthListItemIconPropertyClass
+ )}
+ />
+ </div>
+ <div className={clsx(kcProps.kcSelectAuthListItemBodyClass)}>
+ <div
+ id="kc-webauthn-authenticator-label"
+ className={clsx(kcProps.kcSelectAuthListItemHeadingClass)}
+ >
+ {authenticator.label}
+ </div>
+
+ {authenticator.transports && authenticator.transports.displayNameProperties.length && (
+ <div
+ id="kc-webauthn-authenticator-transport"
+ className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}
+ >
+ {authenticator.transports.displayNameProperties.map(
+ (transport: MessageKeyBase, index: number) => (
+ <>
+ <span>{msg(transport)}</span>
+ {index < authenticator.transports.displayNameProperties.length - 1 && (
+ <span>{", "}</span>
+ )}
+ </>
+ )
+ )}
+ </div>
+ )}
+
+ <div className={clsx(kcProps.kcSelectAuthListItemDescriptionClass)}>
+ <span id="kc-webauthn-authenticator-created-label">{msg("webauthn-createdAt-label")}</span>
+ <span id="kc-webauthn-authenticator-created">{authenticator.createdAt}</span>
+ </div>
+ </div>
+ <div className={clsx(kcProps.kcSelectAuthListItemFillClass)} />
+ </div>
+ ))}
+ </div>
+ </>
+ ))()}
+ <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
+ <input
+ id="authenticateWebAuthnButton"
+ type="button"
+ onClick={webAuthnAuthenticate}
+ autoFocus={true}
+ value={msgStr("webauthn-doAuthenticate")}
+ className={clsx(
+ kcProps.kcButtonClass,
+ kcProps.kcButtonPrimaryClass,
+ kcProps.kcButtonBlockClass,
+ kcProps.kcButtonLargeClass
+ )}
+ />
+ </div>
+ </div>
+ </div>
+ }
+ />
+ );
+}