progressing

This commit is contained in:
garronej
2023-03-17 20:40:29 +01:00
parent 815f510d5f
commit 4b89d15c1e
12 changed files with 712 additions and 499 deletions

View File

@ -1,239 +0,0 @@
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
import { assert } from "tsafe/assert";
import type { KcContextBase } from "./kcContext";
import type { ReactNode } from "react";
import { I18nBase } from "./i18n";
/** Class names can be provided as an array or separated by whitespace */
export type KcPropsGeneric<CssClasses extends string> = {
[key in CssClasses]: readonly string[] | string | undefined;
};
export type KcTemplateClassKey =
| "stylesCommon"
| "styles"
| "scripts"
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
| "kcHeaderWrapperClass"
| "kcFormCardClass"
| "kcFormCardAccountClass"
| "kcFormHeaderClass"
| "kcLocaleWrapperClass"
| "kcContentWrapperClass"
| "kcLabelWrapperClass"
| "kcFormGroupClass"
| "kcResetFlowIcon"
| "kcFeedbackSuccessIcon"
| "kcFeedbackWarningIcon"
| "kcFeedbackErrorIcon"
| "kcFeedbackInfoIcon"
| "kcFormSocialAccountContentClass"
| "kcFormSocialAccountClass"
| "kcSignUpClass"
| "kcInfoAreaWrapperClass";
export type KcTemplateProps = KcPropsGeneric<KcTemplateClassKey>;
export const defaultKcTemplateProps = {
"stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"scripts": [],
"kcHtmlClass": ["login-pf"],
"kcLoginClass": ["login-pf-page"],
"kcContentWrapperClass": ["row"],
"kcHeaderClass": ["login-pf-page-header"],
"kcHeaderWrapperClass": [],
"kcFormCardClass": ["card-pf"],
"kcFormCardAccountClass": ["login-pf-accounts"],
"kcFormSocialAccountClass": ["login-pf-social-section"],
"kcFormSocialAccountContentClass": ["col-xs-12", "col-sm-6"],
"kcFormHeaderClass": ["login-pf-header"],
"kcLocaleWrapperClass": [],
"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-signup"],
"kcInfoAreaWrapperClass": []
} as const;
assert<typeof defaultKcTemplateProps extends KcTemplateProps ? true : false>();
/** Tu use if you don't want any default */
export const allClearKcTemplateProps = allPropertiesValuesToUndefined(defaultKcTemplateProps);
assert<typeof allClearKcTemplateProps extends KcTemplateProps ? true : false>();
export type KcProps = KcPropsGeneric<
| KcTemplateClassKey
| "kcLogoLink"
| "kcLogoClass"
| "kcContainerClass"
| "kcContentClass"
| "kcFeedbackAreaClass"
| "kcLocaleClass"
| "kcAlertIconClasserror"
| "kcFormAreaClass"
| "kcFormSocialAccountListClass"
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass"
| "kcInputWrapperClass"
| "kcFormOptionsClass"
| "kcFormButtonsClass"
| "kcFormSettingClass"
| "kcTextareaClass"
| "kcInfoAreaClass"
| "kcFormGroupHeader"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonDefaultClass"
| "kcButtonLargeClass"
| "kcButtonBlockClass"
| "kcInputLargeClass"
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass"
| "kcSelectOTPListClass"
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass"
>;
export const defaultKcProps = {
...defaultKcTemplateProps,
"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"],
"kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"],
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
"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"],
// user-profile grouping
"kcFormGroupHeader": ["pf-c-form__group"],
// 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"],
"kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
"kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
"kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
"kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
"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"],
"kcFormOptionsWrapperClass": []
} as const;
export type TemplateProps<KcContext extends KcContextBase.Common, I18n extends I18nBase> = {
kcContext: KcContext;
i18n: I18n;
doFetchDefaultThemeResources: boolean;
} & {
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
showUsernameNode?: ReactNode;
formNode: ReactNode;
infoNode?: ReactNode;
} & KcTemplateProps;
export type PageProps<KcContext, I18n extends I18nBase> = {
kcContext: KcContext;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
Template: (props: TemplateProps<any, any>) => JSX.Element | null;
} & KcProps;
assert<typeof defaultKcProps extends KcProps ? true : false>();
/** Tu use if you don't want any default */
export const allClearKcProps = allPropertiesValuesToUndefined(defaultKcProps);
assert<typeof allClearKcProps extends KcProps ? true : false>();

View File

