File structure update

This commit is contained in:
Joseph Garrone 2024-05-06 19:16:17 +02:00
parent fb4a7d2ba3
commit 96f0e6df2a
9 changed files with 218 additions and 115 deletions

View File

@ -3,10 +3,12 @@ import type { PageProps } from "keycloakify/login/pages/PageProps";
import { assert, type Equals } from "tsafe/assert"; import { assert, type Equals } from "tsafe/assert";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
import type { KcContext } from "./kcContext"; import type { KcContext } from "./kcContext";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { TermsAcceptanceProps } from "keycloakify/login/TermsAcceptance";
const Login = lazy(() => import("keycloakify/login/pages/Login")); const Login = lazy(() => import("keycloakify/login/pages/Login"));
const Register = lazy(() => import("keycloakify/login/pages/Register")); const Register = lazy(() => import("keycloakify/login/pages/Register"));
const RegisterUserProfile = lazy(() => import("keycloakify/login/pages/RegisterUserProfile"));
const Info = lazy(() => import("keycloakify/login/pages/Info")); const Info = lazy(() => import("keycloakify/login/pages/Info"));
const Error = lazy(() => import("keycloakify/login/pages/Error")); const Error = lazy(() => import("keycloakify/login/pages/Error"));
const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword")); const LoginResetPassword = lazy(() => import("keycloakify/login/pages/LoginResetPassword"));
@ -31,7 +33,12 @@ const UpdateEmail = lazy(() => import("keycloakify/login/pages/UpdateEmail"));
const SelectAuthenticator = lazy(() => import("keycloakify/login/pages/SelectAuthenticator")); const SelectAuthenticator = lazy(() => import("keycloakify/login/pages/SelectAuthenticator"));
const SamlPostForm = lazy(() => import("keycloakify/login/pages/SamlPostForm")); const SamlPostForm = lazy(() => import("keycloakify/login/pages/SamlPostForm"));
export default function Fallback(props: PageProps<KcContext, I18n>) { type FallbackProps = PageProps<KcContext, I18n> & {
UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
TermsAcceptance: LazyOrNot<(props: TermsAcceptanceProps) => JSX.Element | null>;
};
export default function Fallback(props: FallbackProps) {
const { kcContext, ...rest } = props; const { kcContext, ...rest } = props;
return ( return (
@ -41,9 +48,8 @@ export default function Fallback(props: PageProps<KcContext, I18n>) {
case "login.ftl": case "login.ftl":
return <Login kcContext={kcContext} {...rest} />; return <Login kcContext={kcContext} {...rest} />;
case "register.ftl": case "register.ftl":
return <Register kcContext={kcContext} {...rest} />;
case "register-user-profile.ftl": case "register-user-profile.ftl":
return <RegisterUserProfile kcContext={kcContext} {...rest} />; return <Register kcContext={kcContext} {...rest} />;
case "info.ftl": case "info.ftl":
return <Info kcContext={kcContext} {...rest} />; return <Info kcContext={kcContext} {...rest} />;
case "error.ftl": case "error.ftl":

View File

@ -27,6 +27,7 @@ export type ClassKey =
| "kcInfoAreaWrapperClass" | "kcInfoAreaWrapperClass"
| "kcFormButtonsWrapperClass" | "kcFormButtonsWrapperClass"
| "kcFormOptionsWrapperClass" | "kcFormOptionsWrapperClass"
| "kcCheckboxInputClass"
| "kcLocaleDropDownClass" | "kcLocaleDropDownClass"
| "kcLocaleListItemClass" | "kcLocaleListItemClass"
| "kcContentWrapperClass" | "kcContentWrapperClass"

View File

@ -5,7 +5,7 @@ import { evtTermMarkdown } from "keycloakify/login/lib/useDownloadTerms";
import type { KcContext } from "keycloakify/login/kcContext/KcContext"; import type { KcContext } from "keycloakify/login/kcContext/KcContext";
import type { I18n } from "./i18n"; import type { I18n } from "./i18n";
export type PropsOfTermsAcceptance = { export type TermsAcceptanceProps = {
kcContext: KcContextLike; kcContext: KcContextLike;
i18n: I18n; i18n: I18n;
getClassName: (classKey: ClassKey) => string; getClassName: (classKey: ClassKey) => string;
@ -16,7 +16,7 @@ type KcContextLike = {
messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">; messagesPerField: Pick<KcContext.Common["messagesPerField"], "existsError" | "get">;
}; };
export function TermsAcceptance(props: PropsOfTermsAcceptance) { export function TermsAcceptance(props: TermsAcceptanceProps) {
const { const {
kcContext: { termsAcceptanceRequired = false } kcContext: { termsAcceptanceRequired = false }
} = props; } = props;
@ -28,7 +28,7 @@ export function TermsAcceptance(props: PropsOfTermsAcceptance) {
return <TermsAcceptanceEnabled {...props} />; return <TermsAcceptanceEnabled {...props} />;
} }
export function TermsAcceptanceEnabled(props: PropsOfTermsAcceptance) { export function TermsAcceptanceEnabled(props: TermsAcceptanceProps) {
const { const {
i18n, i18n,
getClassName, getClassName,

View File

@ -3,8 +3,8 @@ import type { ClassKey } from "keycloakify/login/TemplateProps";
import { clsx } from "keycloakify/tools/clsx"; import { clsx } from "keycloakify/tools/clsx";
import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm"; import { useUserProfileForm, type KcContextLike, type FormAction, type FormFieldError } from "keycloakify/login/lib/useUserProfileForm";
import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext"; import type { Attribute, LegacyAttribute } from "keycloakify/login/kcContext/KcContext";
import type { I18n } from "../../i18n";
import { assert } from "tsafe/assert"; import { assert } from "tsafe/assert";
import type { I18n } from "./i18n";
export type UserProfileFormFieldsProps = { export type UserProfileFormFieldsProps = {
kcContext: KcContextLike; kcContext: KcContextLike;
@ -26,7 +26,7 @@ type BeforeAfterFieldProps = {
// NOTE: Enabled by default but it's a UX best practice to set it to false. // NOTE: Enabled by default but it's a UX best practice to set it to false.
const doMakeUserConfirmPassword = true; const doMakeUserConfirmPassword = true;
export function UserProfileFormFields(props: UserProfileFormFieldsProps) { export default function UserProfileFormFields(props: UserProfileFormFieldsProps) {
const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props; const { kcContext, onIsFormSubmittableValueChange, i18n, getClassName, BeforeField, AfterField } = props;
const { advancedMsg } = i18n; const { advancedMsg } = i18n;
@ -260,7 +260,7 @@ function FieldErrors(props: {
); );
} }
type PropsOfInputFiledByType = { type InputFiledByTypeProps = {
attribute: Attribute; attribute: Attribute;
valueOrValues: string | string[]; valueOrValues: string | string[];
displayableErrors: FormFieldError[]; displayableErrors: FormFieldError[];
@ -269,7 +269,7 @@ type PropsOfInputFiledByType = {
i18n: I18n; i18n: I18n;
}; };
function InputFiledByType(props: PropsOfInputFiledByType) { function InputFiledByType(props: InputFiledByTypeProps) {
const { attribute, valueOrValues } = props; const { attribute, valueOrValues } = props;
switch (attribute.annotations.inputType) { switch (attribute.annotations.inputType) {
@ -296,7 +296,7 @@ function InputFiledByType(props: PropsOfInputFiledByType) {
} }
} }
function InputTag(props: PropsOfInputFiledByType & { fieldIndex: number | undefined }) { function InputTag(props: InputFiledByTypeProps & { fieldIndex: number | undefined }) {
const { attribute, fieldIndex, getClassName, formValidationDispatch, valueOrValues, i18n, displayableErrors } = props; const { attribute, fieldIndex, getClassName, formValidationDispatch, valueOrValues, i18n, displayableErrors } = props;
return ( return (
@ -511,7 +511,7 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
); );
} }
function InputTagSelects(props: PropsOfInputFiledByType) { function InputTagSelects(props: InputFiledByTypeProps) {
const { attribute, formValidationDispatch, getClassName, valueOrValues } = props; const { attribute, formValidationDispatch, getClassName, valueOrValues } = props;
const { advancedMsg } = props.i18n; const { advancedMsg } = props.i18n;
@ -619,7 +619,7 @@ function InputTagSelects(props: PropsOfInputFiledByType) {
); );
} }
function TextareaTag(props: PropsOfInputFiledByType) { function TextareaTag(props: InputFiledByTypeProps) {
const { attribute, formValidationDispatch, getClassName, displayableErrors, valueOrValues } = props; const { attribute, formValidationDispatch, getClassName, displayableErrors, valueOrValues } = props;
assert(typeof valueOrValues === "string"); assert(typeof valueOrValues === "string");
@ -655,7 +655,7 @@ function TextareaTag(props: PropsOfInputFiledByType) {
); );
} }
function SelectTag(props: PropsOfInputFiledByType) { function SelectTag(props: InputFiledByTypeProps) {
const { attribute, formValidationDispatch, getClassName, displayableErrors, i18n, valueOrValues } = props; const { attribute, formValidationDispatch, getClassName, displayableErrors, i18n, valueOrValues } = props;
const { advancedMsg } = i18n; const { advancedMsg } = i18n;

View File

@ -13,7 +13,6 @@ type ExtractAfterStartingWith<Prefix extends string, StrEnum> = StrEnum extends
export type KcContext = export type KcContext =
| KcContext.Login | KcContext.Login
| KcContext.Register | KcContext.Register
| KcContext.RegisterUserProfile
| KcContext.Info | KcContext.Info
| KcContext.Error | KcContext.Error
| KcContext.LoginResetPassword | KcContext.LoginResetPassword
@ -190,37 +189,23 @@ export declare namespace KcContext {
*/ */
export type Register = Common & { export type Register = Common & {
pageId: "register.ftl"; pageId: "register.ftl" | "register-user-profile.ftl";
profile: { profile: {
attributes: Attribute[]; attributes: Attribute[];
attributesByName: Record<string, Attribute>; attributesByName: Record<string, Attribute>;
html5DataAnnotations: Record<string, string>; html5DataAnnotations: Record<string, string>;
}; };
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
/** /**
* Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension * Theses values are added by: https://github.com/jcputney/keycloak-theme-additional-info-extension
* A Keycloak Java extension used as dependency in Keycloakify. * A Keycloak Java extension used as dependency in Keycloakify.
*/ */
passwordPolicies?: PasswordPolicies; passwordPolicies?: PasswordPolicies;
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
};
export type RegisterUserProfile = Common & {
pageId: "register-user-profile.ftl";
profile: {
attributes: LegacyAttribute[];
attributesByName: Record<string, LegacyAttribute>;
};
url: {
registrationAction: string;
};
passwordRequired: boolean;
recaptchaRequired: boolean;
recaptchaSiteKey?: string;
}; };
export type Info = Common & { export type Info = Common & {

View File

@ -12,6 +12,7 @@ export const { useGetClassName } = createUseClassName<ClassKey>({
"kcLocaleDropDownClass": undefined, "kcLocaleDropDownClass": undefined,
"kcLocaleListItemClass": undefined, "kcLocaleListItemClass": undefined,
"kcContentWrapperClass": undefined, "kcContentWrapperClass": undefined,
"kcCheckboxInputClass": undefined,
"kcLogoIdP-facebook": "fa fa-facebook", "kcLogoIdP-facebook": "fa fa-facebook",
"kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg", "kcAuthenticatorOTPClass": "fa fa-mobile list-view-pf-icon-lg",

View File

@ -5,15 +5,15 @@ import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext"; import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n"; import type { I18n } from "../i18n";
import type { LazyOrNot } from "keycloakify/tools/LazyOrNot"; import type { LazyOrNot } from "keycloakify/tools/LazyOrNot";
import type { PropsOfUserProfileFormFields } from "keycloakify/login/UserProfileFormFields"; import type { UserProfileFormFieldsProps } from "keycloakify/login/UserProfileFormFields";
import type { PropsOfTermsAcceptance } from "../TermsAcceptance"; import type { TermsAcceptanceProps } from "../TermsAcceptance";
type Props = PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n> & { export type PropsOfRegister = PageProps<Extract<KcContext, { pageId: "register.ftl" | "register-user-profile.ftl" }>, I18n> & {
UserProfileFormFields: LazyOrNot<(props: PropsOfUserProfileFormFields) => JSX.Element>; UserProfileFormFields: LazyOrNot<(props: UserProfileFormFieldsProps) => JSX.Element>;
TermsAcceptance: LazyOrNot<(props: PropsOfTermsAcceptance) => JSX.Element | null>; TermsAcceptance: LazyOrNot<(props: TermsAcceptanceProps) => JSX.Element | null>;
}; };
export default function Register(props: Props) { export default function Register(props: PropsOfRegister) {
const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, TermsAcceptance } = props; const { kcContext, i18n, doUseDefaultCss, Template, classes, UserProfileFormFields, TermsAcceptance } = props;
const { getClassName } = useGetClassName({ const { getClassName } = useGetClassName({

View File

@ -1,72 +0,0 @@
import { useState } from "react";
import { clsx } from "keycloakify/tools/clsx";
import { UserProfileFormFields } from "./shared/UserProfileFormFields";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function RegisterUserProfile(props: PageProps<Extract<KcContext, { pageId: "register-user-profile.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey, realm } = kcContext;
realm.registrationEmailAsUsername;
const { msg, msgStr } = i18n;
const [isFormSubmittable, setIsFormSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doUseDefaultCss, classes }}
displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true}
headerNode={msg("registerTitle")}
>
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<UserProfileFormFields
kcContext={kcContext}
onIsFormSubmittableValueChange={setIsFormSubmittable}
i18n={i18n}
getClassName={getClassName}
/>
{recaptchaRequired && (
<div className="form-group">
<div className={getClassName("kcInputWrapperClass")}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")} style={{ "marginBottom": 30 }}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
type="submit"
value={msgStr("doRegister")}
disabled={!isFormSubmittable}
/>
</div>
</div>
</form>
</Template>
);
}

View File

@ -0,0 +1,182 @@
import { clsx } from "keycloakify/tools/clsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import { useGetClassName } from "keycloakify/login/lib/useGetClassName";
import type { KcContext } from "../kcContext";
import type { I18n } from "../i18n";
export default function Register(props: PageProps<Extract<KcContext, { pageId: "register.ftl" }>, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
const { getClassName } = useGetClassName({
doUseDefaultCss,
classes
});
const { url, messagesPerField, register, realm, passwordRequired, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n;
return (
<Template {...{ kcContext, i18n, doUseDefaultCss, classes }} headerNode={msg("registerTitle")}>
<form id="kc-register-form" className={getClassName("kcFormClass")} action={url.registrationAction} method="post">
<div
className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("firstName", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="firstName" className={getClassName("kcLabelClass")}>
{msg("firstName")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
type="text"
id="firstName"
className={getClassName("kcInputClass")}
name="firstName"
defaultValue={register.formData.firstName ?? ""}
/>
</div>
</div>
<div
className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("lastName", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="lastName" className={getClassName("kcLabelClass")}>
{msg("lastName")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
type="text"
id="lastName"
className={getClassName("kcInputClass")}
name="lastName"
defaultValue={register.formData.lastName ?? ""}
/>
</div>
</div>
<div
className={clsx(getClassName("kcFormGroupClass"), messagesPerField.printIfExists("email", getClassName("kcFormGroupErrorClass")))}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="email" className={getClassName("kcLabelClass")}>
{msg("email")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
type="text"
id="email"
className={getClassName("kcInputClass")}
name="email"
defaultValue={register.formData.email ?? ""}
autoComplete="email"
/>
</div>
</div>
{!realm.registrationEmailAsUsername && (
<div
className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("username", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="username" className={getClassName("kcLabelClass")}>
{msg("username")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
type="text"
id="username"
className={getClassName("kcInputClass")}
name="username"
defaultValue={register.formData.username ?? ""}
autoComplete="username"
/>
</div>
</div>
)}
{passwordRequired && (
<>
<div
className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("password", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="password" className={getClassName("kcLabelClass")}>
{msg("password")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input
type="password"
id="password"
className={getClassName("kcInputClass")}
name="password"
autoComplete="new-password"
/>
</div>
</div>
<div
className={clsx(
getClassName("kcFormGroupClass"),
messagesPerField.printIfExists("password-confirm", getClassName("kcFormGroupErrorClass"))
)}
>
<div className={getClassName("kcLabelWrapperClass")}>
<label htmlFor="password-confirm" className={getClassName("kcLabelClass")}>
{msg("passwordConfirm")}
</label>
</div>
<div className={getClassName("kcInputWrapperClass")}>
<input type="password" id="password-confirm" className={getClassName("kcInputClass")} name="password-confirm" />
</div>
</div>
</>
)}
{recaptchaRequired && (
<div className="form-group">
<div className={getClassName("kcInputWrapperClass")}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey}></div>
</div>
</div>
)}
<div className={getClassName("kcFormGroupClass")}>
<div id="kc-form-options" className={getClassName("kcFormOptionsClass")}>
<div className={getClassName("kcFormOptionsWrapperClass")}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={getClassName("kcFormButtonsClass")}>
<input
className={clsx(
getClassName("kcButtonClass"),
getClassName("kcButtonPrimaryClass"),
getClassName("kcButtonBlockClass"),
getClassName("kcButtonLargeClass")
)}
type="submit"
value={msgStr("doRegister")}
/>
</div>
</div>
</form>
</Template>
);
}