aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xpackage.json142
-rw-r--r--src/keycloak-theme/KcApp.tsx7
-rw-r--r--src/keycloak-theme/pages/RegisterUserProfile.tsx61
-rw-r--r--src/keycloak-theme/pages/shared/UserProfileCommons.tsx176
-rw-r--r--yarn.lock10
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>
+ );
+ })}
+ </>
+ );
+}
+
diff --git a/yarn.lock b/yarn.lock
index 8a47c04..c16aa0e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"