Implement login

This commit is contained in:
Joseph Garrone 2021-03-02 22:48:36 +01:00
parent 6a93c6e894
commit e5e39d036d
9 changed files with 475 additions and 72 deletions

View File

@ -55,6 +55,7 @@
"dependencies": { "dependencies": {
"cheerio": "^1.0.0-rc.5", "cheerio": "^1.0.0-rc.5",
"evt": "^1.9.12", "evt": "^1.9.12",
"minimal-polyfills": "^2.1.6",
"powerhooks": "^0.0.14", "powerhooks": "^0.0.14",
"tss-react": "^0.0.9" "tss-react": "^0.0.9"
} }

View File

@ -4,12 +4,19 @@
"loginAction": "${url.loginAction}", "loginAction": "${url.loginAction}",
"resourcesPath": "${url.resourcesPath}", "resourcesPath": "${url.resourcesPath}",
"resourcesCommonPath": "${url.resourcesCommonPath}", "resourcesCommonPath": "${url.resourcesCommonPath}",
"loginRestartFlowUrl": "${url.loginRestartFlowUrl}" "loginRestartFlowUrl": "${url.loginRestartFlowUrl}",
"loginResetCredentialsUrl": "${url.loginResetCredentialsUrl}",
"registrationUrl": "${url.registrationUrl}"
}, },
"realm": { "realm": {
"displayName": "${realm.displayName!''}" || undefined, "displayName": "${realm.displayName!''}" || undefined,
"displayNameHtml": "${realm.displayNameHtml!''}" || 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 (){ "locale": (function (){
@ -54,6 +61,7 @@
"showUsername": ${auth.showUsername()?c}, "showUsername": ${auth.showUsername()?c},
"showResetCredentials": ${auth.showResetCredentials()?c}, "showResetCredentials": ${auth.showResetCredentials()?c},
"showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c} "showTryAnotherWayLink": ${auth.showTryAnotherWayLink()?c}
"selectedCredential": "${auth.selectedCredential!''}" || undefined
}; };
<#if auth.showUsername() && !auth.showResetCredentials()> <#if auth.showUsername() && !auth.showResetCredentials()>
@ -107,7 +115,59 @@
</#if> </#if>
return false; 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;
})
} }
</script> </script>

View File

@ -1,18 +1,154 @@
/* import { useState, memo } from "react";
import { useState, memo } from "react"; import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
import { KcProperties, Template } from "./Template"; import { Template, defaultKcTemplateProperties } from "./Template";
import { assert } from "evt/tools/typeSafety/assert"; import type { KcTemplateProperties, KcClasses } from "./Template";
import { keycloakPagesContext } from "./keycloakFtlValues"; 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 = { export type Props = {
properties: KcProperties; properties?: KcLoginPageProperties;
}; };
export const LoginPage = memo((props: Props) => { export const LoginPage = memo((props: Props) => {
const { properties = {} } = props;
const [{ }] = useState(() => { const { t, tStr } = useKeycloakThemeTranslation();
Object.assign(properties, defaultKcLoginPageProperties);
const [{
social, realm, url,
usernameEditDisabled, login,
auth, registrationDisabled
}] = useState(() => {
assert(keycloakPagesContext !== undefined); assert(keycloakPagesContext !== undefined);
@ -20,12 +156,145 @@ export const LoginPage = memo((props: Props)=>{
}); });
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
return ( const onSubmit = useConstCallback(() =>
<Template/> (setIsLoginButtonDisabled(true), true)
); );
});
*/
export {}; 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

