diff options
-rw-r--r-- | src/keycloak-theme/pages/LoginPassword.stories.tsx | 16 | ||||
-rw-r--r-- | src/keycloak-theme/pages/LoginPassword.tsx | 86 | ||||
-rw-r--r-- | src/keycloak-theme/pages/LoginResetPassword.stories.tsx | 17 | ||||
-rw-r--r-- | src/keycloak-theme/pages/LoginResetPassword.tsx | 69 | ||||
-rw-r--r-- | src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx | 16 | ||||
-rw-r--r-- | src/keycloak-theme/pages/LoginUpdatePassword.tsx | 117 |
6 files changed, 321 insertions, 0 deletions
diff --git a/src/keycloak-theme/pages/LoginPassword.stories.tsx b/src/keycloak-theme/pages/LoginPassword.stories.tsx new file mode 100644 index 0000000..6a53efa --- /dev/null +++ b/src/keycloak-theme/pages/LoginPassword.stories.tsx @@ -0,0 +1,16 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Password Only', + 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/LoginPassword.tsx b/src/keycloak-theme/pages/LoginPassword.tsx new file mode 100644 index 0000000..ff3798e --- /dev/null +++ b/src/keycloak-theme/pages/LoginPassword.tsx @@ -0,0 +1,86 @@ +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 LoginPassword(props: PageProps<Extract<KcContext, { "pageId": "login-password.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { realm, url, login } = 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; + + formElement.submit(); + }); + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + headerNode={msg("doLogIn")} + formNode={ + <div id="kc-form"> + <div id="kc-form-wrapper"> + <form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post"> + <div className={clsx(kcProps.kcFormGroupClass)}> + <hr /> + <label htmlFor="password" className={clsx(kcProps.kcLabelClass)}> + {msg("password")} + </label> + <input + tabIndex={2} + id="password" + className={clsx(kcProps.kcInputClass)} + name="password" + type="password" + autoFocus={true} + autoComplete="on" + defaultValue={login.password ?? ""} + /> + </div> + <div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}> + <div id="kc-form-options" /> + <div className={clsx(kcProps.kcFormOptionsWrapperClass)}> + {realm.resetPasswordAllowed && ( + <span> + <a tabIndex={5} href={url.loginResetCredentialsUrl}> + {msg("doForgotPassword")} + </a> + </span> + )} + </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> + </div> + } + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginResetPassword.stories.tsx b/src/keycloak-theme/pages/LoginResetPassword.stories.tsx new file mode 100644 index 0000000..5d3aa02 --- /dev/null +++ b/src/keycloak-theme/pages/LoginResetPassword.stories.tsx @@ -0,0 +1,17 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Password Reset', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-reset-password.ftl'); + +export const Default = bind({}) +export const WithEmailAsUsername = bind({realm: {loginWithEmailAllowed: true, registrationEmailAsUsername: true}}) diff --git a/src/keycloak-theme/pages/LoginResetPassword.tsx b/src/keycloak-theme/pages/LoginResetPassword.tsx new file mode 100644 index 0000000..ddeea69 --- /dev/null +++ b/src/keycloak-theme/pages/LoginResetPassword.tsx @@ -0,0 +1,69 @@ +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 LoginResetPassword(props: PageProps<Extract<KcContext, { pageId: "login-reset-password.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { url, realm, auth } = kcContext; + + const { msg, msgStr } = i18n; + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + displayMessage={false} + headerNode={msg("emailForgotTitle")} + formNode={ + <form id="kc-reset-password-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post"> + <div className={clsx(kcProps.kcFormGroupClass)}> + <div className={clsx(kcProps.kcLabelWrapperClass)}> + <label htmlFor="username" className={clsx(kcProps.kcLabelClass)}> + {!realm.loginWithEmailAllowed + ? msg("username") + : !realm.registrationEmailAsUsername + ? msg("usernameOrEmail") + : msg("email")} + </label> + </div> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input + type="text" + id="username" + name="username" + className={clsx(kcProps.kcInputClass)} + autoFocus + defaultValue={auth !== undefined && auth.showUsername ? auth.attemptedUsername : undefined} + /> + </div> + </div> + <div className={clsx(kcProps.kcFormGroupClass, kcProps.kcFormSettingClass)}> + <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> + <div className={clsx(kcProps.kcFormOptionsWrapperClass)}> + <span> + <a href={url.loginUrl}>{msg("backToLogin")}</a> + </span> + </div> + </div> + + <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> + <input + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonPrimaryClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + type="submit" + value={msgStr("doSubmit")} + /> + </div> + </div> + </form> + } + infoNode={msg("emailInstruction")} + /> + ); +}
\ No newline at end of file diff --git a/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx b/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx new file mode 100644 index 0000000..9a68d43 --- /dev/null +++ b/src/keycloak-theme/pages/LoginUpdatePassword.stories.tsx @@ -0,0 +1,16 @@ +import {ComponentMeta} from '@storybook/react'; +import KcApp from '../KcApp'; +import {template} from '../../../.storybook/util' + +export default { + kind: 'Page', + title: 'Theme/Pages/Login/Password Update', + component: KcApp, + parameters: { + layout: 'fullscreen', + }, +} as ComponentMeta<typeof KcApp>; + +const bind = template('login-update-password.ftl'); + +export const Default = bind({}) diff --git a/src/keycloak-theme/pages/LoginUpdatePassword.tsx b/src/keycloak-theme/pages/LoginUpdatePassword.tsx new file mode 100644 index 0000000..d7d375c --- /dev/null +++ b/src/keycloak-theme/pages/LoginUpdatePassword.tsx @@ -0,0 +1,117 @@ +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 LoginUpdatePassword(props: PageProps<Extract<KcContext, { pageId: "login-update-password.ftl" }>, I18n>) { + const { kcContext, i18n, doFetchDefaultThemeResources = true, Template, ...kcProps } = props; + + const { msg, msgStr } = i18n; + + const { url, messagesPerField, isAppInitiatedAction, username } = kcContext; + + return ( + <Template + {...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }} + headerNode={msg("updatePasswordTitle")} + formNode={ + <form id="kc-passwd-update-form" className={clsx(kcProps.kcFormClass)} action={url.loginAction} method="post"> + <input + type="text" + id="username" + name="username" + value={username} + readOnly={true} + autoComplete="username" + style={{ display: "none" }} + /> + <input type="password" id="password" name="password" autoComplete="current-password" style={{ display: "none" }} /> + + <div className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password", kcProps.kcFormGroupErrorClass))}> + <div className={clsx(kcProps.kcLabelWrapperClass)}> + <label htmlFor="password-new" className={clsx(kcProps.kcLabelClass)}> + {msg("passwordNew")} + </label> + </div> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input + type="password" + id="password-new" + name="password-new" + autoFocus + autoComplete="new-password" + className={clsx(kcProps.kcInputClass)} + /> + </div> + </div> + + <div + className={clsx(kcProps.kcFormGroupClass, messagesPerField.printIfExists("password-confirm", kcProps.kcFormGroupErrorClass))} + > + <div className={clsx(kcProps.kcLabelWrapperClass)}> + <label htmlFor="password-confirm" className={clsx(kcProps.kcLabelClass)}> + {msg("passwordConfirm")} + </label> + </div> + <div className={clsx(kcProps.kcInputWrapperClass)}> + <input + type="password" + id="password-confirm" + name="password-confirm" + autoComplete="new-password" + className={clsx(kcProps.kcInputClass)} + /> + </div> + </div> + + <div className={clsx(kcProps.kcFormGroupClass)}> + <div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}> + <div className={clsx(kcProps.kcFormOptionsWrapperClass)}> + {isAppInitiatedAction && ( + <div className="checkbox"> + <label> + <input type="checkbox" id="logout-sessions" name="logout-sessions" value="on" checked /> + {msgStr("logoutOtherSessions")} + </label> + </div> + )} + </div> + </div> + + <div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}> + {isAppInitiatedAction ? ( + <> + <input + className={clsx(kcProps.kcButtonClass, kcProps.kcButtonPrimaryClass, kcProps.kcButtonLargeClass)} + type="submit" + defaultValue={msgStr("doSubmit")} + /> + <button + className={clsx(kcProps.kcButtonClass, kcProps.kcButtonDefaultClass, kcProps.kcButtonLargeClass)} + type="submit" + name="cancel-aia" + value="true" + > + {msg("doCancel")} + </button> + </> + ) : ( + <input + className={clsx( + kcProps.kcButtonClass, + kcProps.kcButtonPrimaryClass, + kcProps.kcButtonBlockClass, + kcProps.kcButtonLargeClass + )} + type="submit" + defaultValue={msgStr("doSubmit")} + /> + )} + </div> + </div> + </form> + } + /> + ); +}
\ No newline at end of file |