Merge remote-tracking branch 'origin/main' into lordvlad/issue125

* origin/main:
  Update KcApp.tsx
  Add RegisterUserProfile customization
This commit is contained in:
Waldemar Reusch 2023-03-12 01:03:58 +01:00
commit 3faa482df1
5 changed files with 308 additions and 88 deletions

View File

@ -1,87 +1,67 @@
{
"name": "keycloakify-starter",
"homepage": "https://starter.keycloakify.dev",
"version": "3.0.0",
"description": "A starter/demo project for keycloakify",
"repository": {
"type": "git",
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build-keycloak-theme": "yarn build ; keycloakify",
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2",
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public"
},
"keycloakify": {
"extraPages": [
"my-extra-page-1.ftl",
"my-extra-page-2.ftl"
]
},
"author": "u/garronej",
"license": "MIT",
"keywords": [],
"dependencies": {
"evt": "^2.4.15",
"jwt-decode": "^3.1.2",
"keycloak-js": "^21.0.1",
"keycloakify": "^6.12.4",
"powerhooks": "^0.26.2",
"react": "18.1.0",
"react-dom": "18.1.0",
"tsafe": "^1.4.3"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.16",
"@storybook/addon-essentials": "^6.5.16",
"@storybook/addon-interactions": "^6.5.16",
"@storybook/addon-links": "^6.5.16",
"@storybook/builder-webpack5": "^6.5.16",
"@storybook/manager-webpack5": "^6.5.16",
"@storybook/node-logger": "^6.5.16",
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.16",
"@storybook/testing-library": "^0.0.13",
"@types/node": "^15.3.1",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.4",
"react-scripts": "5.0.0",
"typescript": "~4.8.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-redeclare": "off",
"no-labels": "off"
"name": "keycloakify-starter",
"homepage": "https://starter.keycloakify.dev",
"version": "3.1.0",
"description": "A starter/demo project for keycloakify",
"repository": {
"type": "git",
"url": "git://github.com/codegouvfr/keycloakify-starter.git"
},
"overrides": [
{
"files": [
"**/*.stories.*"
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"build-keycloak-theme": "yarn build && keycloakify",
"download-builtin-keycloak-theme": "download-builtin-keycloak-theme 15.0.2"
},
"keycloakify": {
"extraPages": [
"my-extra-page-1.ftl",
"my-extra-page-2.ftl"
]
},
"author": "u/garronej",
"license": "MIT",
"keywords": [],
"dependencies": {
"evt": "^2.4.15",
"jwt-decode": "^3.1.2",
"keycloak-js": "^21.0.1",
"keycloakify": "^6.12.7",
"powerhooks": "^0.26.2",
"react": "18.1.0",
"react-dom": "18.1.0",
"tsafe": "^1.4.3"
},
"devDependencies": {
"@types/node": "^15.3.1",
"@types/react": "18.0.9",
"@types/react-dom": "18.0.4",
"react-scripts": "5.0.0",
"typescript": "~4.8.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"rules": {
"import/no-anonymous-default-export": "off"
"react-hooks/exhaustive-deps": "off",
"@typescript-eslint/no-redeclare": "off",
"no-labels": "off"
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -5,13 +5,15 @@ import { useI18n } from "./i18n";
import Fallback, { defaultKcProps, type KcProps, type PageProps } from "keycloakify";
import Template from "./Template";
import DefaultTemplate from "keycloakify/lib/Template";
import { foo, bar } from "./valuesTransferredOverUrl";
console.log(`Values passed by the main app in the URL parameter:`, { foo, bar });
// You can uncomment this to see the values passed by the main app before redirecting.
//import { foo, bar } from "./valuesTransferredOverUrl";
//console.log(`Values passed by the main app in the URL parameter:`, { foo, bar });
const Login = lazy(()=> import("./pages/Login"));
// If you can, favor register-user-profile.ftl over register.ftl, see: https://docs.keycloakify.dev/realtime-input-validation
const Register = lazy(() => import("./pages/Register"));
const RegisterUserProfile = lazy(() => import("./pages/RegisterUserProfile"));
const Terms = lazy(() => import("./pages/Terms"));
const MyExtraPage1 = lazy(() => import("./pages/MyExtraPage1"));
const MyExtraPage2 = lazy(() => import("./pages/MyExtraPage2"));
@ -61,6 +63,7 @@ export default function App(props: { kcContext: KcContext; }) {
switch (kcContext.pageId) {
case "login.ftl": return <Login {...{ kcContext, ...pageProps }} />;
case "register.ftl": return <Register {...{ kcContext, ...pageProps }} />;
case "register-user-profile.ftl": return <RegisterUserProfile {...{ kcContext, ...pageProps }} />
case "terms.ftl": return <Terms {...{ kcContext, ...pageProps }} />;
case "my-extra-page-1.ftl": return <MyExtraPage1 {...{ kcContext, ...pageProps }} />;
case "my-extra-page-2.ftl": return <MyExtraPage2 {...{ kcContext, ...pageProps }} />;

View File

@ -0,0 +1,61 @@
// Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/RegisterUserProfile.tsx
import { useState } from "react";
import { clsx } from "keycloakify/lib/tools/clsx";
import { UserProfileFormFields } from "./shared/UserProfileCommons";
import type { PageProps } from "keycloakify/lib/KcProps";
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, doFetchDefaultThemeResources = true, Template, ...kcProps } = props;
const { url, messagesPerField, recaptchaRequired, recaptchaSiteKey } = kcContext;
const { msg, msgStr } = i18n;
const [isFomSubmittable, setIsFomSubmittable] = useState(false);
return (
<Template
{...{ kcContext, i18n, doFetchDefaultThemeResources, ...kcProps }}
displayMessage={messagesPerField.exists("global")}
displayRequiredFields={true}
headerNode={msg("registerTitle")}
formNode={
<form id="kc-register-form" className={clsx(kcProps.kcFormClass)} action={url.registrationAction} method="post">
<UserProfileFormFields kcContext={kcContext} onIsFormSubmittableValueChange={setIsFomSubmittable} i18n={i18n} {...kcProps} />
{recaptchaRequired && (
<div className="form-group">
<div className={clsx(kcProps.kcInputWrapperClass)}>
<div className="g-recaptcha" data-size="compact" data-sitekey={recaptchaSiteKey} />
</div>
</div>
)}
<div className={clsx(kcProps.kcFormGroupClass)} style={{ "marginBottom": 30 }}>
<div id="kc-form-options" className={clsx(kcProps.kcFormOptionsClass)}>
<div className={clsx(kcProps.kcFormOptionsWrapperClass)}>
<span>
<a href={url.loginUrl}>{msg("backToLogin")}</a>
</span>
</div>
</div>
<div id="kc-form-buttons" className={clsx(kcProps.kcFormButtonsClass)}>
<input
className={clsx(
kcProps.kcButtonClass,
kcProps.kcButtonPrimaryClass,
kcProps.kcButtonBlockClass,
kcProps.kcButtonLargeClass
)}
type="submit"
value={msgStr("doRegister")}
disabled={!isFomSubmittable}
/>
</div>
</div>
</form>
}
/>
);
}

View File

@ -0,0 +1,176 @@
//NOTE: Copy pasted from: https://github.com/InseeFrLab/keycloakify/blob/main/src/lib/pages/shared/UserProfileCommons.tsx
import { useEffect, Fragment } from "react";
import type { KcProps } from "keycloakify/lib/KcProps";
import { clsx } from "keycloakify/lib/tools/clsx";
import type { I18nBase } from "keycloakify/lib/i18n";
import type { Attribute } from "keycloakify/lib/getKcContext";
import { useFormValidation } from "keycloakify/lib/pages/shared/UserProfileCommons";
export type UserProfileFormFieldsProps = {
kcContext: Parameters<typeof useFormValidation>[0]["kcContext"];
i18n: I18nBase;
} & KcProps &
Partial<Record<"BeforeField" | "AfterField", (props: { attribute: Attribute }) => JSX.Element | null>> & {
onIsFormSubmittableValueChange: (isFormSubmittable: boolean) => void;
};
export function UserProfileFormFields({
kcContext,
onIsFormSubmittableValueChange,
i18n,
BeforeField,
AfterField,
...props
}: UserProfileFormFieldsProps) {
const { advancedMsg } = i18n;
const {
formValidationState: { fieldStateByAttributeName, isFormSubmittable },
formValidationDispatch,
attributesWithPassword
} = useFormValidation({
kcContext,
i18n
});
useEffect(() => {
onIsFormSubmittableValueChange(isFormSubmittable);
}, [isFormSubmittable]);
let currentGroup = "";
return (
<>
{attributesWithPassword.map((attribute, i) => {
const { group = "", groupDisplayHeader = "", groupDisplayDescription = "" } = attribute;
const { value, displayableErrors } = fieldStateByAttributeName[attribute.name];
const formGroupClassName = clsx(props.kcFormGroupClass, displayableErrors.length !== 0 && props.kcFormGroupErrorClass);
return (
<Fragment key={i}>
{group !== currentGroup && (currentGroup = group) !== "" && (
<div className={formGroupClassName}>
<div className={clsx(props.kcContentWrapperClass)}>
<label id={`header-${group}`} className={clsx(props.kcFormGroupHeader)}>
{advancedMsg(groupDisplayHeader) || currentGroup}
</label>
</div>
{groupDisplayDescription !== "" && (
<div className={clsx(props.kcLabelWrapperClass)}>
<label id={`description-${group}`} className={`${clsx(props.kcLabelClass)}`}>
{advancedMsg(groupDisplayDescription)}
</label>
</div>
)}
</div>
)}
{BeforeField && <BeforeField attribute={attribute} />}
<div className={formGroupClassName}>
<div className={clsx(props.kcLabelWrapperClass)}>
<label htmlFor={attribute.name} className={clsx(props.kcLabelClass)}>
{advancedMsg(attribute.displayName ?? "")}
</label>
{attribute.required && <>*</>}
</div>
<div className={clsx(props.kcInputWrapperClass)}>
{(() => {
const { options } = attribute.validators;
if (options !== undefined) {
return (
<select
id={attribute.name}
name={attribute.name}
onChange={event =>
formValidationDispatch({
"action": "update value",
"name": attribute.name,
"newValue": event.target.value
})
}
onBlur={() =>
formValidationDispatch({
"action": "focus lost",
"name": attribute.name
})
}
value={value}
>
{options.options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
);
}
return (
<input
type={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "password";
default:
return "text";
}
})()}
id={attribute.name}
name={attribute.name}
value={value}
onChange={event =>
formValidationDispatch({
"action": "update value",
"name": attribute.name,
"newValue": event.target.value
})
}
onBlur={() =>
formValidationDispatch({
"action": "focus lost",
"name": attribute.name
})
}
className={clsx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
autoComplete={attribute.autocomplete}
/>
);
})()}
{displayableErrors.length !== 0 &&
(() => {
const divId = `input-error-${attribute.name}`;
return (
<>
<style>{`#${divId} > span: { display: block; }`}</style>
<span
id={divId}
className={clsx(props.kcInputErrorMessageClass)}
style={{
"position": displayableErrors.length === 1 ? "absolute" : undefined
}}
aria-live="polite"
>
{displayableErrors.map(({ errorMessage }) => errorMessage)}
</span>
</>
);
})()}
</div>
</div>
{AfterField && <AfterField attribute={attribute} />}
</Fragment>
);
})}
</>
);
}

View File

@ -10336,15 +10336,15 @@ keycloak-js@^21.0.1:
base64-js "^1.5.1"
js-sha256 "^0.9.0"
keycloakify@^6.12.4:
version "6.12.4"
resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-6.12.4.tgz#96aa3cab5e3f76e550f20e9b4de7e6b425df79ad"
integrity sha512-Po3GfnAsAUVVEzIuNiVi/Wz2Jxb4VCLHre8FheLAZilt0MFZ6h2NJ5EK7oaCb6I5pe4Ip4LsxnrM2DP2/kqcaQ==
keycloakify@^6.12.7:
version "6.12.7"
resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-6.12.7.tgz#7ec97117c6b83be13999cd95cb01d27eaf4411a9"
integrity sha512-qCPrkD6bDjXh2/8ISqLga056Y0eExAspfn1pWZE6LE5DP7gIUsAieOCR1tgswGXxGJfgBZn2IgU2UYipm5FnYg==
dependencies:
"@octokit/rest" "^18.12.0"
cheerio "^1.0.0-rc.5"
cli-select "^1.1.2"
evt "^2.4.13"
evt "^2.4.15"
minimal-polyfills "^2.2.2"
minimist "^1.2.6"
path-browserify "^1.0.1"