@ -1,13 +1,12 @@
import React, { useReducer, useEffect } from "react"; import { assert } from "keycloakify/tools/assert";
import { assert } from "./tools/assert"; import { clsx } from "keycloakify/tools/clsx";
import { headInsert } from "./tools/headInsert"; import { usePrepareTemplate } from "keycloakify/lib/usePrepareTemplate";
import { pathJoin } from "./bin/tools/pathJoin"; import { type TemplateProps, defaultTemplateClasses } from "keycloakify/TemplateProps";
import { clsx } from "./tools/clsx"; import { useGetClassName } from "keycloakify/lib/getClassName";
import type { TemplateProps } from "./KcProps"; type KcContext = import("./kcContext/KcContextBase").KcContextBase.Common.Login;
import type { KcContextBase } from "./kcContext/KcContextBase"; import type { I18nBase as I18n } from "./i18n";
import type { I18nBase } from "./i18n";
export default function Template(props: TemplateProps<KcContextBase.Common, I18nBase>) { export default function Template(props: TemplateProps<KcContext, I18n>) {
const { const {
displayInfo = false, displayInfo = false,
displayMessage = true, displayMessage = true,
@ -20,24 +19,29 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
infoNode = null, infoNode = null,
kcContext, kcContext,
i18n, i18n,
doFetchDefaultThemeResources, doUseDefaultCss,
stylesCommon, classes
styles,
scripts,
kcHtmlClass
} = props; } = props;
const { getClassName } = useGetClassName({
"defaultClasses": !doUseDefaultCss ? undefined : defaultTemplateClasses,
classes
});
const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n; const { msg, changeLocale, labelBySupportedLanguageTag, currentLanguageTag } = i18n;
const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext; const { realm, locale, auth, url, message, isAppInitiatedAction } = kcContext;
const { isReady } = usePrepareTemplate({ const { isReady } = usePrepareTemplate({
doFetchDefaultThemeResources, "doFetchDefaultThemeResources": doUseDefaultCss,
stylesCommon,
styles,
scripts,
url, url,
kcHtmlClass "stylesCommon": [
"node_modules/patternfly/dist/css/patternfly.min.css",
"node_modules/patternfly/dist/css/patternfly-additions.min.css",
"lib/zocial/zocial.css"
],
"styles": ["css/login.css"],
"htmlClassName": getClassName("kcHtmlClass")
}); });
if (!isReady) { if (!isReady) {
@ -45,18 +49,18 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
} }
return ( return (
<div className={clsx(props.kcLoginClass)}> <div className={getClassName("kcLoginClass")}>
<div id="kc-header" className={clsx(props.kcHeaderClass)}> <div id="kc-header" className={getClassName("kcHeaderClass")}>
<div id="kc-header-wrapper" className={clsx(props.kcHeaderWrapperClass)}> <div id="kc-header-wrapper" className={getClassName("kcHeaderWrapperClass")}>
{msg("loginTitleHtml", realm.displayNameHtml)} {msg("loginTitleHtml", realm.displayNameHtml)}
</div> </div>
</div> </div>
<div className={clsx(props.kcFormCardClass, displayWide && props.kcFormCardAccountClass)}> <div className={clsx(getClassName("kcFormCardClass"), displayWide && getClassName("kcFormCardAccountClass"))}>
<header className={clsx(props.kcFormHeaderClass)}> <header className={getClassName("kcFormHeaderClass")}>
{realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && ( {realm.internationalizationEnabled && (assert(locale !== undefined), true) && locale.supported.length > 1 && (
<div id="kc-locale"> <div id="kc-locale">
<div id="kc-locale-wrapper" className={clsx(props.kcLocaleWrapperClass)}> <div id="kc-locale-wrapper" className={getClassName("kcLocaleWrapperClass")}>
<div className="kc-dropdown" id="kc-locale-dropdown"> <div className="kc-dropdown" id="kc-locale-dropdown">
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a href="#" id="kc-current-locale-link"> <a href="#" id="kc-current-locale-link">
@ -78,8 +82,8 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
)} )}
{!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? ( {!(auth !== undefined && auth.showUsername && !auth.showResetCredentials) ? (
displayRequiredFields ? ( displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> <span className="required">*</span>
{msg("requiredFields")} {msg("requiredFields")}
@ -93,20 +97,20 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
<h1 id="kc-page-title">{headerNode}</h1> <h1 id="kc-page-title">{headerNode}</h1>
) )
) : displayRequiredFields ? ( ) : displayRequiredFields ? (
<div className={clsx(props.kcContentWrapperClass)}> <div className={getClassName("kcContentWrapperClass")}>
<div className={clsx(props.kcLabelWrapperClass, "subtitle")}> <div className={clsx(getClassName("kcLabelWrapperClass"), "subtitle")}>
<span className="subtitle"> <span className="subtitle">
<span className="required">*</span> {msg("requiredFields")} <span className="required">*</span> {msg("requiredFields")}
</span> </span>
</div> </div>
<div className="col-md-10"> <div className="col-md-10">
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("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={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -117,12 +121,12 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
) : ( ) : (
<> <>
{showUsernameNode} {showUsernameNode}
<div className={clsx(props.kcFormGroupClass)}> <div className={getClassName("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={clsx(props.kcResetFlowIcon)}></i> <i className={getClassName("kcResetFlowIcon")}></i>
<span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span> <span className="kc-tooltip-text">{msg("restartLoginTooltip")}</span>
</div> </div>
</a> </a>
@ -136,10 +140,10 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
{/* App-initiated actions should not see warning messages about the need to complete the action during login. */} {/* App-initiated actions should not see warning messages about the need to complete the action during login. */}
{displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && ( {displayMessage && message !== undefined && (message.type !== "warning" || !isAppInitiatedAction) && (
<div className={clsx("alert", `alert-${message.type}`)}> <div className={clsx("alert", `alert-${message.type}`)}>
{message.type === "success" && <span className={clsx(props.kcFeedbackSuccessIcon)}></span>} {message.type === "success" && <span className={getClassName("kcFeedbackSuccessIcon")}></span>}
{message.type === "warning" && <span className={clsx(props.kcFeedbackWarningIcon)}></span>} {message.type === "warning" && <span className={getClassName("kcFeedbackWarningIcon")}></span>}
{message.type === "error" && <span className={clsx(props.kcFeedbackErrorIcon)}></span>} {message.type === "error" && <span className={getClassName("kcFeedbackErrorIcon")}></span>}
{message.type === "info" && <span className={clsx(props.kcFeedbackInfoIcon)}></span>} {message.type === "info" && <span className={getClassName("kcFeedbackInfoIcon")}></span>}
<span <span
className="kc-feedback-text" className="kc-feedback-text"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@ -154,10 +158,14 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
id="kc-select-try-another-way-form" id="kc-select-try-another-way-form"
action={url.loginAction} action={url.loginAction}
method="post" method="post"
className={clsx(displayWide && props.kcContentWrapperClass)} className={clsx(displayWide && getClassName("kcContentWrapperClass"))}
> >
<div className={clsx(displayWide && [props.kcFormSocialAccountContentClass, props.kcFormSocialAccountClass])}> <div
<div className={clsx(props.kcFormGroupClass)}> className={clsx(
displayWide && [getClassName("kcFormSocialAccountContentClass"), getClassName("kcFormSocialAccountClass")]
)}
>
<div className={getClassName("kcFormGroupClass")}>
<input type="hidden" name="tryAnotherWay" value="on" /> <input type="hidden" name="tryAnotherWay" value="on" />
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
<a <a
@ -175,8 +183,8 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
</form> </form>
)} )}
{displayInfo && ( {displayInfo && (
<div id="kc-info" className={clsx(props.kcSignUpClass)}> <div id="kc-info" className={getClassName("kcSignUpClass")}>
<div id="kc-info-wrapper" className={clsx(props.kcInfoAreaWrapperClass)}> <div id="kc-info-wrapper" className={getClassName("kcInfoAreaWrapperClass")}>
{infoNode} {infoNode}
</div> </div>
</div> </div>
@ -187,79 +195,3 @@ export default function Template(props: TemplateProps<KcContextBase.Common, I18n
</div> </div>
); );
} }
export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean;
stylesCommon: string | readonly string[] | undefined;
styles: string | readonly string[] | undefined;
scripts: string | readonly string[] | undefined;
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
kcHtmlClass: string | readonly string[] | undefined;
}) {
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, kcHtmlClass } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
useEffect(() => {
if (!doFetchDefaultThemeResources) {
return;
}
let isUnmounted = false;
const toArr = (x: string | readonly string[] | undefined) => (typeof x === "string" ? x.split(" ") : x ?? []);
Promise.all(
[
...toArr(stylesCommon).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...toArr(styles).map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
"type": "css",
href,
"position": "prepend"
})
)
).then(() => {
if (isUnmounted) {
return;
}
setReady();
});
toArr(scripts).forEach(relativePath =>
headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
return () => {
isUnmounted = true;
};
}, [kcHtmlClass]);
useEffect(() => {
if (kcHtmlClass === undefined) {
return;
}
const htmlClassList = document.getElementsByTagName("html")[0].classList;
const tokens = clsx(kcHtmlClass).split(" ");
htmlClassList.add(...tokens);
return () => {
htmlClassList.remove(...tokens);
};
}, [kcHtmlClass]);
return { isReady };
}

64
src/TemplateProps.ts Normal file
View File

@ -0,0 +1,64 @@
import type { ReactNode } from "react";
import type { KcContextBase } from "keycloakify/kcContext";
import type { I18nBase } from "keycloakify/i18n";
export type TemplateProps<KcContext extends KcContextBase.Common.Login, I18n extends I18nBase> = {
kcContext: KcContext;
i18n: I18n;
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
headerNode: ReactNode;
showUsernameNode?: ReactNode;
formNode: ReactNode;
infoNode?: ReactNode;
doUseDefaultCss: boolean;
classes?: Partial<Record<TemplateClassKey, string>>;
};
export type TemplateClassKey =
| "kcHtmlClass"
| "kcLoginClass"
| "kcHeaderClass"
| "kcHeaderWrapperClass"
| "kcFormCardClass"
| "kcFormCardAccountClass"
| "kcFormHeaderClass"
| "kcLocaleWrapperClass"
| "kcContentWrapperClass"
| "kcLabelWrapperClass"
| "kcFormGroupClass"
| "kcResetFlowIcon"
| "kcFeedbackSuccessIcon"
| "kcFeedbackWarningIcon"
| "kcFeedbackErrorIcon"
| "kcFeedbackInfoIcon"
| "kcFormSocialAccountContentClass"
| "kcFormSocialAccountClass"
| "kcSignUpClass"
| "kcInfoAreaWrapperClass";
export const defaultTemplateClasses: Record<TemplateClassKey, string | undefined> = {
"kcHtmlClass": "login-pf",
"kcLoginClass": "login-pf-page",
"kcContentWrapperClass": "row",
"kcHeaderClass": "login-pf-page-header",
"kcHeaderWrapperClass": undefined,
"kcFormCardClass": "card-pf",
"kcFormCardAccountClass": "login-pf-accounts",
"kcFormSocialAccountClass": "login-pf-social-section",
"kcFormSocialAccountContentClass": "col-xs-12 col-sm-6",
"kcFormHeaderClass": "login-pf-header",
"kcLocaleWrapperClass": undefined,
"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-signup",
"kcInfoAreaWrapperClass": undefined
};

View File

@ -1,8 +1,8 @@
import type { LoginThemePageId } from "../bin/keycloakify/generateFtl"; import type { LoginThemePageId, AccountThemePageId } from "../bin/keycloakify/generateFtl";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { Equals } from "tsafe"; import type { Equals } from "tsafe";
import type { MessageKeyBase } from "../i18n"; import type { MessageKeyBase } from "../i18n";
import type { KcTemplateClassKey } from "../KcProps"; import type { KcTemplateClassKey } from "../templates/LoginThemeTemplate";
type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never; type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends `${Prefix}${infer U}` ? U : never;
@ -31,69 +31,69 @@ export type KcContextBase =
| KcContextBase.LoginConfigTotp | KcContextBase.LoginConfigTotp
| KcContextBase.LogoutConfirm | KcContextBase.LogoutConfirm
| KcContextBase.UpdateUserProfile | KcContextBase.UpdateUserProfile
| KcContextBase.IdpReviewUserProfile; | KcContextBase.IdpReviewUserProfile
| KcContextBase.Password;
export type WebauthnAuthenticator = {
credentialId: string;
transports: {
iconClass: KcTemplateClassKey;
displayNameProperties: MessageKeyBase[];
};
label: string;
createdAt: string;
};
export declare namespace KcContextBase { export declare namespace KcContextBase {
export type Common = { export namespace Common {
url: { export type Login = {
loginAction: string; url: {
resourcesPath: string; loginAction: string;
resourcesCommonPath: string; resourcesPath: string;
loginRestartFlowUrl: string; resourcesCommonPath: string;
loginUrl: string; loginRestartFlowUrl: string;
loginUrl: string;
};
realm: {
name: string;
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
registrationEmailAsUsername: boolean;
};
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
auth?: {
showUsername?: boolean;
showResetCredentials?: boolean;
showTryAnotherWayLink?: boolean;
attemptedUsername?: string;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
client: {
clientId: string;
name?: string;
description?: string;
};
isAppInitiatedAction: boolean;
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
existsError: (fieldName: string) => boolean;
get: (fieldName: string) => string;
exists: (fieldName: string) => boolean;
};
}; };
realm: {
name: string;
displayName?: string;
displayNameHtml?: string;
internationalizationEnabled: boolean;
registrationEmailAsUsername: boolean;
};
/** Undefined if !realm.internationalizationEnabled */
locale?: {
supported: {
url: string;
label: string;
languageTag: string;
}[];
currentLanguageTag: string;
};
auth?: {
showUsername?: boolean;
showResetCredentials?: boolean;
showTryAnotherWayLink?: boolean;
attemptedUsername?: string;
};
scripts: string[];
message?: {
type: "success" | "warning" | "error" | "info";
summary: string;
};
client: {
clientId: string;
name?: string;
description?: string;
};
isAppInitiatedAction: boolean;
messagesPerField: {
printIfExists: <T>(fieldName: string, x: T) => T | undefined;
existsError: (fieldName: string) => boolean;
get: (fieldName: string) => string;
exists: (fieldName: string) => boolean;
};
};
export type Login = Common & { export type Account = {
url: {
resourcesPath: string;
resourcesCommonPath: string;
};
};
}
export type Login = Common.Login & {
pageId: "login.ftl"; pageId: "login.ftl";
url: { url: {
loginResetCredentialsUrl: string; loginResetCredentialsUrl: string;
@ -126,25 +126,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type RegisterCommon = Common & { export type Register = RegisterUserProfile.CommonWithLegacy & {
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[];
};
};
export type Register = RegisterCommon & {
pageId: "register.ftl"; pageId: "register.ftl";
register: { register: {
formData: { formData: {
@ -157,7 +139,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type RegisterUserProfile = RegisterCommon & { export type RegisterUserProfile = RegisterUserProfile.CommonWithLegacy & {
pageId: "register-user-profile.ftl"; pageId: "register-user-profile.ftl";
profile: { profile: {
context: "REGISTRATION_PROFILE"; context: "REGISTRATION_PROFILE";
@ -166,7 +148,27 @@ export declare namespace KcContextBase {
}; };
}; };
export type Info = Common & { export namespace RegisterUserProfile {
export type CommonWithLegacy = Common.Login & {
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
social: {
displayInfo: boolean;
providers?: {
loginUrl: string;
alias: string;
providerId: string;
displayName: string;
}[];
};
};
}
export type Info = Common.Login & {
pageId: "info.ftl"; pageId: "info.ftl";
messageHeader?: string; messageHeader?: string;
requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKeyBase>[]; requiredActions?: ExtractAfterStartingWith<"requiredAction.", MessageKeyBase>[];
@ -178,22 +180,22 @@ export declare namespace KcContextBase {
}; };
}; };
export type Error = Common & { export type Error = Common.Login & {
pageId: "error.ftl"; pageId: "error.ftl";
client?: { client?: {
baseUrl?: string; baseUrl?: string;
}; };
message: NonNullable<Common["message"]>; message: NonNullable<Common.Login["message"]>;
}; };
export type LoginResetPassword = Common & { export type LoginResetPassword = Common.Login & {
pageId: "login-reset-password.ftl"; pageId: "login-reset-password.ftl";
realm: { realm: {
loginWithEmailAllowed: boolean; loginWithEmailAllowed: boolean;
}; };
}; };
export type LoginVerifyEmail = Common & { export type LoginVerifyEmail = Common.Login & {
pageId: "login-verify-email.ftl"; pageId: "login-verify-email.ftl";
//NOTE: Optional because maybe it wasn't defined in older keycloak versions. //NOTE: Optional because maybe it wasn't defined in older keycloak versions.
user?: { user?: {
@ -201,18 +203,18 @@ export declare namespace KcContextBase {
}; };
}; };
export type Terms = Common & { export type Terms = Common.Login & {
pageId: "terms.ftl"; pageId: "terms.ftl";
}; };
export type LoginOtp = Common & { export type LoginOtp = Common.Login & {
pageId: "login-otp.ftl"; pageId: "login-otp.ftl";
otpLogin: { otpLogin: {
userOtpCredentials: { id: string; userLabel: string }[]; userOtpCredentials: { id: string; userLabel: string }[];
}; };
}; };
export type LoginUsername = Common & { export type LoginUsername = Common.Login & {
pageId: "login-username.ftl"; pageId: "login-username.ftl";
url: { url: {
loginResetCredentialsUrl: string; loginResetCredentialsUrl: string;
@ -242,7 +244,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type LoginPassword = Common & { export type LoginPassword = Common.Login & {
pageId: "login-password.ftl"; pageId: "login-password.ftl";
url: { url: {
loginResetCredentialsUrl: string; loginResetCredentialsUrl: string;
@ -265,10 +267,10 @@ export declare namespace KcContextBase {
}; };
}; };
export type WebauthnAuthenticate = Common & { export type WebauthnAuthenticate = Common.Login & {
pageId: "webauthn-authenticate.ftl"; pageId: "webauthn-authenticate.ftl";
authenticators: { authenticators: {
authenticators: WebauthnAuthenticator[]; authenticators: WebauthnAuthenticate.WebauthnAuthenticator[];
}; };
challenge: string; challenge: string;
// I hate this: // I hate this:
@ -283,12 +285,24 @@ export declare namespace KcContextBase {
login: {}; login: {};
}; };
export type LoginUpdatePassword = Common & { export namespace WebauthnAuthenticate {
export type WebauthnAuthenticator = {
credentialId: string;
transports: {
iconClass: KcTemplateClassKey;
displayNameProperties: MessageKeyBase[];
};
label: string;
createdAt: string;
};
}
export type LoginUpdatePassword = Common.Login & {
pageId: "login-update-password.ftl"; pageId: "login-update-password.ftl";
username: string; username: string;
}; };
export type LoginUpdateProfile = Common & { export type LoginUpdateProfile = Common.Login & {
pageId: "login-update-profile.ftl"; pageId: "login-update-profile.ftl";
user: { user: {
editUsernameAllowed: boolean; editUsernameAllowed: boolean;
@ -299,12 +313,12 @@ export declare namespace KcContextBase {
}; };
}; };
export type LoginIdpLinkConfirm = Common & { export type LoginIdpLinkConfirm = Common.Login & {
pageId: "login-idp-link-confirm.ftl"; pageId: "login-idp-link-confirm.ftl";
idpAlias: string; idpAlias: string;
}; };
export type LoginIdpLinkEmail = Common & { export type LoginIdpLinkEmail = Common.Login & {
pageId: "login-idp-link-email.ftl"; pageId: "login-idp-link-email.ftl";
brokerContext: { brokerContext: {
username: string; username: string;
@ -312,11 +326,11 @@ export declare namespace KcContextBase {
idpAlias: string; idpAlias: string;
}; };
export type LoginPageExpired = Common & { export type LoginPageExpired = Common.Login & {
pageId: "login-page-expired.ftl"; pageId: "login-page-expired.ftl";
}; };
export type LoginConfigTotp = Common & { export type LoginConfigTotp = Common.Login & {
pageId: "login-config-totp.ftl"; pageId: "login-config-totp.ftl";
mode?: "qr" | "manual" | undefined | null; mode?: "qr" | "manual" | undefined | null;
totp: { totp: {
@ -344,7 +358,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type LogoutConfirm = Common & { export type LogoutConfirm = Common.Login & {
pageId: "logout-confirm.ftl"; pageId: "logout-confirm.ftl";
url: { url: {
logoutConfirmAction: string; logoutConfirmAction: string;
@ -358,7 +372,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type UpdateUserProfile = Common & { export type UpdateUserProfile = Common.Login & {
pageId: "update-user-profile.ftl"; pageId: "update-user-profile.ftl";
profile: { profile: {
attributes: Attribute[]; attributes: Attribute[];
@ -366,7 +380,7 @@ export declare namespace KcContextBase {
}; };
}; };
export type IdpReviewUserProfile = Common & { export type IdpReviewUserProfile = Common.Login & {
pageId: "idp-review-user-profile.ftl"; pageId: "idp-review-user-profile.ftl";
profile: { profile: {
context: "IDP_REVIEW"; context: "IDP_REVIEW";
@ -374,6 +388,13 @@ export declare namespace KcContextBase {
attributesByName: Record<string, Attribute>; attributesByName: Record<string, Attribute>;
}; };
}; };
export type Password = Common.Account & {
pageId: "password.ftl";
url: {
passwordUrl: string;
};
};
} }
export type Attribute = { export type Attribute = {
@ -493,4 +514,4 @@ export declare namespace Validators {
}; };
} }
assert<Equals<KcContextBase["pageId"], LoginThemePageId>>(); assert<Equals<KcContextBase["pageId"], LoginThemePageId | AccountThemePageId>>();

14
src/lib/getClassName.ts Normal file
View File

@ -0,0 +1,14 @@
import { clsx } from "keycloakify/tools/clsx";
export function useGetClassName<ClassKey extends string>(params: {
defaultClasses?: Record<ClassKey, string | undefined>;
classes?: Partial<Record<ClassKey, string>>;
}) {
const { defaultClasses, classes } = params;
const getClassName = (classKey: ClassKey): string => {
return clsx(classKey, defaultClasses?.[classKey], classes?.[classKey]);
};
return { getClassName };
}

View File

@ -0,0 +1,45 @@
import { useEffect } from "react";
import { memoize } from "../tools/memoize";
import { fallbackLanguageTag } from "../i18n";
import { useConst } from "../tools/useConst";
import { useConstCallback } from "../tools/useConstCallback";
import { assert } from "tsafe/assert";
export type KcContextLike = {
pageId: KcContextBase["pageId"];
locale?: {
currentLanguageTag: string;
};
};
assert<KcContextBase extends KcContextLike ? true : false>();
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextLike;
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
}) {
const { kcContext } = params;
const { downloadTermMarkdownMemoized } = (function useClosure() {
const { downloadTermMarkdown } = params;
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
const downloadTermMarkdownMemoized = useConst(() =>
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }))
);
return { downloadTermMarkdownMemoized };
})();
useEffect(() => {
if (kcContext.pageId !== "terms.ftl") {
return;
}
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
);
}, []);
}

View File

@ -0,0 +1,74 @@
import { useReducer, useEffect } from "react";
import { headInsert } from "../tools/headInsert";
import { pathJoin } from "../bin/tools/pathJoin";
import { clsx } from "../tools/clsx";
export function usePrepareTemplate(params: {
doFetchDefaultThemeResources: boolean;
stylesCommon?: string[];
styles?: string[];
scripts?: string[];
url: {
resourcesCommonPath: string;
resourcesPath: string;
};
htmlClassName: string;
}) {
const { doFetchDefaultThemeResources, stylesCommon, styles, url, scripts, htmlClassName } = params;
const [isReady, setReady] = useReducer(() => true, !doFetchDefaultThemeResources);
useEffect(() => {
if (!doFetchDefaultThemeResources) {
return;
}
let isUnmounted = false;
Promise.all(
[
...(stylesCommon ?? []).map(relativePath => pathJoin(url.resourcesCommonPath, relativePath)),
...(styles ?? []).map(relativePath => pathJoin(url.resourcesPath, relativePath))
]
.reverse()
.map(href =>
headInsert({
"type": "css",
href,
"position": "prepend"
})
)
).then(() => {
if (isUnmounted) {
return;
}
setReady();
});
(scripts ?? []).forEach(relativePath =>
headInsert({
"type": "javascript",
"src": pathJoin(url.resourcesPath, relativePath)
})
);
return () => {
isUnmounted = true;
};
}, []);
useEffect(() => {
const htmlClassList = document.getElementsByTagName("html")[0].classList;
const tokens = clsx(htmlClassName).split(" ");
htmlClassList.add(...tokens);
return () => {
htmlClassList.remove(...tokens);
};
}, [htmlClassName]);
return { isReady };
}

159
src/pages/PageProps.ts Normal file
View File

@ -0,0 +1,159 @@
import type { LazyExoticComponent } from "react";
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { assert } from "tsafe/assert";
import type { I18nBase } from "../i18n";
import type { ThemeType } from "../bin/keycloakify/generateFtl";
import type { Equals } from "tsafe";
import type { TemplateProps } from "../templates/TemplateProps";
export const defaultKcProps = {
"login": {
...defaultKcTemplateProps.login,
"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"],
"kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"],
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
"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"],
// user-profile grouping
"kcFormGroupHeader": ["pf-c-form__group"],
// 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"],
"kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
"kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
"kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
"kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
"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"],
"kcFormOptionsWrapperClass": []
},
"account": {
...defaultKcTemplateProps.account
}
};
assert<Equals<keyof typeof defaultKcProps, ThemeType>>();
assert<typeof defaultKcProps.login extends KcProps.Login ? true : false>();
assert<typeof defaultKcProps.account extends KcProps.Account ? true : false>();
export type PageProps<KcContext, I18n extends I18nBase> = {
Template: LazyExoticComponent<(props: TemplateProps<any, any>) => JSX.Element | null>;
kcContext: KcContext;
i18n: I18n;
templateProps: Pick<TemplateProps<any, any>, "doUseDefaultCss" | "classes">;
classes?: Partial<
Record<
| "kcLogoLink"
| "kcLogoClass"
| "kcContainerClass"
| "kcContentClass"
| "kcFeedbackAreaClass"
| "kcLocaleClass"
| "kcAlertIconClasserror"
| "kcFormAreaClass"
| "kcFormSocialAccountListClass"
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass"
| "kcInputWrapperClass"
| "kcFormOptionsClass"
| "kcFormButtonsClass"
| "kcFormSettingClass"
| "kcTextareaClass"
| "kcInfoAreaClass"
| "kcFormGroupHeader"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonDefaultClass"
| "kcButtonLargeClass"
| "kcButtonBlockClass"
| "kcInputLargeClass"
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass"
| "kcSelectOTPListClass"
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass",
string
>
>;
};

View File

@ -64,42 +64,3 @@ export default function Terms(props: PageProps<Extract<KcContextBase, { pageId:
} }
export const evtTermMarkdown = Evt.create<string | undefined>(undefined); export const evtTermMarkdown = Evt.create<string | undefined>(undefined);
export type KcContextLike = {
pageId: KcContextBase["pageId"];
locale?: {
currentLanguageTag: string;
};
};
assert<Extends<KcContextBase, KcContextLike>>();
/** Allow to avoid bundling the terms and download it on demand*/
export function useDownloadTerms(params: {
kcContext: KcContextLike;
downloadTermMarkdown: (params: { currentLanguageTag: string }) => Promise<string>;
}) {
const { kcContext } = params;
const { downloadTermMarkdownMemoized } = (function useClosure() {
const { downloadTermMarkdown } = params;
const downloadTermMarkdownConst = useConstCallback(downloadTermMarkdown);
const downloadTermMarkdownMemoized = useConst(() =>
memoize((currentLanguageTag: string) => downloadTermMarkdownConst({ currentLanguageTag }))
);
return { downloadTermMarkdownMemoized };
})();
useEffect(() => {
if (kcContext.pageId !== "terms.ftl") {
return;
}
downloadTermMarkdownMemoized(kcContext.locale?.currentLanguageTag ?? fallbackLanguageTag).then(
thermMarkdown => (evtTermMarkdown.state = thermMarkdown)
);
}, []);
}

178
src/pages/index.ts Normal file
View File

@ -0,0 +1,178 @@
import { allPropertiesValuesToUndefined } from "../tools/allPropertiesValuesToUndefined";
import { assert } from "tsafe/assert";
import type { I18nBase } from "../i18n";
import type { ThemeType } from "../bin/keycloakify/generateFtl";
import type { Equals } from "tsafe";
import type { LoginThemeTemplateProps } from "../templates/LoginThemeTemplate";
export namespace KcProps {
export type Login = KcPropsGeneric<
| KcTemplateClassKey.Login
| "kcLogoLink"
| "kcLogoClass"
| "kcContainerClass"
| "kcContentClass"
| "kcFeedbackAreaClass"
| "kcLocaleClass"
| "kcAlertIconClasserror"
| "kcFormAreaClass"
| "kcFormSocialAccountListClass"
| "kcFormSocialAccountDoubleListClass"
| "kcFormSocialAccountListLinkClass"
| "kcWebAuthnKeyIcon"
| "kcWebAuthnDefaultIcon"
| "kcFormClass"
| "kcFormGroupErrorClass"
| "kcLabelClass"
| "kcInputClass"
| "kcInputErrorMessageClass"
| "kcInputWrapperClass"
| "kcFormOptionsClass"
| "kcFormButtonsClass"
| "kcFormSettingClass"
| "kcTextareaClass"
| "kcInfoAreaClass"
| "kcFormGroupHeader"
| "kcButtonClass"
| "kcButtonPrimaryClass"
| "kcButtonDefaultClass"
| "kcButtonLargeClass"
| "kcButtonBlockClass"
| "kcInputLargeClass"
| "kcSrOnlyClass"
| "kcSelectAuthListClass"
| "kcSelectAuthListItemClass"
| "kcSelectAuthListItemFillClass"
| "kcSelectAuthListItemInfoClass"
| "kcSelectAuthListItemLeftClass"
| "kcSelectAuthListItemBodyClass"
| "kcSelectAuthListItemDescriptionClass"
| "kcSelectAuthListItemHeadingClass"
| "kcSelectAuthListItemHelpTextClass"
| "kcSelectAuthListItemIconPropertyClass"
| "kcSelectAuthListItemIconClass"
| "kcSelectAuthListItemTitle"
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass"
| "kcSelectOTPListClass"
| "kcSelectOTPListItemClass"
| "kcAuthenticatorOtpCircleClass"
| "kcSelectOTPItemHeadingClass"
| "kcFormOptionsWrapperClass"
>;
export type Account = KcPropsGeneric<KcTemplateClassKey.Account>;
}
export const defaultKcProps = {
"login": {
...defaultKcTemplateProps.login,
"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"],
"kcWebAuthnDefaultIcon": ["pficon", "pficon-key"],
"kcFormClass": ["form-horizontal"],
"kcFormGroupErrorClass": ["has-error"],
"kcLabelClass": ["control-label"],
"kcInputClass": ["form-control"],
"kcInputErrorMessageClass": ["pf-c-form__helper-text", "pf-m-error", "required", "kc-feedback-text"],
"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"],
// user-profile grouping
"kcFormGroupHeader": ["pf-c-form__group"],
// 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"],
"kcSelectAuthListItemFillClass": ["pf-l-split__item", "pf-m-fill"],
"kcSelectAuthListItemIconPropertyClass": ["fa-2x", "select-auth-box-icon-properties"],
"kcSelectAuthListItemIconClass": ["pf-l-split__item", "select-auth-box-icon"],
"kcSelectAuthListItemTitle": ["select-auth-box-paragraph"],
"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"],
"kcFormOptionsWrapperClass": []
},
"account": {
...defaultKcTemplateProps.account
}
};
assert<Equals<keyof typeof defaultKcProps, ThemeType>>();
assert<typeof defaultKcProps.login extends KcProps.Login ? true : false>();
assert<typeof defaultKcProps.account extends KcProps.Account ? true : false>();
/** To use if you don't want any default */
export const allClearKcProps = {
"login": allPropertiesValuesToUndefined(defaultKcProps.login),
"account": allPropertiesValuesToUndefined(defaultKcProps.account)
};
assert<Equals<keyof typeof allClearKcProps, ThemeType>>();
export namespace PageProps {
export type Login<KcContext, I18n extends I18nBase> = {
kcContext: KcContext;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
LoginThemeTemplate: (props: LoginThemeTemplateProps<any, any>) => JSX.Element | null;
} & KcProps.Login;
export type Account<KcContext, I18n extends I18nBase> = {
kcContext: KcContext;
i18n: I18n;
doFetchDefaultThemeResources?: boolean;
AccountThemeTemplate: (props: LoginThemeTemplateProps<any, any>) => JSX.Element | null;
} & KcProps.Account;
}

View File

@ -7,7 +7,11 @@
"target": "ES2017", "target": "ES2017",
"lib": ["es2015", "DOM", "ES2019.Object"], "lib": ["es2015", "DOM", "ES2019.Object"],
"moduleResolution": "node", "moduleResolution": "node",
"jsx": "react", "baseUrl": ".",
"paths": {
"keycloakify/*": ["./*"]
},
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"exclude": ["./test", "./bin"], "exclude": ["./test", "./bin"],