diff --git a/package.json b/package.json index 40d762a5..9d093c58 100755 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "dependencies": { "cheerio": "^1.0.0-rc.5", "evt": "^1.9.12", + "minimal-polyfills": "^2.1.6", "powerhooks": "^0.0.14", "tss-react": "^0.0.9" } diff --git a/src/bin/build-keycloak-theme/generateFtl/ftl2js.ftl b/src/bin/build-keycloak-theme/generateFtl/ftl2js.ftl index 54486631..1daca7c5 100644 --- a/src/bin/build-keycloak-theme/generateFtl/ftl2js.ftl +++ b/src/bin/build-keycloak-theme/generateFtl/ftl2js.ftl @@ -4,12 +4,19 @@ "loginAction": "${url.loginAction}", "resourcesPath": "${url.resourcesPath}", "resourcesCommonPath": "${url.resourcesCommonPath}", - "loginRestartFlowUrl": "${url.loginRestartFlowUrl}" + "loginRestartFlowUrl": "${url.loginRestartFlowUrl}", + "loginResetCredentialsUrl": "${url.loginResetCredentialsUrl}", + "registrationUrl": "${url.registrationUrl}" }, "realm": { "displayName": "${realm.displayName!''}" || undefined, "displayNameHtml": "${realm.displayNameHtml!''}" || undefined, - "internationalizationEnabled": ${realm.internationalizationEnabled?c} + "internationalizationEnabled": ${realm.internationalizationEnabled?c}, + "password": ${realm.password?c}, + "loginWithEmailAllowed": ${realm.loginWithEmailAllowed?c}, + "registrationEmailAsUsername": ${realm.registrationEmailAsUsername?c}, + "rememberMe": ${realm.rememberMe?c}, + "resetPasswordAllowed": ${realm.resetPasswordAllowed?c} }, "locale": (function (){ @@ -54,6 +61,7 @@ "showUsername": ${auth.showUsername()?c}, "showResetCredentials": ${auth.showResetCredentials()?c}, "showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c} + "selectedCredential": "${auth.selectedCredential!''}" || undefined }; <#if auth.showUsername() && !auth.showResetCredentials()> @@ -79,7 +87,7 @@ <#if scripts??> <#list scripts as script> - out.push("${script}"); + out.push("${script}"); #list> #if> @@ -107,7 +115,59 @@ #if> return false; - })() + })(), + "social": { + "displayInfo": ${social.displayInfo?c}, + "providers": (()=>{ + <#if social.providers??> + + var out= []; + + <#list social.providers as p> + out.push({ + "loginUrl": "${p.loginUrl}", + "alias": "${p.alias}", + "providerId": "${p.providerId}", + "displayName": "${p.displayName}" + }); + #list> + + return out; + + #if> + + return undefined; + + })() + }, + "usernameEditDisabled": (function () { + + <#if usernameEditDisabled??> + return true; + #if> + return false; + + })(), + "login": { + "username": "${login.username!''}" || undefined, + "rememberMe": (function (){ + + <#if login.rememberMe??> + return true; + #if> + return false; + + + })() + }, + "registrationDisabled": (function (){ + + <#if registrationDisabled??> + return true; + #if> + return false; + + }) } \ No newline at end of file diff --git a/src/lib/LoginPage.tsx b/src/lib/LoginPage.tsx index 9540e829..7836b2a7 100644 --- a/src/lib/LoginPage.tsx +++ b/src/lib/LoginPage.tsx @@ -1,18 +1,154 @@ -/* -import { useState, memo } from "react"; -import { KcProperties, Template } from "./Template"; -import { assert } from "evt/tools/typeSafety/assert"; -import { keycloakPagesContext } from "./keycloakFtlValues"; +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 Props = { - properties: KcProperties; +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" }; -export const LoginPage = memo((props: Props)=>{ - const [{ }] = useState(() => { +/** 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); @@ -20,12 +156,145 @@ export const LoginPage = memo((props: Props)=>{ }); + const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false); - return ( - + const onSubmit = useConstCallback(() => + (setIsLoginButtonDisabled(true), true) ); -}); -*/ -export {}; \ No newline at end of file + return ( + + + { + realm.password && + ( + + + + { + !realm.loginWithEmailAllowed ? + t("username") + : + ( + !realm.registrationEmailAsUsername ? + t("usernameOrEmail") : + t("email") + ) + } + + + + + + + + {t("password")} + + + + + + + { + ( + realm.rememberMe && + !usernameEditDisabled + ) && + + + {t("rememberMe")} + + + } + + + { + realm.resetPasswordAllowed && + + {t("doForgotPassword")} + + } + + + + + + + + + + ) + } + + { + (realm.password && social.providers !== undefined) && + + 4 && properties.kcFormSocialAccountDoubleListClass)}> + { + social.providers.map(p => + + + {p.displayName} + + + ) + } + + + } + + } + displayInfoNode={ + ( + realm.password && + realm.resetPasswordAllowed && + !registrationDisabled + ) && + + + {t("noAccount")} + + {t("doRegister")} + + + + } + /> + ); +}); + + diff --git a/src/lib/Template.tsx b/src/lib/Template.tsx index 12e3b84a..93c2dcb2 100644 --- a/src/lib/Template.tsx +++ b/src/lib/Template.tsx @@ -1,6 +1,6 @@ +import { useState, useEffect, memo } from "react"; import type { ReactNode } from "react"; -import { useState, useEffect } from "react"; import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation"; import { keycloakPagesContext } from "./keycloakFtlValues"; import { assert } from "evt/tools/typeSafety/assert"; @@ -12,53 +12,84 @@ import { appendLinkInHead } from "./tools/appendLinkInHead"; import { appendScriptInHead } from "./tools/appendScriptInHead"; import { join as pathJoin } from "path"; import { useConstCallback } from "powerhooks"; - -type KcClasses = { [key in T]?: string[] | string }; - -export type KcProperties = { - 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" ->; +import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined"; export type Props = { displayInfo?: boolean; - displayMessage: boolean; - displayRequiredFields: boolean; - displayWide: boolean; - showAnotherWayIfPresent: boolean; - properties?: KcProperties; + displayMessage?: boolean; + displayRequiredFields?: boolean; + displayWide?: boolean; + showAnotherWayIfPresent?: boolean; + properties: KcTemplateProperties; headerNode: ReactNode; showUsernameNode: ReactNode; formNode: ReactNode; displayInfoNode: ReactNode; }; -export function Template(props: Props) { +/** Class names can be provided as an array or separated by whitespace */ +export type KcClasses = { [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) =>{ const { displayInfo = false, @@ -75,6 +106,8 @@ export function Template(props: Props) { const { t } = useKeycloakThemeTranslation(); + Object.assign(properties, defaultKcTemplateProperties); + const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage(); const onChangeLanguageClickFactory = useCallbackFactory( @@ -286,4 +319,4 @@ export function Template(props: Props) { ); -} +}); diff --git a/src/lib/i18n/useKeycloakTranslation.tsx b/src/lib/i18n/useKeycloakTranslation.tsx index 8397a663..77abf2e9 100644 --- a/src/lib/i18n/useKeycloakTranslation.tsx +++ b/src/lib/i18n/useKeycloakTranslation.tsx @@ -3,18 +3,18 @@ import { useKeycloakLanguage } from "./useKeycloakLanguage"; import { messages } from "./generated_messages/login"; import { useConstCallback } from "powerhooks"; import type { ReactNode } from "react"; +import { id } from "evt/tools/typeSafety/id"; -export type MessageKey = keyof typeof messages["en"] - +export type MessageKey = keyof typeof messages["en"]; export function useKeycloakThemeTranslation() { const { keycloakLanguage } = useKeycloakLanguage(); - const t = useConstCallback( - (key: MessageKey, ...args: (string | undefined)[]): ReactNode => { + const tStr = useConstCallback( + (key: MessageKey, ...args: (string | undefined)[]): string => { - let out: string = messages[keycloakLanguage as any as "en"][key] ?? messages["en"][key]; + let str: string = messages[keycloakLanguage as any as "en"][key] ?? messages["en"][key]; args.forEach((arg, i) => { @@ -22,15 +22,22 @@ export function useKeycloakThemeTranslation() { return; } - out = out.replace(new RegExp(`\\{${i}\\}`, "g"), arg); + str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg); }); - return ; + return str; } ); - return { t }; + const t = useConstCallback( + id<(...args: Parameters) => ReactNode>( + (key, ...args) => + + ) + ); + + return { t, tStr }; } \ No newline at end of file diff --git a/src/lib/keycloakFtlValues.ts b/src/lib/keycloakFtlValues.ts index f32d27f2..4924e761 100644 --- a/src/lib/keycloakFtlValues.ts +++ b/src/lib/keycloakFtlValues.ts @@ -13,12 +13,19 @@ export type KeycloakFtlValues = { resourcesPath: string; resourcesCommonPath: string; loginRestartFlowUrl: string; - }, + loginResetCredentialsUrl: string; + registrationUrl: string; + }; realm: { displayName?: string; displayNameHtml?: string; internationalizationEnabled: boolean; - }, + password: boolean; + loginWithEmailAllowed: boolean; + registrationEmailAsUsername: boolean; + rememberMe: boolean; + resetPasswordAllowed: boolean; + }; /** Undefined if !realm.internationalizationEnabled */ locale?: { supported: { @@ -38,13 +45,29 @@ export type KeycloakFtlValues = { showResetCredentials: boolean; showTryAnotherWayLink: boolean; attemptedUsername?: boolean; - }, + selectedCredential?: string; + }; scripts: string[]; message?: { type: "success" | "warning" | "error" | "info"; summary: string; - }, + }; isAppInitiatedAction: boolean; + social: { + displayInfo: boolean; + providers?: { + loginUrl: string; + alias: string; + providerId: string; + displayName: string; + }[] + }; + usernameEditDisabled: boolean; + login: { + username?: string; + rememberMe: boolean; + }; + registrationDisabled: boolean; }; export const { keycloakPagesContext } = diff --git a/src/lib/tools/allPropertiesValuesToUndefined.ts b/src/lib/tools/allPropertiesValuesToUndefined.ts new file mode 100644 index 00000000..51537f19 --- /dev/null +++ b/src/lib/tools/allPropertiesValuesToUndefined.ts @@ -0,0 +1,10 @@ + + +import "minimal-polyfills/Object.fromEntries"; + +export function allPropertiesValuesToUndefined>(obj: T): Record { + return Object.fromEntries( + Object.entries(obj) + .map(([key]) => [key, undefined]) + ) as any; +} diff --git a/tsconfig.json b/tsconfig.json index fa46bf0f..f7be1215 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "module": "CommonJS", "target": "es5", - "lib": ["es2015", "DOM"], + "lib": ["es2015", "DOM", "ES2019.Object"], "esModuleInterop": true, "declaration": true, "outDir": "./dist", diff --git a/yarn.lock b/yarn.lock index 61155dd6..402b23bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -816,7 +816,7 @@ memoizee@^0.4.15: next-tick "^1.1.0" timers-ext "^0.1.7" -minimal-polyfills@^2.1.5: +minimal-polyfills@^2.1.5, minimal-polyfills@^2.1.6: version "2.1.6" resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.1.6.tgz#eab50832add31afd40a22b38fb76d1fdcd2a51e4" integrity sha512-vqoxj7eMzsqX0M6/dkgoNFPw6Mztgn5qjSl0bWGboQeU7Y4UPLeyoqQw6JI+0qmBcJYdkr3nK7dqY8u/fgRp5g==