Login page implemented

This commit is contained in:
Joseph Garrone 2021-03-02 23:48:31 +01:00
parent 7f6ab1d391
commit 43c412405a
12 changed files with 487 additions and 485 deletions

View File

@ -1,6 +1,6 @@
{
"name": "keycloak-react-theming",
"version": "0.0.25",
"version": "0.0.26",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",

View File

@ -1,2 +1,2 @@
export const ftlValuesGlobalName = "keycloakPagesContext";
export const ftlValuesGlobalName = "kcContext";

View File

@ -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>
}
/>
);
});

View 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);

View 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>
}
/>
);
});

View File

@ -1,95 +1,36 @@
import { useState, useEffect, memo } from "react";
import type { ReactNode } from "react";
import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation";
import { keycloakPagesContext } from "./keycloakFtlValues";
import { useKcTranslation } from "../i18n/useKcTranslation";
import { kcContext } from "../kcContext";
import { assert } from "evt/tools/typeSafety/assert";
import { cx } from "tss-react";
import { useKeycloakLanguage, AvailableLanguages } from "./i18n/useKeycloakLanguage";
import { getLanguageLabel } from "./i18n/getLanguageLabel";
import { useKcLanguageTag } from "../i18n/useKcLanguageTag";
import type { KcLanguageTag } from "../i18n/KcLanguageTag";
import { getKcLanguageTagLabel } from "../i18n/KcLanguageTag";
import { useCallbackFactory } from "powerhooks";
import { appendLinkInHead } from "./tools/appendLinkInHead";
import { appendScriptInHead } from "./tools/appendScriptInHead";
import { appendLinkInHead } from "../tools/appendLinkInHead";
import { appendScriptInHead } from "../tools/appendScriptInHead";
import { join as pathJoin } from "path";
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;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
properties: KcTemplateProperties;
headerNode: ReactNode;
showUsernameNode: ReactNode;
formNode: 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 = {
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) =>{
export const Template = memo((props: TemplateProps) => {
const {
displayInfo = false,
@ -97,22 +38,22 @@ export const Template = memo((props: Props) =>{
displayRequiredFields = false,
displayWide = false,
showAnotherWayIfPresent = true,
properties = {},
kcProperties = {},
headerNode,
showUsernameNode,
formNode,
displayInfoNode
} = 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(
([languageTag]: [AvailableLanguages]) =>
setKeycloakLanguage(languageTag)
([languageTag]: [KcLanguageTag]) =>
setKcLanguageTag(languageTag)
);
const onTryAnotherWayClick = useConstCallback(() => {
@ -123,31 +64,28 @@ export const Template = memo((props: Props) =>{
});
const [{ realm, locale, auth, url, message, isAppInitiatedAction }] = useState(() => {
assert(keycloakPagesContext !== undefined);
return keycloakPagesContext;
});
const [{ realm, locale, auth, url, message, isAppInitiatedAction }] = useState(() => (
assert(kcContext !== undefined, "App is not currently being served by KeyCloak"),
kcContext
));
useEffect(() => {
properties.stylesCommon?.forEach(
kcProperties.stylesCommon?.forEach(
relativePath =>
appendLinkInHead(
{ "href": pathJoin(url.resourcesCommonPath, relativePath) }
)
);
properties.styles?.forEach(
kcProperties.styles?.forEach(
relativePath =>
appendLinkInHead(
{ "href": pathJoin(url.resourcesPath, relativePath) }
)
);
properties.scripts?.forEach(
kcProperties.scripts?.forEach(
relativePath =>
appendScriptInHead(
{ "src": pathJoin(url.resourcesPath, relativePath) }
@ -158,16 +96,16 @@ export const Template = memo((props: Props) =>{
}, []);
return (
<div className={cx(properties.kcLoginClass)}>
<div className={cx(kcProperties.kcLoginClass)}>
<div id="kc-header" className={cx(properties.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(properties.kcHeaderWrapperClass)}>
<div id="kc-header" className={cx(kcProperties.kcHeaderClass)}>
<div id="kc-header-wrapper" className={cx(kcProperties.kcHeaderWrapperClass)}>
{t("loginTitleHtml", realm.displayNameHtml)}
</div>
</div>
<div className={cx("kcFormCardClass", displayWide && properties.kcFormCardAccountClass)}>
<header className={cx(properties.kcFormHeaderClass)}>
<div className={cx("kcFormCardClass", displayWide && kcProperties.kcFormCardAccountClass)}>
<header className={cx(kcProperties.kcFormHeaderClass)}>
{
(
realm.internationalizationEnabled &&
@ -175,10 +113,10 @@ export const Template = memo((props: Props) =>{
locale.supported.length > 1
) &&
<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">
<a href="#" id="kc-current-locale-link">
{getLanguageLabel(keycloakLanguage)}
{getKcLanguageTagLabel(kcLanguageTag)}
</a>
<ul>
{
@ -186,7 +124,7 @@ export const Template = memo((props: Props) =>{
({ languageTag }) =>
<li className="kc-dropdown-item">
<a href="#" onClick={onChangeLanguageClickFactory(languageTag)}>
{getLanguageLabel(languageTag)}
{getKcLanguageTagLabel(languageTag)}
</a>
</li>
@ -209,8 +147,8 @@ export const Template = memo((props: Props) =>{
displayRequiredFields ?
(
<div className={cx(properties.kcContentWrapperClass)}>
<div className={cx(properties.kcLabelWrapperClass, "subtitle")}>
<div className={cx(kcProperties.kcContentWrapperClass)}>
<div className={cx(kcProperties.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle">
<span className="required">*</span>
{t("requiredFields")}
@ -230,18 +168,18 @@ export const Template = memo((props: Props) =>{
)
) : (
displayRequiredFields ? (
<div className={cx(properties.kcContentWrapperClass)}>
<div className={cx(properties.kcLabelWrapperClass, "subtitle")}>
<div className={cx(kcProperties.kcContentWrapperClass)}>
<div className={cx(kcProperties.kcLabelWrapperClass, "subtitle")}>
<span className="subtitle"><span className="required">*</span> {t("requiredFields")}</span>
</div>
<div className="col-md-10">
{showUsernameNode}
<div className={cx(properties.kcFormGroupClass)}>
<div className={cx(kcProperties.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<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>
</div>
</a>
@ -252,12 +190,12 @@ export const Template = memo((props: Props) =>{
) : (
<>
{showUsernameNode}
<div className={cx(properties.kcFormGroupClass)}>
<div className={cx(kcProperties.kcFormGroupClass)}>
<div id="kc-username">
<label id="kc-attempted-username">{auth?.attemptedUsername}</label>
<a id="reset-login" href={url.loginRestartFlowUrl}>
<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>
</div>
</a>
@ -281,10 +219,10 @@ export const Template = memo((props: Props) =>{
)
) &&
<div className={cx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={cx(properties.kcFeedbackSuccessIcon)}></span>}
{message.type === "warning" && <span className={cx(properties.kcFeedbackWarningIcon)}></span>}
{message.type === "error" && <span className={cx(properties.kcFeedbackErrorIcon)}></span>}
{message.type === "info" && <span className={cx(properties.kcFeedbackInfoIcon)}></span>}
{message.type === "success" && <span className={cx(kcProperties.kcFeedbackSuccessIcon)}></span>}
{message.type === "warning" && <span className={cx(kcProperties.kcFeedbackWarningIcon)}></span>}
{message.type === "error" && <span className={cx(kcProperties.kcFeedbackErrorIcon)}></span>}
{message.type === "info" && <span className={cx(kcProperties.kcFeedbackInfoIcon)}></span>}
<span className="kc-feedback-text">{message.summary}</span>
</div>
}
@ -296,9 +234,9 @@ export const Template = memo((props: Props) =>{
showAnotherWayIfPresent
) &&
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && properties.kcContentWrapperClass)} >
<div className={cx(displayWide && [properties.kcFormSocialAccountContentClass, properties.kcFormSocialAccountClass])} >
<div className={cx(properties.kcFormGroupClass)}>
<form id="kc-select-try-another-way-form" action={url.loginAction} method="post" className={cx(displayWide && kcProperties.kcContentWrapperClass)} >
<div className={cx(displayWide && [kcProperties.kcFormSocialAccountContentClass, kcProperties.kcFormSocialAccountClass])} >
<div className={cx(kcProperties.kcFormGroupClass)}>
<input type="hidden" name="tryAnotherWay" value="on" />
<a href="#" id="try-another-way" onClick={onTryAnotherWayClick}>{t("doTryAnotherWay")}</a>
</div>
@ -308,15 +246,15 @@ export const Template = memo((props: Props) =>{
{
displayInfo &&
<div id="kc-info" className={cx(properties.kcSignUpClass)}>
<div id="kc-info-wrapper" className={cx(properties.kcInfoAreaWrapperClass)}>
<div id="kc-info" className={cx(kcProperties.kcSignUpClass)}>
<div id="kc-info-wrapper" className={cx(kcProperties.kcInfoAreaWrapperClass)}>
{displayInfoNode}
</div>
</div>
}
</div >
</div >
</div >
</div >
</div>
</div>
</div>
</div>
);
});

View File

@ -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) {
/* spell-checker: disable */
@ -31,9 +41,29 @@ export function getLanguageLabel(language: AvailableLanguages): LanguageLabel {
}
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 */
const availableLanguages = objectKeys(messages);
/**
* 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 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";
}

View 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" }
);

View File

@ -1,5 +1,5 @@
import { useKeycloakLanguage } from "./useKeycloakLanguage";
import { useKcLanguageTag } from "./useKcLanguageTag";
import { messages } from "./generated_messages/login";
import { useConstCallback } from "powerhooks";
import type { ReactNode } from "react";
@ -7,14 +7,14 @@ import { id } from "evt/tools/typeSafety/id";
export type MessageKey = keyof typeof messages["en"];
export function useKeycloakThemeTranslation() {
export function useKcTranslation() {
const { keycloakLanguage } = useKeycloakLanguage();
const { kcLanguageTag } = useKcLanguageTag();
const tStr = useConstCallback(
(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) => {

View File

@ -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";
}

View File

@ -1,4 +1,9 @@
export * from "./keycloakFtlValues";
export * from "./i18n/useKeycloakLanguage";
export * from "./i18n/useKeycloakTranslation";
export * from "./i18n/getLanguageLabel";
export * from "./kcContext";
export * from "./i18n/KcLanguageTag";
export * from "./i18n/useKcLanguageTag";
export * from "./i18n/useKcTranslation";
export * from "./components/KcProperties";
export * from "./components/Login";
export * from "./components/Template";

View File

@ -2,11 +2,10 @@
import { ftlValuesGlobalName } from "../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { generateFtlFilesCodeFactory } from "../bin/build-keycloak-theme/generateFtl";
import { id } from "evt/tools/typeSafety/id";
//import type { LanguageLabel } from "./i18n/getLanguageLabel";
import type { AvailableLanguages } from "./i18n/useKeycloakLanguage";
import type { KcLanguageTag } from "./i18n/KcLanguageTag";
export type KeycloakFtlValues = {
export type KcContext = {
pageBasename: Parameters<ReturnType<typeof generateFtlFilesCodeFactory>["generateFtlFilesCode"]>[0]["pageBasename"];
url: {
loginAction: string;
@ -30,7 +29,7 @@ export type KeycloakFtlValues = {
locale?: {
supported: {
//url: string;
languageTag: AvailableLanguages;
languageTag: KcLanguageTag;
/** Is determined by languageTag. Ex: languageTag === "en" => label === "English"
* or getLanguageLabel(languageTag) === label
*/
@ -70,6 +69,4 @@ export type KeycloakFtlValues = {
registrationDisabled: boolean;
};
export const { keycloakPagesContext } =
{ [ftlValuesGlobalName]: id<KeycloakFtlValues | undefined>((window as any)[ftlValuesGlobalName]) };
;
export const kcContext = id<KcContext | undefined>((window as any)[ftlValuesGlobalName]);