Login page implemented
This commit is contained in:
parent
7f6ab1d391
commit
43c412405a
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloak-react-theming",
|
"name": "keycloak-react-theming",
|
||||||
"version": "0.0.25",
|
"version": "0.0.26",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
|
|
||||||
export const ftlValuesGlobalName = "keycloakPagesContext";
|
export const ftlValuesGlobalName = "kcContext";
|
@ -1,300 +0,0 @@
|
|||||||
|
|
||||||
import { useState, memo } from "react";
|
|
||||||
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
|
|
||||||
import { Template, defaultKcTemplateProperties } from "./Template";
|
|
||||||
import type { KcTemplateProperties, KcClasses } from "./Template";
|
|
||||||
import { assert } from "evt/tools/typeSafety/assert";
|
|
||||||
import { keycloakPagesContext } from "./keycloakFtlValues";
|
|
||||||
import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation";
|
|
||||||
import { cx } from "tss-react";
|
|
||||||
import { useConstCallback } from "powerhooks";
|
|
||||||
|
|
||||||
export type KcLoginPageProperties = KcTemplateProperties & KcClasses<
|
|
||||||
"kcLogoLink" |
|
|
||||||
"kcLogoClass" |
|
|
||||||
"kcContainerClass" |
|
|
||||||
"kcContentClass" |
|
|
||||||
"kcFeedbackAreaClass" |
|
|
||||||
"kcLocaleClass" |
|
|
||||||
"kcAlertIconClasserror" |
|
|
||||||
"kcFormAreaClass" |
|
|
||||||
"kcFormSocialAccountListClass" |
|
|
||||||
"kcFormSocialAccountDoubleListClass" |
|
|
||||||
"kcFormSocialAccountListLinkClass" |
|
|
||||||
"kcWebAuthnKeyIcon" |
|
|
||||||
"kcFormClass" |
|
|
||||||
"kcFormGroupErrorClass" |
|
|
||||||
"kcLabelClass" |
|
|
||||||
"kcInputClass" |
|
|
||||||
"kcInputWrapperClass" |
|
|
||||||
"kcFormOptionsClass" |
|
|
||||||
"kcFormButtonsClass" |
|
|
||||||
"kcFormSettingClass" |
|
|
||||||
"kcTextareaClass" |
|
|
||||||
"kcInfoAreaClass" |
|
|
||||||
"kcButtonClass" |
|
|
||||||
"kcButtonPrimaryClass" |
|
|
||||||
"kcButtonDefaultClass" |
|
|
||||||
"kcButtonLargeClass" |
|
|
||||||
"kcButtonBlockClass" |
|
|
||||||
"kcInputLargeClass" |
|
|
||||||
"kcSrOnlyClass" |
|
|
||||||
"kcSelectAuthListClass" |
|
|
||||||
"kcSelectAuthListItemClass" |
|
|
||||||
"kcSelectAuthListItemInfoClass" |
|
|
||||||
"kcSelectAuthListItemLeftClass" |
|
|
||||||
"kcSelectAuthListItemBodyClass" |
|
|
||||||
"kcSelectAuthListItemDescriptionClass" |
|
|
||||||
"kcSelectAuthListItemHeadingClass" |
|
|
||||||
"kcSelectAuthListItemHelpTextClass" |
|
|
||||||
"kcAuthenticatorDefaultClass" |
|
|
||||||
"kcAuthenticatorPasswordClass" |
|
|
||||||
"kcAuthenticatorOTPClass" |
|
|
||||||
"kcAuthenticatorWebAuthnClass" |
|
|
||||||
"kcAuthenticatorWebAuthnPasswordlessClass" |
|
|
||||||
"kcSelectOTPListClass" |
|
|
||||||
"kcSelectOTPListItemClass" |
|
|
||||||
"kcAuthenticatorOtpCircleClass" |
|
|
||||||
"kcSelectOTPItemHeadingClass" |
|
|
||||||
"kcFormOptionsWrapperClass"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const defaultKcLoginPageProperties: KcLoginPageProperties = {
|
|
||||||
...defaultKcTemplateProperties,
|
|
||||||
"kcLogoLink": "http://www.keycloak.org",
|
|
||||||
"kcLogoClass": "login-pf-brand",
|
|
||||||
"kcContainerClass": "container-fluid",
|
|
||||||
"kcContentClass": "col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
|
|
||||||
"kcFeedbackAreaClass": "col-md-12",
|
|
||||||
"kcLocaleClass": "col-xs-12 col-sm-1",
|
|
||||||
"kcAlertIconClasserror": "pficon pficon-error-circle-o",
|
|
||||||
|
|
||||||
"kcFormAreaClass": "col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
|
|
||||||
"kcFormSocialAccountListClass": "login-pf-social list-unstyled login-pf-social-all",
|
|
||||||
"kcFormSocialAccountDoubleListClass": "login-pf-social-double-col",
|
|
||||||
"kcFormSocialAccountListLinkClass": "login-pf-social-link",
|
|
||||||
"kcWebAuthnKeyIcon": "pficon pficon-key",
|
|
||||||
|
|
||||||
"kcFormClass": "form-horizontal",
|
|
||||||
"kcFormGroupErrorClass": "has-error",
|
|
||||||
"kcLabelClass": "control-label",
|
|
||||||
"kcInputClass": "form-control",
|
|
||||||
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
|
||||||
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
|
||||||
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
|
||||||
"kcFormSettingClass": "login-pf-settings",
|
|
||||||
"kcTextareaClass": "form-control",
|
|
||||||
|
|
||||||
"kcInfoAreaClass": "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
|
|
||||||
|
|
||||||
// css classes for form buttons main class used for all buttons
|
|
||||||
"kcButtonClass": "btn",
|
|
||||||
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
|
|
||||||
"kcButtonPrimaryClass": "btn-primary",
|
|
||||||
"kcButtonDefaultClass": "btn-default",
|
|
||||||
// classes defining size of the button
|
|
||||||
"kcButtonLargeClass": "btn-lg",
|
|
||||||
"kcButtonBlockClass": "btn-block",
|
|
||||||
|
|
||||||
// css classes for input
|
|
||||||
"kcInputLargeClass": "input-lg",
|
|
||||||
|
|
||||||
// css classes for form accessability
|
|
||||||
"kcSrOnlyClass": "sr-only",
|
|
||||||
|
|
||||||
// css classes for select-authenticator form
|
|
||||||
"kcSelectAuthListClass": "list-group list-view-pf",
|
|
||||||
"kcSelectAuthListItemClass": "list-group-item list-view-pf-stacked",
|
|
||||||
"kcSelectAuthListItemInfoClass": "list-view-pf-main-info",
|
|
||||||
"kcSelectAuthListItemLeftClass": "list-view-pf-left",
|
|
||||||
"kcSelectAuthListItemBodyClass": "list-view-pf-body",
|
|
||||||
"kcSelectAuthListItemDescriptionClass": "list-view-pf-description",
|
|
||||||
"kcSelectAuthListItemHeadingClass": "list-group-item-heading",
|
|
||||||
"kcSelectAuthListItemHelpTextClass": "list-group-item-text",
|
|
||||||
|
|
||||||
// css classes for the authenticators
|
|
||||||
"kcAuthenticatorDefaultClass": "fa list-view-pf-icon-lg",
|
|
||||||
"kcAuthenticatorPasswordClass": "fa fa-unlock list-view-pf-icon-lg",
|
|
||||||
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",
|
|
||||||
"kcAuthenticatorWebAuthnClass": "fa fa-key list-view-pf-icon-lg",
|
|
||||||
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
|
|
||||||
|
|
||||||
//css classes for the OTP Login Form
|
|
||||||
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select",
|
|
||||||
"kcSelectOTPListItemClass": "card-pf-body card-pf-top-element",
|
|
||||||
"kcAuthenticatorOtpCircleClass": "fa fa-mobile card-pf-icon-circle",
|
|
||||||
"kcSelectOTPItemHeadingClass": "card-pf-title text-center"
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/** Tu use if you don't want any default */
|
|
||||||
export const allClearKcLoginPageProperties =
|
|
||||||
allPropertiesValuesToUndefined(defaultKcLoginPageProperties);
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
properties?: KcLoginPageProperties;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LoginPage = memo((props: Props) => {
|
|
||||||
|
|
||||||
const { properties = {} } = props;
|
|
||||||
|
|
||||||
const { t, tStr } = useKeycloakThemeTranslation();
|
|
||||||
|
|
||||||
Object.assign(properties, defaultKcLoginPageProperties);
|
|
||||||
|
|
||||||
const [{
|
|
||||||
social, realm, url,
|
|
||||||
usernameEditDisabled, login,
|
|
||||||
auth, registrationDisabled
|
|
||||||
}] = useState(() => {
|
|
||||||
|
|
||||||
assert(keycloakPagesContext !== undefined);
|
|
||||||
|
|
||||||
return keycloakPagesContext;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
|
||||||
|
|
||||||
const onSubmit = useConstCallback(() =>
|
|
||||||
(setIsLoginButtonDisabled(true), true)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Template
|
|
||||||
displayInfo={social.displayInfo}
|
|
||||||
displayWide={realm.password && social.providers !== undefined}
|
|
||||||
properties={properties}
|
|
||||||
headerNode={t("doLogIn")}
|
|
||||||
showUsernameNode={null}
|
|
||||||
formNode={
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="kc-form"
|
|
||||||
className={cx(realm.password && social.providers !== undefined && properties.kcContentWrapperClass)}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="kc-form-wrapper"
|
|
||||||
className={cx(realm.password && social.providers && [properties.kcFormSocialAccountContentClass, properties.kcFormSocialAccountClass])}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
realm.password &&
|
|
||||||
(
|
|
||||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
|
||||||
<div className={cx(properties.kcFormGroupClass)}>
|
|
||||||
<label htmlFor="username" className={cx(properties.kcLabelClass)}>
|
|
||||||
{
|
|
||||||
!realm.loginWithEmailAllowed ?
|
|
||||||
t("username")
|
|
||||||
:
|
|
||||||
(
|
|
||||||
!realm.registrationEmailAsUsername ?
|
|
||||||
t("usernameOrEmail") :
|
|
||||||
t("email")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
tabIndex={1}
|
|
||||||
id="username"
|
|
||||||
className={cx(properties.kcInputClass)}
|
|
||||||
name="username"
|
|
||||||
value={login.username ?? ''}
|
|
||||||
type="text"
|
|
||||||
{...(usernameEditDisabled ? { "disabled": true } : { "autofocus": true, "autocomplete": "off" })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(properties.kcFormGroupClass)}>
|
|
||||||
<label htmlFor="password" className={cx(properties.kcLabelClass)}>
|
|
||||||
{t("password")}
|
|
||||||
</label>
|
|
||||||
<input tabIndex={2} id="password" className={cx(properties.kcInputClass)} name="password" type="password" autoComplete="off" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={cx(properties.kcFormGroupClass, properties.kcFormSettingClass)}>
|
|
||||||
<div id="kc-form-options">
|
|
||||||
{
|
|
||||||
(
|
|
||||||
realm.rememberMe &&
|
|
||||||
!usernameEditDisabled
|
|
||||||
) &&
|
|
||||||
<div className="checkbox">
|
|
||||||
<label>
|
|
||||||
<input tabIndex={3} id="rememberMe" name="rememberMe" type="checkbox" {...(login.rememberMe ? { "checked": true } : {})}> {t("rememberMe")}</input>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className={cx(properties.kcFormOptionsWrapperClass)}>
|
|
||||||
{
|
|
||||||
realm.resetPasswordAllowed &&
|
|
||||||
<span>
|
|
||||||
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{t("doForgotPassword")}</a>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="kc-form-buttons" className={cx(properties.kcFormGroupClass)}>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
id="id-hidden-input"
|
|
||||||
name="credentialId"
|
|
||||||
{...(auth?.selectedCredential !== undefined ? { "value": auth.selectedCredential } : {})}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
tabIndex={4}
|
|
||||||
className={cx(properties.kcButtonClass, properties.kcButtonPrimaryClass, properties.kcButtonBlockClass, properties.kcButtonLargeClass)} name="login" id="kc-login" type="submit"
|
|
||||||
value={tStr("doLogIn")}
|
|
||||||
disabled={isLoginButtonDisabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{
|
|
||||||
(realm.password && social.providers !== undefined) &&
|
|
||||||
<div id="kc-social-providers" className={cx(properties.kcFormSocialAccountContentClass, properties.kcFormSocialAccountClass)}>
|
|
||||||
<ul className={cx(properties.kcFormSocialAccountListClass, social.providers.length > 4 && properties.kcFormSocialAccountDoubleListClass)}>
|
|
||||||
{
|
|
||||||
social.providers.map(p =>
|
|
||||||
<li className={cx(properties.kcFormSocialAccountListLinkClass)}>
|
|
||||||
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
|
|
||||||
<span>{p.displayName}</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
displayInfoNode={
|
|
||||||
(
|
|
||||||
realm.password &&
|
|
||||||
realm.resetPasswordAllowed &&
|
|
||||||
!registrationDisabled
|
|
||||||
) &&
|
|
||||||
<div id="kc-registration">
|
|
||||||
<span>
|
|
||||||
{t("noAccount")}
|
|
||||||
<a tabIndex={6} href={url.registrationUrl}>
|
|
||||||
{t("doRegister")}
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
190
src/lib/components/KcProperties.ts
Normal file
190
src/lib/components/KcProperties.ts
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
|
||||||
|
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
|
||||||
|
|
||||||
|
/** Class names can be provided as an array or separated by whitespace */
|
||||||
|
export type KcClasses<CssClasses extends string> = { [key in CssClasses]?: string[] | string };
|
||||||
|
|
||||||
|
export type KcTemplateCssClasses =
|
||||||
|
"kcLoginClass" |
|
||||||
|
"kcHeaderClass" |
|
||||||
|
"kcHeaderWrapperClass" |
|
||||||
|
"kcFormCardClass" |
|
||||||
|
"kcFormCardAccountClass" |
|
||||||
|
"kcFormHeaderClass" |
|
||||||
|
"kcLocaleWrapperClass" |
|
||||||
|
"kcContentWrapperClass" |
|
||||||
|
"kcLabelWrapperClass" |
|
||||||
|
"kcContentWrapperClass" |
|
||||||
|
"kcLabelWrapperClass" |
|
||||||
|
"kcFormGroupClass" |
|
||||||
|
"kcResetFlowIcon" |
|
||||||
|
"kcResetFlowIcon" |
|
||||||
|
"kcFeedbackSuccessIcon" |
|
||||||
|
"kcFeedbackWarningIcon" |
|
||||||
|
"kcFeedbackErrorIcon" |
|
||||||
|
"kcFeedbackInfoIcon" |
|
||||||
|
"kcContentWrapperClass" |
|
||||||
|
"kcFormSocialAccountContentClass" |
|
||||||
|
"kcFormSocialAccountClass" |
|
||||||
|
"kcSignUpClass" |
|
||||||
|
"kcInfoAreaWrapperClass"
|
||||||
|
;
|
||||||
|
|
||||||
|
export type KcTemplateProperties = {
|
||||||
|
stylesCommon?: string[];
|
||||||
|
styles?: string[];
|
||||||
|
scripts?: string[];
|
||||||
|
} & KcClasses<KcTemplateCssClasses>;
|
||||||
|
|
||||||
|
export const defaultKcTemplateProperties: KcTemplateProperties = {
|
||||||
|
"styles": ["css/login.css"],
|
||||||
|
"stylesCommon": [
|
||||||
|
...[".min.css", "-additions.min.css"]
|
||||||
|
.map(end => `node_modules/patternfly/dist/css/patternfly${end}`),
|
||||||
|
"lib/zocial/zocial.css"
|
||||||
|
],
|
||||||
|
"kcLoginClass": "login-pf-page",
|
||||||
|
"kcContentWrapperClass": "row",
|
||||||
|
"kcHeaderClass": "login-pf-page-header",
|
||||||
|
"kcFormCardClass": "card-pf",
|
||||||
|
"kcFormCardAccountClass": "login-pf-accounts",
|
||||||
|
"kcFormSocialAccountClass": "login-pf-social-section",
|
||||||
|
"kcFormSocialAccountContentClass": "col-xs-12 col-sm-6",
|
||||||
|
"kcFormHeaderClass": "login-pf-header",
|
||||||
|
"kcFeedbackErrorIcon": "pficon pficon-error-circle-o",
|
||||||
|
"kcFeedbackWarningIcon": "pficon pficon-warning-triangle-o",
|
||||||
|
"kcFeedbackSuccessIcon": "pficon pficon-ok",
|
||||||
|
"kcFeedbackInfoIcon": "pficon pficon-info",
|
||||||
|
"kcResetFlowIcon": "pficon pficon-arrow fa-2x",
|
||||||
|
"kcFormGroupClass": "form-group",
|
||||||
|
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||||
|
"kcSignUpClass": "login-pf-sighup"
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Tu use if you don't want any default */
|
||||||
|
export const allClearKcTemplateProperties =
|
||||||
|
allPropertiesValuesToUndefined(defaultKcTemplateProperties);
|
||||||
|
|
||||||
|
export type KcPagesProperties = KcClasses<
|
||||||
|
KcTemplateCssClasses |
|
||||||
|
"kcLogoLink" |
|
||||||
|
"kcLogoClass" |
|
||||||
|
"kcContainerClass" |
|
||||||
|
"kcContentClass" |
|
||||||
|
"kcFeedbackAreaClass" |
|
||||||
|
"kcLocaleClass" |
|
||||||
|
"kcAlertIconClasserror" |
|
||||||
|
"kcFormAreaClass" |
|
||||||
|
"kcFormSocialAccountListClass" |
|
||||||
|
"kcFormSocialAccountDoubleListClass" |
|
||||||
|
"kcFormSocialAccountListLinkClass" |
|
||||||
|
"kcWebAuthnKeyIcon" |
|
||||||
|
"kcFormClass" |
|
||||||
|
"kcFormGroupErrorClass" |
|
||||||
|
"kcLabelClass" |
|
||||||
|
"kcInputClass" |
|
||||||
|
"kcInputWrapperClass" |
|
||||||
|
"kcFormOptionsClass" |
|
||||||
|
"kcFormButtonsClass" |
|
||||||
|
"kcFormSettingClass" |
|
||||||
|
"kcTextareaClass" |
|
||||||
|
"kcInfoAreaClass" |
|
||||||
|
"kcButtonClass" |
|
||||||
|
"kcButtonPrimaryClass" |
|
||||||
|
"kcButtonDefaultClass" |
|
||||||
|
"kcButtonLargeClass" |
|
||||||
|
"kcButtonBlockClass" |
|
||||||
|
"kcInputLargeClass" |
|
||||||
|
"kcSrOnlyClass" |
|
||||||
|
"kcSelectAuthListClass" |
|
||||||
|
"kcSelectAuthListItemClass" |
|
||||||
|
"kcSelectAuthListItemInfoClass" |
|
||||||
|
"kcSelectAuthListItemLeftClass" |
|
||||||
|
"kcSelectAuthListItemBodyClass" |
|
||||||
|
"kcSelectAuthListItemDescriptionClass" |
|
||||||
|
"kcSelectAuthListItemHeadingClass" |
|
||||||
|
"kcSelectAuthListItemHelpTextClass" |
|
||||||
|
"kcAuthenticatorDefaultClass" |
|
||||||
|
"kcAuthenticatorPasswordClass" |
|
||||||
|
"kcAuthenticatorOTPClass" |
|
||||||
|
"kcAuthenticatorWebAuthnClass" |
|
||||||
|
"kcAuthenticatorWebAuthnPasswordlessClass" |
|
||||||
|
"kcSelectOTPListClass" |
|
||||||
|
"kcSelectOTPListItemClass" |
|
||||||
|
"kcAuthenticatorOtpCircleClass" |
|
||||||
|
"kcSelectOTPItemHeadingClass" |
|
||||||
|
"kcFormOptionsWrapperClass"
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const defaultKcPagesProperties: KcPagesProperties = {
|
||||||
|
...defaultKcTemplateProperties,
|
||||||
|
"kcLogoLink": "http://www.keycloak.org",
|
||||||
|
"kcLogoClass": "login-pf-brand",
|
||||||
|
"kcContainerClass": "container-fluid",
|
||||||
|
"kcContentClass": "col-sm-8 col-sm-offset-2 col-md-6 col-md-offset-3 col-lg-6 col-lg-offset-3",
|
||||||
|
"kcFeedbackAreaClass": "col-md-12",
|
||||||
|
"kcLocaleClass": "col-xs-12 col-sm-1",
|
||||||
|
"kcAlertIconClasserror": "pficon pficon-error-circle-o",
|
||||||
|
|
||||||
|
"kcFormAreaClass": "col-sm-10 col-sm-offset-1 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-2",
|
||||||
|
"kcFormSocialAccountListClass": "login-pf-social list-unstyled login-pf-social-all",
|
||||||
|
"kcFormSocialAccountDoubleListClass": "login-pf-social-double-col",
|
||||||
|
"kcFormSocialAccountListLinkClass": "login-pf-social-link",
|
||||||
|
"kcWebAuthnKeyIcon": "pficon pficon-key",
|
||||||
|
|
||||||
|
"kcFormClass": "form-horizontal",
|
||||||
|
"kcFormGroupErrorClass": "has-error",
|
||||||
|
"kcLabelClass": "control-label",
|
||||||
|
"kcInputClass": "form-control",
|
||||||
|
"kcInputWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||||
|
"kcFormOptionsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||||
|
"kcFormButtonsClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
||||||
|
"kcFormSettingClass": "login-pf-settings",
|
||||||
|
"kcTextareaClass": "form-control",
|
||||||
|
|
||||||
|
"kcInfoAreaClass": "col-xs-12 col-sm-4 col-md-4 col-lg-5 details",
|
||||||
|
|
||||||
|
// css classes for form buttons main class used for all buttons
|
||||||
|
"kcButtonClass": "btn",
|
||||||
|
// classes defining priority of the button - primary or default (there is typically only one priority button for the form)
|
||||||
|
"kcButtonPrimaryClass": "btn-primary",
|
||||||
|
"kcButtonDefaultClass": "btn-default",
|
||||||
|
// classes defining size of the button
|
||||||
|
"kcButtonLargeClass": "btn-lg",
|
||||||
|
"kcButtonBlockClass": "btn-block",
|
||||||
|
|
||||||
|
// css classes for input
|
||||||
|
"kcInputLargeClass": "input-lg",
|
||||||
|
|
||||||
|
// css classes for form accessability
|
||||||
|
"kcSrOnlyClass": "sr-only",
|
||||||
|
|
||||||
|
// css classes for select-authenticator form
|
||||||
|
"kcSelectAuthListClass": "list-group list-view-pf",
|
||||||
|
"kcSelectAuthListItemClass": "list-group-item list-view-pf-stacked",
|
||||||
|
"kcSelectAuthListItemInfoClass": "list-view-pf-main-info",
|
||||||
|
"kcSelectAuthListItemLeftClass": "list-view-pf-left",
|
||||||
|
"kcSelectAuthListItemBodyClass": "list-view-pf-body",
|
||||||
|
"kcSelectAuthListItemDescriptionClass": "list-view-pf-description",
|
||||||
|
"kcSelectAuthListItemHeadingClass": "list-group-item-heading",
|
||||||
|
"kcSelectAuthListItemHelpTextClass": "list-group-item-text",
|
||||||
|
|
||||||
|
// css classes for the authenticators
|
||||||
|
"kcAuthenticatorDefaultClass": "fa list-view-pf-icon-lg",
|
||||||
|
"kcAuthenticatorPasswordClass": "fa fa-unlock list-view-pf-icon-lg",
|
||||||
|
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",
|
||||||
|
"kcAuthenticatorWebAuthnClass": "fa fa-key list-view-pf-icon-lg",
|
||||||
|
"kcAuthenticatorWebAuthnPasswordlessClass": "fa fa-key list-view-pf-icon-lg",
|
||||||
|
|
||||||
|
//css classes for the OTP Login Form
|
||||||
|
"kcSelectOTPListClass": "card-pf card-pf-view card-pf-view-select card-pf-view-single-select",
|
||||||
|
"kcSelectOTPListItemClass": "card-pf-body card-pf-top-element",
|
||||||
|
"kcAuthenticatorOtpCircleClass": "fa fa-mobile card-pf-icon-circle",
|
||||||
|
"kcSelectOTPItemHeadingClass": "card-pf-title text-center"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/** Tu use if you don't want any default */
|
||||||
|
export const allClearKcLoginPageProperties =
|
||||||
|
allPropertiesValuesToUndefined(defaultKcPagesProperties);
|
171
src/lib/components/Login.tsx
Normal file
171
src/lib/components/Login.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
|
||||||
|
import { useState, memo } from "react";
|
||||||
|
import { Template } from "./Template";
|
||||||
|
import type { KcPagesProperties } from "./KcProperties";
|
||||||
|
import { defaultKcPagesProperties } from "./KcProperties";
|
||||||
|
import { assert } from "evt/tools/typeSafety/assert";
|
||||||
|
import { kcContext } from "../kcContext";
|
||||||
|
import { useKcTranslation } from "../i18n/useKcTranslation";
|
||||||
|
import { cx } from "tss-react";
|
||||||
|
import { useConstCallback } from "powerhooks";
|
||||||
|
|
||||||
|
export type LoginProps = {
|
||||||
|
kcProperties?: KcPagesProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Login = memo((props: LoginProps) => {
|
||||||
|
|
||||||
|
const { kcProperties = {} } = props;
|
||||||
|
|
||||||
|
const { t, tStr } = useKcTranslation();
|
||||||
|
|
||||||
|
Object.assign(kcProperties, defaultKcPagesProperties);
|
||||||
|
|
||||||
|
const [{
|
||||||
|
social, realm, url,
|
||||||
|
usernameEditDisabled, login,
|
||||||
|
auth, registrationDisabled
|
||||||
|
}] = useState(() => (
|
||||||
|
assert(
|
||||||
|
kcContext !== undefined,
|
||||||
|
"App is currently being served by keycloak"
|
||||||
|
),
|
||||||
|
kcContext
|
||||||
|
));
|
||||||
|
|
||||||
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||||
|
|
||||||
|
const onSubmit = useConstCallback(() =>
|
||||||
|
(setIsLoginButtonDisabled(true), true)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Template
|
||||||
|
displayInfo={social.displayInfo}
|
||||||
|
displayWide={realm.password && social.providers !== undefined}
|
||||||
|
kcProperties={kcProperties}
|
||||||
|
headerNode={t("doLogIn")}
|
||||||
|
showUsernameNode={null}
|
||||||
|
formNode={
|
||||||
|
<div
|
||||||
|
id="kc-form"
|
||||||
|
className={cx(realm.password && social.providers !== undefined && kcProperties.kcContentWrapperClass)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="kc-form-wrapper"
|
||||||
|
className={cx(realm.password && social.providers && [kcProperties.kcFormSocialAccountContentClass, kcProperties.kcFormSocialAccountClass])}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
realm.password &&
|
||||||
|
(
|
||||||
|
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||||
|
<div className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
|
<label htmlFor="username" className={cx(kcProperties.kcLabelClass)}>
|
||||||
|
{
|
||||||
|
!realm.loginWithEmailAllowed ?
|
||||||
|
t("username")
|
||||||
|
:
|
||||||
|
(
|
||||||
|
!realm.registrationEmailAsUsername ?
|
||||||
|
t("usernameOrEmail") :
|
||||||
|
t("email")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
tabIndex={1}
|
||||||
|
id="username"
|
||||||
|
className={cx(kcProperties.kcInputClass)}
|
||||||
|
name="username"
|
||||||
|
value={login.username ?? ''}
|
||||||
|
type="text"
|
||||||
|
{...(usernameEditDisabled ? { "disabled": true } : { "autofocus": true, "autocomplete": "off" })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
|
<label htmlFor="password" className={cx(kcProperties.kcLabelClass)}>
|
||||||
|
{t("password")}
|
||||||
|
</label>
|
||||||
|
<input tabIndex={2} id="password" className={cx(kcProperties.kcInputClass)} name="password" type="password" autoComplete="off" />
|
||||||
|
</div>
|
||||||
|
<div className={cx(kcProperties.kcFormGroupClass, kcProperties.kcFormSettingClass)}>
|
||||||
|
<div id="kc-form-options">
|
||||||
|
{
|
||||||
|
(
|
||||||
|
realm.rememberMe &&
|
||||||
|
!usernameEditDisabled
|
||||||
|
) &&
|
||||||
|
<div className="checkbox">
|
||||||
|
<label>
|
||||||
|
<input tabIndex={3} id="rememberMe" name="rememberMe" type="checkbox" {...(login.rememberMe ? { "checked": true } : {})}> {t("rememberMe")}</input>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className={cx(kcProperties.kcFormOptionsWrapperClass)}>
|
||||||
|
{
|
||||||
|
realm.resetPasswordAllowed &&
|
||||||
|
<span>
|
||||||
|
<a tabIndex={5} href={url.loginResetCredentialsUrl}>{t("doForgotPassword")}</a>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div id="kc-form-buttons" className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
id="id-hidden-input"
|
||||||
|
name="credentialId"
|
||||||
|
{...(auth?.selectedCredential !== undefined ? { "value": auth.selectedCredential } : {})}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
tabIndex={4}
|
||||||
|
className={cx(kcProperties.kcButtonClass, kcProperties.kcButtonPrimaryClass, kcProperties.kcButtonBlockClass, kcProperties.kcButtonLargeClass)} name="login" id="kc-login" type="submit"
|
||||||
|
value={tStr("doLogIn")}
|
||||||
|
disabled={isLoginButtonDisabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
(realm.password && social.providers !== undefined) &&
|
||||||
|
<div id="kc-social-providers" className={cx(kcProperties.kcFormSocialAccountContentClass, kcProperties.kcFormSocialAccountClass)}>
|
||||||
|
<ul className={cx(kcProperties.kcFormSocialAccountListClass, social.providers.length > 4 && kcProperties.kcFormSocialAccountDoubleListClass)}>
|
||||||
|
{
|
||||||
|
social.providers.map(p =>
|
||||||
|
<li className={cx(kcProperties.kcFormSocialAccountListLinkClass)}>
|
||||||
|
<a href={p.loginUrl} id={`zocial-${p.alias}`} className={cx("zocial", p.providerId)}>
|
||||||
|
<span>{p.displayName}</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
displayInfoNode={
|
||||||
|
(
|
||||||
|
realm.password &&
|
||||||
|
realm.resetPasswordAllowed &&
|
||||||
|
!registrationDisabled
|
||||||
|
) &&
|
||||||
|
<div id="kc-registration">
|
||||||
|
<span>
|
||||||
|
{t("noAccount")}
|
||||||
|
<a tabIndex={6} href={url.registrationUrl}>
|
||||||
|
{t("doRegister")}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
@ -1,95 +1,36 @@
|
|||||||
|
|
||||||
import { useState, useEffect, memo } from "react";
|
import { useState, useEffect, memo } from "react";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation";
|
import { useKcTranslation } from "../i18n/useKcTranslation";
|
||||||
import { keycloakPagesContext } from "./keycloakFtlValues";
|
import { kcContext } from "../kcContext";
|
||||||
import { assert } from "evt/tools/typeSafety/assert";
|
import { assert } from "evt/tools/typeSafety/assert";
|
||||||
import { cx } from "tss-react";
|
import { cx } from "tss-react";
|
||||||
import { useKeycloakLanguage, AvailableLanguages } from "./i18n/useKeycloakLanguage";
|
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
|
||||||
import { getLanguageLabel } from "./i18n/getLanguageLabel";
|
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
|
||||||
|
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
|
||||||
import { useCallbackFactory } from "powerhooks";
|
import { useCallbackFactory } from "powerhooks";
|
||||||
import { appendLinkInHead } from "./tools/appendLinkInHead";
|
import { appendLinkInHead } from "../tools/appendLinkInHead";
|
||||||
import { appendScriptInHead } from "./tools/appendScriptInHead";
|
import { appendScriptInHead } from "../tools/appendScriptInHead";
|
||||||
import { join as pathJoin } from "path";
|
import { join as pathJoin } from "path";
|
||||||
import { useConstCallback } from "powerhooks";
|
import { useConstCallback } from "powerhooks";
|
||||||
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
|
import type { KcTemplateProperties } from "./KcProperties";
|
||||||
|
import { defaultKcTemplateProperties } from "./KcProperties";
|
||||||
|
|
||||||
export type Props = {
|
export type TemplateProps = {
|
||||||
|
kcProperties: KcTemplateProperties;
|
||||||
displayInfo?: boolean;
|
displayInfo?: boolean;
|
||||||
displayMessage?: boolean;
|
displayMessage?: boolean;
|
||||||
displayRequiredFields?: boolean;
|
displayRequiredFields?: boolean;
|
||||||
displayWide?: boolean;
|
displayWide?: boolean;
|
||||||
showAnotherWayIfPresent?: boolean;
|
showAnotherWayIfPresent?: boolean;
|
||||||
properties: KcTemplateProperties;
|
|
||||||
headerNode: ReactNode;
|
headerNode: ReactNode;
|
||||||
showUsernameNode: ReactNode;
|
showUsernameNode: ReactNode;
|
||||||
formNode: ReactNode;
|
formNode: ReactNode;
|
||||||
displayInfoNode: ReactNode;
|
displayInfoNode: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Class names can be provided as an array or separated by whitespace */
|
|
||||||
export type KcClasses<T extends string> = { [key in T]?: string[] | string };
|
|
||||||
|
|
||||||
export type KcTemplateProperties = {
|
export const Template = memo((props: TemplateProps) => {
|
||||||
stylesCommon?: string[];
|
|
||||||
styles?: string[];
|
|
||||||
scripts?: string[];
|
|
||||||
} & KcClasses<
|
|
||||||
"kcLoginClass" |
|
|
||||||
"kcHeaderClass" |
|
|
||||||
"kcHeaderWrapperClass" |
|
|
||||||
"kcFormCardClass" |
|
|
||||||
"kcFormCardAccountClass" |
|
|
||||||
"kcFormHeaderClass" |
|
|
||||||
"kcLocaleWrapperClass" |
|
|
||||||
"kcContentWrapperClass" |
|
|
||||||
"kcLabelWrapperClass" |
|
|
||||||
"kcContentWrapperClass" |
|
|
||||||
"kcLabelWrapperClass" |
|
|
||||||
"kcFormGroupClass" |
|
|
||||||
"kcResetFlowIcon" |
|
|
||||||
"kcResetFlowIcon" |
|
|
||||||
"kcFeedbackSuccessIcon" |
|
|
||||||
"kcFeedbackWarningIcon" |
|
|
||||||
"kcFeedbackErrorIcon" |
|
|
||||||
"kcFeedbackInfoIcon" |
|
|
||||||
"kcContentWrapperClass" |
|
|
||||||
"kcFormSocialAccountContentClass" |
|
|
||||||
"kcFormSocialAccountClass" |
|
|
||||||
"kcSignUpClass" |
|
|
||||||
"kcInfoAreaWrapperClass"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const defaultKcTemplateProperties: KcTemplateProperties = {
|
|
||||||
"styles": ["css/login.css"],
|
|
||||||
"stylesCommon": [
|
|
||||||
...[".min.css", "-additions.min.css"]
|
|
||||||
.map(end => `node_modules/patternfly/dist/css/patternfly${end}`),
|
|
||||||
"lib/zocial/zocial.css"
|
|
||||||
],
|
|
||||||
"kcLoginClass": "login-pf-page",
|
|
||||||
"kcContentWrapperClass": "row",
|
|
||||||
"kcHeaderClass": "login-pf-page-header",
|
|
||||||
"kcFormCardClass": "card-pf",
|
|
||||||
"kcFormCardAccountClass": "login-pf-accounts",
|
|
||||||
"kcFormSocialAccountClass": "login-pf-social-section",
|
|
||||||
"kcFormSocialAccountContentClass": "col-xs-12 col-sm-6",
|
|
||||||
"kcFormHeaderClass": "login-pf-header",
|
|
||||||
"kcFeedbackErrorIcon": "pficon pficon-error-circle-o",
|
|
||||||
"kcFeedbackWarningIcon": "pficon pficon-warning-triangle-o",
|
|
||||||
"kcFeedbackSuccessIcon": "pficon pficon-ok",
|
|
||||||
"kcFeedbackInfoIcon": "pficon pficon-info",
|
|
||||||
"kcResetFlowIcon": "pficon pficon-arrow fa-2x",
|
|
||||||
"kcFormGroupClass": "form-group",
|
|
||||||
"kcLabelWrapperClass": "col-xs-12 col-sm-12 col-md-12 col-lg-12",
|
|
||||||
"kcSignUpClass": "login-pf-sighup"
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Tu use if you don't want any default */
|
|
||||||
export const allClearKcTemplateProperties =
|
|
||||||
allPropertiesValuesToUndefined(defaultKcTemplateProperties);
|
|
||||||
|
|
||||||
export const Template = memo((props: Props) =>{
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displayInfo = false,
|
displayInfo = false,
|
||||||
@ -97,22 +38,22 @@ export const Template = memo((props: Props) =>{
|
|||||||
displayRequiredFields = false,
|
displayRequiredFields = false,
|
||||||
displayWide = false,
|
displayWide = false,
|
||||||
showAnotherWayIfPresent = true,
|
showAnotherWayIfPresent = true,
|
||||||
properties = {},
|
kcProperties = {},
|
||||||
headerNode,
|
headerNode,
|
||||||
showUsernameNode,
|
showUsernameNode,
|
||||||
formNode,
|
formNode,
|
||||||
displayInfoNode
|
displayInfoNode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { t } = useKeycloakThemeTranslation();
|
const { t } = useKcTranslation();
|
||||||
|
|
||||||
Object.assign(properties, defaultKcTemplateProperties);
|
Object.assign(kcProperties, defaultKcTemplateProperties);
|
||||||
|
|
||||||
const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage();
|
const { kcLanguageTag, setKcLanguageTag } = useKcLanguageTag();
|
||||||
|
|
||||||
const onChangeLanguageClickFactory = useCallbackFactory(
|
const onChangeLanguageClickFactory = useCallbackFactory(
|
||||||
([languageTag]: [AvailableLanguages]) =>
|
([languageTag]: [KcLanguageTag]) =>
|
||||||
setKeycloakLanguage(languageTag)
|
setKcLanguageTag(languageTag)
|
||||||
);
|
);
|
||||||
|
|
||||||
const onTryAnotherWayClick = useConstCallback(() => {
|
const onTryAnotherWayClick = useConstCallback(() => {
|
||||||
@ -123,31 +64,28 @@ export const Template = memo((props: Props) =>{
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const [{ realm, locale, auth, url, message, isAppInitiatedAction }] = useState(() => {
|
const [{ realm, locale, auth, url, message, isAppInitiatedAction }] = useState(() => (
|
||||||
|
assert(kcContext !== undefined, "App is not currently being served by KeyCloak"),
|
||||||
assert(keycloakPagesContext !== undefined);
|
kcContext
|
||||||
|
));
|
||||||
return keycloakPagesContext;
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
properties.stylesCommon?.forEach(
|
kcProperties.stylesCommon?.forEach(
|
||||||
relativePath =>
|
relativePath =>
|
||||||
appendLinkInHead(
|
appendLinkInHead(
|
||||||
{ "href": pathJoin(url.resourcesCommonPath, relativePath) }
|
{ "href": pathJoin(url.resourcesCommonPath, relativePath) }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
properties.styles?.forEach(
|
kcProperties.styles?.forEach(
|
||||||
relativePath =>
|
relativePath =>
|
||||||
appendLinkInHead(
|
appendLinkInHead(
|
||||||
{ "href": pathJoin(url.resourcesPath, relativePath) }
|
{ "href": pathJoin(url.resourcesPath, relativePath) }
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
properties.scripts?.forEach(
|
kcProperties.scripts?.forEach(
|
||||||
relativePath =>
|
relativePath =>
|
||||||
appendScriptInHead(
|
appendScriptInHead(
|
||||||
{ "src": pathJoin(url.resourcesPath, relativePath) }
|
{ "src": pathJoin(url.resourcesPath, relativePath) }
|
||||||
@ -158,16 +96,16 @@ export const Template = memo((props: Props) =>{
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(properties.kcLoginClass)}>
|
<div className={cx(kcProperties.kcLoginClass)}>
|
||||||
|
|
||||||
<div id="kc-header" className={cx(properties.kcHeaderClass)}>
|
<div id="kc-header" className={cx(kcProperties.kcHeaderClass)}>
|
||||||
<div id="kc-header-wrapper" className={cx(properties.kcHeaderWrapperClass)}>
|
<div id="kc-header-wrapper" className={cx(kcProperties.kcHeaderWrapperClass)}>
|
||||||
{t("loginTitleHtml", realm.displayNameHtml)}
|
{t("loginTitleHtml", realm.displayNameHtml)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cx("kcFormCardClass", displayWide && properties.kcFormCardAccountClass)}>
|
<div className={cx("kcFormCardClass", displayWide && kcProperties.kcFormCardAccountClass)}>
|
||||||
<header className={cx(properties.kcFormHeaderClass)}>
|
<header className={cx(kcProperties.kcFormHeaderClass)}>
|
||||||
{
|
{
|
||||||
(
|
(
|
||||||
realm.internationalizationEnabled &&
|
realm.internationalizationEnabled &&
|
||||||
@ -175,10 +113,10 @@ export const Template = memo((props: Props) =>{
|
|||||||
locale.supported.length > 1
|
locale.supported.length > 1
|
||||||
) &&
|
) &&
|
||||||
<div id="kc-locale">
|
<div id="kc-locale">
|
||||||
<div id="kc-locale-wrapper" className={cx(properties.kcLocaleWrapperClass)}>
|
<div id="kc-locale-wrapper" className={cx(kcProperties.kcLocaleWrapperClass)}>
|
||||||
<div className="kc-dropdown" id="kc-locale-dropdown">
|
<div className="kc-dropdown" id="kc-locale-dropdown">
|
||||||
<a href="#" id="kc-current-locale-link">
|
<a href="#" id="kc-current-locale-link">
|
||||||
{getLanguageLabel(keycloakLanguage)}
|
{getKcLanguageTagLabel(kcLanguageTag)}
|
||||||
</a>
|
</a>
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
@ -186,7 +124,7 @@ export const Template = memo((props: Props) =>{
|
|||||||
({ languageTag }) =>
|
({ languageTag }) =>
|
||||||
<li className="kc-dropdown-item">
|
<li className="kc-dropdown-item">
|
||||||
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
|
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
|
||||||
{getLanguageLabel(languageTag)}
|
{getKcLanguageTagLabel(languageTag)}
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
@ -209,8 +147,8 @@ export const Template = memo((props: Props) =>{
|
|||||||
displayRequiredFields ?
|
displayRequiredFields ?
|
||||||
(
|
(
|
||||||
|
|
||||||
<div className={cx(properties.kcContentWrapperClass)}>
|
<div className={cx(kcProperties.kcContentWrapperClass)}>
|
||||||
<div className={cx(properties.kcLabelWrapperClass, "subtitle")}>
|
<div className={cx(kcProperties.kcLabelWrapperClass, "subtitle")}>
|
||||||
<span className="subtitle">
|
<span className="subtitle">
|
||||||
<span className="required">*</span>
|
<span className="required">*</span>
|
||||||
{t("requiredFields")}
|
{t("requiredFields")}
|
||||||
@ -230,18 +168,18 @@ export const Template = memo((props: Props) =>{
|
|||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
displayRequiredFields ? (
|
displayRequiredFields ? (
|
||||||
<div className={cx(properties.kcContentWrapperClass)}>
|
<div className={cx(kcProperties.kcContentWrapperClass)}>
|
||||||
<div className={cx(properties.kcLabelWrapperClass, "subtitle")}>
|
<div className={cx(kcProperties.kcLabelWrapperClass, "subtitle")}>
|
||||||
<span className="subtitle"><span className="required">*</span> {t("requiredFields")}</span>
|
<span className="subtitle"><span className="required">*</span> {t("requiredFields")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-md-10">
|
<div className="col-md-10">
|
||||||
{showUsernameNode}
|
{showUsernameNode}
|
||||||
<div className={cx(properties.kcFormGroupClass)}>
|
<div className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
<div id="kc-username">
|
<div id="kc-username">
|
||||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||||
<div className="kc-login-tooltip">
|
<div className="kc-login-tooltip">
|
||||||
<i className={cx(properties.kcResetFlowIcon)}></i>
|
<i className={cx(kcProperties.kcResetFlowIcon)}></i>
|
||||||
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
|
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -252,12 +190,12 @@ export const Template = memo((props: Props) =>{
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{showUsernameNode}
|
{showUsernameNode}
|
||||||
<div className={cx(properties.kcFormGroupClass)}>
|
<div className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
<div id="kc-username">
|
<div id="kc-username">
|
||||||
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
|
||||||
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
<a id="reset-login" href={url.loginRestartFlowUrl}>
|
||||||
<div className="kc-login-tooltip">
|
<div className="kc-login-tooltip">
|
||||||
<i className={cx(properties.kcResetFlowIcon)}></i>
|
<i className={cx(kcProperties.kcResetFlowIcon)}></i>
|
||||||
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
|
<span className="kc-tooltip-text">{t("restartLoginTooltip")}</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@ -281,10 +219,10 @@ export const Template = memo((props: Props) =>{
|
|||||||
)
|
)
|
||||||
) &&
|
) &&
|
||||||
<div className={cx("alert", `alert-${message.type}`)}>
|
<div className={cx("alert", `alert-${message.type}`)}>
|
||||||
{message.type === "success" && <span className={cx(properties.kcFeedbackSuccessIcon)}></span>}
|
{message.type === "success" && <span className={cx(kcProperties.kcFeedbackSuccessIcon)}></span>}
|
||||||
{message.type === "warning" && <span className={cx(properties.kcFeedbackWarningIcon)}></span>}
|
{message.type === "warning" && <span className={cx(kcProperties.kcFeedbackWarningIcon)}></span>}
|
||||||
{message.type === "error" && <span className={cx(properties.kcFeedbackErrorIcon)}></span>}
|
{message.type === "error" && <span className={cx(kcProperties.kcFeedbackErrorIcon)}></span>}
|
||||||
{message.type === "info" && <span className={cx(properties.kcFeedbackInfoIcon)}></span>}
|
{message.type === "info" && <span className={cx(kcProperties.kcFeedbackInfoIcon)}></span>}
|
||||||
<span className="kc-feedback-text">{message.summary}</span>
|
<span className="kc-feedback-text">{message.summary}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@ -296,9 +234,9 @@ export const Template = memo((props: Props) =>{
|
|||||||
showAnotherWayIfPresent
|
showAnotherWayIfPresent
|
||||||
) &&
|
) &&
|
||||||
|
|
||||||
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && properties.kcContentWrapperClass)} >
|
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && kcProperties.kcContentWrapperClass)} >
|
||||||
<div className={cx(displayWide && [properties.kcFormSocialAccountContentClass, properties.kcFormSocialAccountClass])} >
|
<div className={cx(displayWide && [kcProperties.kcFormSocialAccountContentClass, kcProperties.kcFormSocialAccountClass])} >
|
||||||
<div className={cx(properties.kcFormGroupClass)}>
|
<div className={cx(kcProperties.kcFormGroupClass)}>
|
||||||
<input type="hidden" name="tryAnotherWay" value="on" />
|
<input type="hidden" name="tryAnotherWay" value="on" />
|
||||||
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{t("doTryAnotherWay")}</a>
|
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{t("doTryAnotherWay")}</a>
|
||||||
</div>
|
</div>
|
||||||
@ -308,8 +246,8 @@ export const Template = memo((props: Props) =>{
|
|||||||
{
|
{
|
||||||
displayInfo &&
|
displayInfo &&
|
||||||
|
|
||||||
<div id="kc-info" className={cx(properties.kcSignUpClass)}>
|
<div id="kc-info" className={cx(kcProperties.kcSignUpClass)}>
|
||||||
<div id="kc-info-wrapper" className={cx(properties.kcInfoAreaWrapperClass)}>
|
<div id="kc-info-wrapper" className={cx(kcProperties.kcInfoAreaWrapperClass)}>
|
||||||
{displayInfoNode}
|
{displayInfoNode}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -1,7 +1,17 @@
|
|||||||
|
|
||||||
import type { AvailableLanguages } from "./useKeycloakLanguage";
|
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
||||||
|
import { messages } from "./generated_messages/login";
|
||||||
|
|
||||||
export function getLanguageLabel(language: AvailableLanguages): LanguageLabel {
|
export type KcLanguageTag = keyof typeof messages;
|
||||||
|
|
||||||
|
export type LanguageLabel =
|
||||||
|
/* spell-checker: disable */
|
||||||
|
"Deutsch" | "Norsk" | "Русский" | "Svenska" | "Português (Brasil)" | "Lietuvių" |
|
||||||
|
"English" | "Italiano" | "Français" | "中文简体" | "Español" | "Čeština" | "日本語" |
|
||||||
|
"Slovenčina" | "Polish" | "Català" | "Nederlands" | "tr";
|
||||||
|
/* spell-checker: enable */
|
||||||
|
|
||||||
|
export function getKcLanguageTagLabel(language: KcLanguageTag): LanguageLabel {
|
||||||
|
|
||||||
switch (language) {
|
switch (language) {
|
||||||
/* spell-checker: disable */
|
/* spell-checker: disable */
|
||||||
@ -31,9 +41,29 @@ export function getLanguageLabel(language: AvailableLanguages): LanguageLabel {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LanguageLabel =
|
const availableLanguages = objectKeys(messages);
|
||||||
/* spell-checker: disable */
|
|
||||||
"Deutsch" | "Norsk" | "Русский" | "Svenska" | "Português (Brasil)" | "Lietuvių" |
|
/**
|
||||||
"English" | "Italiano" | "Français" | "中文简体" | "Español" | "Čeština" | "日本語" |
|
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
|
||||||
"Slovenčina" | "Polish" | "Català" | "Nederlands" | "tr";
|
* it corresponds to: "fr".
|
||||||
/* spell-checker: enable */
|
* If there is no reasonable match it's guessed from navigator.language.
|
||||||
|
* If still no matches "en" is returned.
|
||||||
|
*/
|
||||||
|
export function getBestMatchAmongKcLanguageTag(
|
||||||
|
languageLike: string
|
||||||
|
): KcLanguageTag {
|
||||||
|
|
||||||
|
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
|
||||||
|
|
||||||
|
const language = availableLanguages.find(language =>
|
||||||
|
language.toLowerCase().includes(iso2LanguageLike) ||
|
||||||
|
getKcLanguageTagLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (language === undefined && languageLike !== navigator.language) {
|
||||||
|
return getBestMatchAmongKcLanguageTag(navigator.language);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "en";
|
||||||
|
}
|
||||||
|
|
15
src/lib/i18n/useKcLanguageTag.ts
Normal file
15
src/lib/i18n/useKcLanguageTag.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
import { createUseGlobalState } from "powerhooks";
|
||||||
|
import { kcContext } from "../kcContext";
|
||||||
|
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
|
||||||
|
import { assert } from "evt/tools/typeSafety/assert";
|
||||||
|
|
||||||
|
export const { useKcLanguageTag } = createUseGlobalState(
|
||||||
|
"kcLanguageTag",
|
||||||
|
() => getBestMatchAmongKcLanguageTag((
|
||||||
|
assert(kcContext !== undefined, "Page not served by KeyCloak"),
|
||||||
|
kcContext.locale?.["current" as never] ??
|
||||||
|
navigator.language
|
||||||
|
)),
|
||||||
|
{ "persistance": "cookies" }
|
||||||
|
);
|
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import { useKeycloakLanguage } from "./useKeycloakLanguage";
|
import { useKcLanguageTag } from "./useKcLanguageTag";
|
||||||
import { messages } from "./generated_messages/login";
|
import { messages } from "./generated_messages/login";
|
||||||
import { useConstCallback } from "powerhooks";
|
import { useConstCallback } from "powerhooks";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
@ -7,14 +7,14 @@ import { id } from "evt/tools/typeSafety/id";
|
|||||||
|
|
||||||
export type MessageKey = keyof typeof messages["en"];
|
export type MessageKey = keyof typeof messages["en"];
|
||||||
|
|
||||||
export function useKeycloakThemeTranslation() {
|
export function useKcTranslation() {
|
||||||
|
|
||||||
const { keycloakLanguage } = useKeycloakLanguage();
|
const { kcLanguageTag } = useKcLanguageTag();
|
||||||
|
|
||||||
const tStr = useConstCallback(
|
const tStr = useConstCallback(
|
||||||
(key: MessageKey, ...args: (string | undefined)[]): string => {
|
(key: MessageKey, ...args: (string | undefined)[]): string => {
|
||||||
|
|
||||||
let str: string = messages[keycloakLanguage as any as "en"][key] ?? messages["en"][key];
|
let str: string = messages[kcLanguageTag as any as "en"][key] ?? messages["en"][key];
|
||||||
|
|
||||||
args.forEach((arg, i) => {
|
args.forEach((arg, i) => {
|
||||||
|
|
@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
import { createUseGlobalState } from "powerhooks";
|
|
||||||
import { messages } from "./generated_messages/login";
|
|
||||||
import { objectKeys } from "evt/tools/typeSafety/objectKeys";
|
|
||||||
import { getLanguageLabel } from "./getLanguageLabel";
|
|
||||||
import { keycloakPagesContext } from "../keycloakFtlValues";
|
|
||||||
|
|
||||||
const availableLanguages = objectKeys(messages);
|
|
||||||
|
|
||||||
export type AvailableLanguages = typeof availableLanguages[number];
|
|
||||||
|
|
||||||
export const { useKeycloakLanguage } = createUseGlobalState(
|
|
||||||
"keycloakLanguage",
|
|
||||||
() => getBestMatchAmongKeycloakAvailableLanguages(
|
|
||||||
keycloakPagesContext?.locale?.["current" as never] ??
|
|
||||||
navigator.language
|
|
||||||
),
|
|
||||||
{ "persistance": "cookies" }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pass in "fr-FR" or "français" for example, it will return the AvailableLanguage
|
|
||||||
* it corresponds to: "fr".
|
|
||||||
* If there is no reasonable match it's guessed from navigator.language.
|
|
||||||
* If still no matches "en" is returned.
|
|
||||||
*/
|
|
||||||
export function getBestMatchAmongKeycloakAvailableLanguages(
|
|
||||||
languageLike: string
|
|
||||||
): AvailableLanguages {
|
|
||||||
|
|
||||||
const iso2LanguageLike = languageLike.split("-")[0].toLowerCase();
|
|
||||||
|
|
||||||
const language = availableLanguages.find(language =>
|
|
||||||
language.toLowerCase().includes(iso2LanguageLike) ||
|
|
||||||
getLanguageLabel(language).toLocaleLowerCase() === languageLike.toLocaleLowerCase()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (language === undefined && languageLike !== navigator.language) {
|
|
||||||
return getBestMatchAmongKeycloakAvailableLanguages(navigator.language);
|
|
||||||
}
|
|
||||||
|
|
||||||
return "en";
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,9 @@
|
|||||||
export * from "./keycloakFtlValues";
|
export * from "./kcContext";
|
||||||
export * from "./i18n/useKeycloakLanguage";
|
|
||||||
export * from "./i18n/useKeycloakTranslation";
|
export * from "./i18n/KcLanguageTag";
|
||||||
export * from "./i18n/getLanguageLabel";
|
export * from "./i18n/useKcLanguageTag";
|
||||||
|
export * from "./i18n/useKcTranslation";
|
||||||
|
|
||||||
|
export * from "./components/KcProperties";
|
||||||
|
export * from "./components/Login";
|
||||||
|
export * from "./components/Template";
|
@ -2,11 +2,10 @@
|
|||||||
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
|
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
|
||||||
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
|
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
|
||||||
import { id } from "evt/tools/typeSafety/id";
|
import { id } from "evt/tools/typeSafety/id";
|
||||||
//import type { LanguageLabel } from "./i18n/getLanguageLabel";
|
import type { KcLanguageTag } from "./i18n/KcLanguageTag";
|
||||||
import type { AvailableLanguages } from "./i18n/useKeycloakLanguage";
|
|
||||||
|
|
||||||
|
|
||||||
export type KeycloakFtlValues = {
|
export type KcContext = {
|
||||||
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
|
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
|
||||||
url: {
|
url: {
|
||||||
loginAction: string;
|
loginAction: string;
|
||||||
@ -30,7 +29,7 @@ export type KeycloakFtlValues = {
|
|||||||
locale?: {
|
locale?: {
|
||||||
supported: {
|
supported: {
|
||||||
//url: string;
|
//url: string;
|
||||||
languageTag: AvailableLanguages;
|
languageTag: KcLanguageTag;
|
||||||
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
|
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
|
||||||
* or getLanguageLabel(languageTag) === label
|
* or getLanguageLabel(languageTag) === label
|
||||||
*/
|
*/
|
||||||
@ -70,6 +69,4 @@ export type KeycloakFtlValues = {
|
|||||||
registrationDisabled: boolean;
|
registrationDisabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { keycloakPagesContext } =
|
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);
|
||||||
{ [ftlValuesGlobalName]: id<KeycloakFtlValues | undefined>((window as any)[ftlValuesGlobalName]) };
|
|
||||||
;
|
|
Loading…
x
Reference in New Issue
Block a user