@ -1,6 +1,6 @@
import { useState, useEffect, memo } from "react";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useState, useEffect } from "react";
import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation"; import { useKeycloakThemeTranslation } from "./i18n/useKeycloakTranslation";
import { keycloakPagesContext } from "./keycloakFtlValues"; import { keycloakPagesContext } from "./keycloakFtlValues";
import { assert } from "evt/tools/typeSafety/assert"; import { assert } from "evt/tools/typeSafety/assert";
@ -12,10 +12,25 @@ import { appendLinkInHead } from "./tools/appendLinkInHead";
import { appendScriptInHead } from "./tools/appendScriptInHead"; import { appendScriptInHead } from "./tools/appendScriptInHead";
import { join as pathJoin } from "path"; import { join as pathJoin } from "path";
import { useConstCallback } from "powerhooks"; import { useConstCallback } from "powerhooks";
import { allPropertiesValuesToUndefined } from "./tools/allPropertiesValuesToUndefined";
type KcClasses<T extends string> = { [key in T]?: string[] | string }; export type Props = {
displayInfo?: boolean;
displayMessage?: boolean;
displayRequiredFields?: boolean;
displayWide?: boolean;
showAnotherWayIfPresent?: boolean;
properties: KcTemplateProperties;
headerNode: ReactNode;
showUsernameNode: ReactNode;
formNode: ReactNode;
displayInfoNode: ReactNode;
};
export type KcProperties = { /** 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[]; stylesCommon?: string[];
styles?: string[]; styles?: string[];
scripts?: string[]; scripts?: string[];
@ -45,20 +60,36 @@ export type KcProperties = {
"kcInfoAreaWrapperClass" "kcInfoAreaWrapperClass"
>; >;
export type Props = { export const defaultKcTemplateProperties: KcTemplateProperties = {
displayInfo?: boolean; "styles": ["css/login.css"],
displayMessage: boolean; "stylesCommon": [
displayRequiredFields: boolean; ...[".min.css", "-additions.min.css"]
displayWide: boolean; .map(end => `node_modules/patternfly/dist/css/patternfly${end}`),
showAnotherWayIfPresent: boolean; "lib/zocial/zocial.css"
properties?: KcProperties; ],
headerNode: ReactNode; "kcLoginClass": "login-pf-page",
showUsernameNode: ReactNode; "kcContentWrapperClass": "row",
formNode: ReactNode; "kcHeaderClass": "login-pf-page-header",
displayInfoNode: ReactNode; "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"
}; };
export function Template(props: Props) { /** Tu use if you don't want any default */
export const allClearKcTemplateProperties =
allPropertiesValuesToUndefined(defaultKcTemplateProperties);
export const Template = memo((props: Props) =>{
const { const {
displayInfo = false, displayInfo = false,
@ -75,6 +106,8 @@ export function Template(props: Props) {
const { t } = useKeycloakThemeTranslation(); const { t } = useKeycloakThemeTranslation();
Object.assign(properties, defaultKcTemplateProperties);
const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage(); const { keycloakLanguage, setKeycloakLanguage } = useKeycloakLanguage();
const onChangeLanguageClickFactory = useCallbackFactory( const onChangeLanguageClickFactory = useCallbackFactory(
@ -286,4 +319,4 @@ export function Template(props: Props) {
</div > </div >
</div > </div >
); );
} });

View File

@ -3,18 +3,18 @@ import { useKeycloakLanguage } from "./useKeycloakLanguage";
import { messages } from "./generated_messages/login"; import { messages } from "./generated_messages/login";
import { useConstCallback } from "powerhooks"; import { useConstCallback } from "powerhooks";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { id } from "evt/tools/typeSafety/id";
export type MessageKey = keyof typeof messages["en"] export type MessageKey = keyof typeof messages["en"];
export function useKeycloakThemeTranslation() { export function useKeycloakThemeTranslation() {
const { keycloakLanguage } = useKeycloakLanguage(); const { keycloakLanguage } = useKeycloakLanguage();
const t = useConstCallback( const tStr = useConstCallback(
(key: MessageKey, ...args: (string | undefined)[]): ReactNode => { (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) => { args.forEach((arg, i) => {
@ -22,15 +22,22 @@ export function useKeycloakThemeTranslation() {
return; return;
} }
out = out.replace(new RegExp(`\\{${i}\\}`, "g"), arg); str = str.replace(new RegExp(`\\{${i}\\}`, "g"), arg);
}); });
return <span className={key} dangerouslySetInnerHTML={{ "__html": out }} />; return str;
} }
); );
return { t }; const t = useConstCallback(
id<(...args: Parameters<typeof tStr>) => ReactNode>(
(key, ...args) =>
<span className={key} dangerouslySetInnerHTML={{ "__html": tStr(key, ...args) }} />
)
);
return { t, tStr };
} }

View File

@ -13,12 +13,19 @@ export type KeycloakFtlValues = {
resourcesPath: string; resourcesPath: string;
resourcesCommonPath: string; resourcesCommonPath: string;
loginRestartFlowUrl: string; loginRestartFlowUrl: string;
}, loginResetCredentialsUrl: string;
registrationUrl: string;
};
realm: { realm: {
displayName?: string; displayName?: string;
displayNameHtml?: string; displayNameHtml?: string;
internationalizationEnabled: boolean; internationalizationEnabled: boolean;
}, password: boolean;
loginWithEmailAllowed: boolean;
registrationEmailAsUsername: boolean;
rememberMe: boolean;
resetPasswordAllowed: boolean;
};
/** Undefined if !realm.internationalizationEnabled */ /** Undefined if !realm.internationalizationEnabled */
locale?: { locale?: {
supported: { supported: {
@ -38,13 +45,29 @@ export type KeycloakFtlValues = {
showResetCredentials: boolean; showResetCredentials: boolean;
showTryAnotherWayLink: boolean; showTryAnotherWayLink: boolean;
attemptedUsername?: boolean; attemptedUsername?: boolean;
}, selectedCredential?: string;
};
scripts: string[]; scripts: string[];
message?: { message?: {
type: "success" | "warning" | "error" | "info"; type: "success" | "warning" | "error" | "info";
summary: string; summary: string;
}, };
isAppInitiatedAction: boolean; 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 } = export const { keycloakPagesContext } =

View File

@ -0,0 +1,10 @@
import "minimal-polyfills/Object.fromEntries";
export function allPropertiesValuesToUndefined<T extends Record<string, unknown>>(obj: T): Record<keyof T, undefined> {
return Object.fromEntries(
Object.entries(obj)
.map(([key]) => [key, undefined])
) as any;
}

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"module": "CommonJS", "module": "CommonJS",
"target": "es5", "target": "es5",
"lib": ["es2015", "DOM"], "lib": ["es2015", "DOM", "ES2019.Object"],
"esModuleInterop": true, "esModuleInterop": true,
"declaration": true, "declaration": true,
"outDir": "./dist", "outDir": "./dist",

View File

@ -816,7 +816,7 @@ memoizee@^0.4.15:
next-tick "^1.1.0" next-tick "^1.1.0"
timers-ext "^0.1.7" timers-ext "^0.1.7"
minimal-polyfills@^2.1.5: minimal-polyfills@^2.1.5, minimal-polyfills@^2.1.6:
version "2.1.6" version "2.1.6"
resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.1.6.tgz#eab50832add31afd40a22b38fb76d1fdcd2a51e4" resolved "https://registry.yarnpkg.com/minimal-polyfills/-/minimal-polyfills-2.1.6.tgz#eab50832add31afd40a22b38fb76d1fdcd2a51e4"
integrity sha512-vqoxj7eMzsqX0M6/dkgoNFPw6Mztgn5qjSl0bWGboQeU7Y4UPLeyoqQw6JI+0qmBcJYdkr3nK7dqY8u/fgRp5g== integrity sha512-vqoxj7eMzsqX0M6/dkgoNFPw6Mztgn5qjSl0bWGboQeU7Y4UPLeyoqQw6JI+0qmBcJYdkr3nK7dqY8u/fgRp5g==