feat(storybook): update formatting; introduce new pages; update keycloakify
This commit is contained in:
@ -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({})
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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:',
|
||||
|
@ -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: 'max.mustermann@mail.com'}})
|
||||
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: 'max.mustermann@mail.com' } })
|
||||
export const WithImmutablePresetUsername = bind({
|
||||
login: {username: 'max.mustermann@mail.com'},
|
||||
login: { username: 'max.mustermann@mail.com' },
|
||||
usernameEditDisabled: true
|
||||
})
|
||||
export const WithSocialProviders = bind({social: {displayInfo: true, providers: socialProviders}})
|
||||
export const WithSocialProviders = bind({ social: { displayInfo: true, providers: socialProviders } })
|
||||
|
@ -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
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -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({})
|
||||
|
||||
|
@ -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({})
|
||||
|
||||
|
@ -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({})
|
||||
|
@ -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({})
|
||||
|
@ -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({})
|
||||
|
@ -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 } })
|
||||
|
@ -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({})
|
||||
|
17
src/keycloak-theme/pages/LoginUsername.stories.tsx
Normal file
17
src/keycloak-theme/pages/LoginUsername.stories.tsx
Normal file
@ -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 } })
|
158
src/keycloak-theme/pages/LoginUsername.tsx
Normal file
158
src/keycloak-theme/pages/LoginUsername.tsx
Normal file
@ -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>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
16
src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
Normal file
16
src/keycloak-theme/pages/LoginVerifyEmail.stories.tsx
Normal file
@ -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({})
|
32
src/keycloak-theme/pages/LoginVerifyEmail.tsx
Normal file
32
src/keycloak-theme/pages/LoginVerifyEmail.tsx
Normal file
@ -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>
|
||||
|
||||
{msg("emailVerifyInstruction3")}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
16
src/keycloak-theme/pages/LogoutConfirm.stories.tsx
Normal file
16
src/keycloak-theme/pages/LogoutConfirm.stories.tsx
Normal file
@ -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({})
|
58
src/keycloak-theme/pages/LogoutConfirm.tsx
Normal file
58
src/keycloak-theme/pages/LogoutConfirm.tsx
Normal file
@ -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>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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({})
|
||||
|
@ -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' })
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
*/
|
@ -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";
|
||||
|
@ -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',
|
||||
|
16
src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
Normal file
16
src/keycloak-theme/pages/UpdateUserProfile.stories.tsx
Normal file
@ -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({})
|
67
src/keycloak-theme/pages/UpdateUserProfile.tsx
Normal file
67
src/keycloak-theme/pages/UpdateUserProfile.tsx
Normal file
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
16
src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
Normal file
16
src/keycloak-theme/pages/WebauthnAuthenticate.stories.tsx
Normal file
@ -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({})
|
193
src/keycloak-theme/pages/WebauthnAuthenticate.tsx
Normal file
193
src/keycloak-theme/pages/WebauthnAuthenticate.tsx
Normal file
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user