feat: add select-authenticator page

This commit is contained in:
Void 2023-03-31 17:38:22 +02:00
parent b0db8caf65
commit e7837aea88
5 changed files with 133 additions and 2 deletions

View File

@ -36,7 +36,8 @@ export const loginThemePageIds = [
"logout-confirm.ftl", "logout-confirm.ftl",
"update-user-profile.ftl", "update-user-profile.ftl",
"idp-review-user-profile.ftl", "idp-review-user-profile.ftl",
"update-email.ftl" "update-email.ftl",
"select-authenticator.ftl"
] as const; ] as const;
export const accountThemePageIds = ["password.ftl", "account.ftl"] as const; export const accountThemePageIds = ["password.ftl", "account.ftl"] as const;

View File

@ -26,6 +26,7 @@ const LogoutConfirm = lazy(() => import("keycloakify/login/pages/LogoutConfirm")
const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile")); const UpdateUserProfile = lazy(() => import("keycloakify/login/pages/UpdateUserProfile"));
const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile")); const IdpReviewUserProfile = lazy(() => import("keycloakify/login/pages/IdpReviewUserProfile"));
const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail")); const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
const SelectAuthenticator = lazy(() => import("keycloakify/login/pages/SelectAuthenticator"));
export default function Fallback(props: PageProps<KcContext, I18n>) { export default function Fallback(props: PageProps<KcContext, I18n>) {
const { kcContext, ...rest } = props; const { kcContext, ...rest } = props;
@ -78,6 +79,8 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
return <IdpReviewUserProfile kcContext={kcContext} {...rest} />; return <IdpReviewUserProfile kcContext={kcContext} {...rest} />;
case "update-email.ftl": case "update-email.ftl":
return <UpdateEmail kcContext={kcContext} {...rest} />; return <UpdateEmail kcContext={kcContext} {...rest} />;
case "select-authenticator.ftl":
return <SelectAuthenticator kcContext={kcContext} {...rest} />;
} }
assert<Equals<typeof kcContext, never>>(false); assert<Equals<typeof kcContext, never>>(false);
})()} })()}

View File

@ -31,7 +31,8 @@ export type KcContext =
| KcContext.LogoutConfirm | KcContext.LogoutConfirm
| KcContext.UpdateUserProfile | KcContext.UpdateUserProfile
| KcContext.IdpReviewUserProfile | KcContext.IdpReviewUserProfile
| KcContext.UpdateEmail; | KcContext.UpdateEmail
| KcContext.SelectAuthenticator;
export declare namespace KcContext { export declare namespace KcContext {
export type Common = { export type Common = {
@ -389,6 +390,39 @@ export declare namespace KcContext {
value?: string; value?: string;
}; };
}; };
export type SelectAuthenticator = Common & {
pageId: "select-authenticator.ftl";
auth: {
authenticationSelections: SelectAuthenticator.AuthenticationSelection[];
};
};
export namespace SelectAuthenticator {
export type AuthenticationSelection = {
authExecId: string;
displayName:
| "otp-display-name"
| "password-display-name"
| "auth-username-form-display-name"
| "auth-username-password-form-display-name"
| "webauthn-display-name"
| "webauthn-passwordless-display-name";
helpText:
| "otp-help-text"
| "password-help-text"
| "auth-username-form-help-text"
| "auth-username-password-form-help-text"
| "webauthn-help-text"
| "webauthn-passwordless-help-text";
iconCssClass?:
| "kcAuthenticatorDefaultClass"
| "kcAuthenticatorPasswordClass"
| "kcAuthenticatorOTPClass"
| "kcAuthenticatorWebAuthnClass"
| "kcAuthenticatorWebAuthnPasswordlessClass";
};
}
} }
export type Attribute = { export type Attribute = {

View File

@ -498,5 +498,25 @@ export const kcContextMocks: KcContext[] = [
"email": { "email": {
value: "email@example.com" value: "email@example.com"
} }
}),
id<KcContext.SelectAuthenticator>({
...kcContextCommonMock,
pageId: "select-authenticator.ftl",
auth: {
authenticationSelections: [
{
authExecId: "f607f83c-537e-42b7-99d7-c52d459afe84",
displayName: "otp-display-name",
helpText: "otp-help-text",
iconCssClass: "kcAuthenticatorOTPClass"
},
{
authExecId: "5ed881b1-84cd-4e9b-b4d9-f329ea61a58c",
displayName: "webauthn-display-name",
helpText: "webauthn-help-text",
iconCssClass: "kcAuthenticatorWebAuthnClass"
}
]
}
}) })
]; ];

View File

@ -0,0 +1,73 @@
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "keycloakify/login/kcContext";
import type { I18n } from "keycloakify/login/i18n";
import { MouseEvent, useRef } from "react";
import { useConstCallback } from "keycloakify/tools/useConstCallback";
export default function SelectAuthenticator(props: PageProps<Extract<KcContext, { pageId: "select-authenticator.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { url, auth } = kcContext;
const { getClassName } = useGetClassName({ doUseDefaultCss, classes });
const { msg } = i18n;
const selectCredentialsForm = useRef<HTMLFormElement>(null);
const authExecIdInput = useRef<HTMLInputElement>(null);
const submitForm = useConstCallback(() => {
selectCredentialsForm.current?.submit();
});
const onSelectedAuthenticator = useConstCallback((event: MouseEvent<HTMLDivElement>) => {
const divElement = event.currentTarget;
const authExecId = divElement.dataset.authExecId;
if (!authExecIdInput.current || !authExecId) {
return;
}
authExecIdInput.current.value = authExecId;
submitForm();
});
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("loginChooseAuthenticator")}>
<form
id="kc-select-credential-form"
className={getClassName("kcFormClass")}
ref={selectCredentialsForm}
action={url.loginAction}
method="post"
>
<div className={getClassName("kcSelectAuthListClass")}>
{auth.authenticationSelections.map((authenticationSelection, index) => (
<div key={index} className={getClassName("kcSelectAuthListItemClass")}>
<div
style={{ cursor: "pointer" }}
onClick={onSelectedAuthenticator}
data-auth-exec-id={authenticationSelection.authExecId}
className={getClassName("kcSelectAuthListItemInfoClass")}
>
<div className={getClassName("kcSelectAuthListItemLeftClass")}>
<span className={getClassName(authenticationSelection.iconCssClass ?? "kcAuthenticatorDefaultClass")}></span>
</div>
<div className={getClassName("kcSelectAuthListItemBodyClass")}>
<div className={getClassName("kcSelectAuthListItemDescriptionClass")}>
<div className={getClassName("kcSelectAuthListItemHeadingClass")}>
{msg(authenticationSelection.displayName)}
</div>
<div className={getClassName("kcSelectAuthListItemHelpTextClass")}>
{msg(authenticationSelection.helpText)}
</div>
</div>
</div>
</div>
</div>
))}
<input type="hidden" id="authexec-hidden-input" name="authenticationExecution" ref={authExecIdInput} />
</div>
</form>
</Template>
);
}