diff options
| -rwxr-xr-x | package.json | 142 | ||||
| -rw-r--r-- | src/keycloak-theme/KcApp.tsx | 7 | ||||
| -rw-r--r-- | src/keycloak-theme/pages/RegisterUserProfile.tsx | 61 | ||||
| -rw-r--r-- | src/keycloak-theme/pages/shared/UserProfileCommons.tsx | 176 | ||||
| -rw-r--r-- | yarn.lock | 10 |
5 files changed, 308 insertions, 88 deletions
diff --git a/package.json b/package.json index 6589cb0..121d0c3 100755 --- a/package.json +++ b/package.json @@ -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" +] } +}
\ No newline at end of file diff --git a/src/keycloak-theme/KcApp.tsx b/src/keycloak-theme/KcApp.tsx index efe0cf6..1235b6b 100644 --- a/src/keycloak-theme/KcApp.tsx +++ b/src/keycloak-theme/KcApp.tsx @@ -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 }} />; diff --git a/src/keycloak-theme/pages/RegisterUserProfile.tsx b/src/keycloak-theme/pages/RegisterUserProfile.tsx new file mode 100644 index 0000000..492e82b --- /dev/null +++ b/src/keycloak-theme/pages/RegisterUserProfile.tsx @@ -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> + } + /> + ); +} diff --git a/src/keycloak-theme/pages/shared/UserProfileCommons.tsx b/src/keycloak-theme/pages/shared/UserProfileCommons.tsx new file mode 100644 index 0000000..08b9442 --- /dev/null +++ b/src/keycloak-theme/pages/shared/UserProfileCommons.tsx @@ -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> + ); + })} + </> + ); +} + @@ -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" |