From 5892cf2ba74448abd6adbc0fcfe9753024bd8e55 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 00:19:37 +0200
Subject: [PATCH 01/82] Decouple user profile form logic so it can be consumed
in angular
---
src/login/lib/getUserProfileApi.ts | 133 ++
src/login/lib/useUserProfileForm copy.tsx | 1403 +++++++++++++++++++++
src/login/lib/useUserProfileForm.tsx | 1401 ++------------------
3 files changed, 1616 insertions(+), 1321 deletions(-)
create mode 100644 src/login/lib/getUserProfileApi.ts
create mode 100644 src/login/lib/useUserProfileForm copy.tsx
diff --git a/src/login/lib/getUserProfileApi.ts b/src/login/lib/getUserProfileApi.ts
new file mode 100644
index 00000000..5cc88463
--- /dev/null
+++ b/src/login/lib/getUserProfileApi.ts
@@ -0,0 +1,133 @@
+import "keycloakify/tools/Array.prototype.every";
+import { assert } from "tsafe/assert";
+import type {
+ PasswordPolicies,
+ Attribute,
+ Validators
+} from "keycloakify/login/KcContext";
+import type { KcContext } from "../KcContext";
+import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
+
+export type FormFieldError = {
+ advancedMsgArgs: [string, ...string[]];
+ source: FormFieldError.Source;
+ fieldIndex: number | undefined;
+};
+
+export namespace FormFieldError {
+ export type Source =
+ | Source.Validator
+ | Source.PasswordPolicy
+ | Source.Server
+ | Source.Other;
+
+ export namespace Source {
+ export type Validator = {
+ type: "validator";
+ name: keyof Validators;
+ };
+ export type PasswordPolicy = {
+ type: "passwordPolicy";
+ name: keyof PasswordPolicies;
+ };
+ export type Server = {
+ type: "server";
+ };
+
+ export type Other = {
+ type: "other";
+ rule: "passwordConfirmMatchesPassword" | "requiredField";
+ };
+ }
+}
+
+export type FormFieldState = {
+ attribute: Attribute;
+ displayableErrors: FormFieldError[];
+ valueOrValues: string | string[];
+};
+
+export type FormState = {
+ isFormSubmittable: boolean;
+ formFieldStates: FormFieldState[];
+};
+
+export type FormAction =
+ | {
+ action: "update";
+ name: string;
+ valueOrValues: string | string[];
+ /** Default false */
+ displayErrorsImmediately?: boolean;
+ }
+ | {
+ action: "focus lost";
+ name: string;
+ fieldIndex: number | undefined;
+ };
+
+export type KcContextLike = KcContextLike_i18n &
+ KcContextLike_useGetErrors & {
+ profile: {
+ attributesByName: Record;
+ html5DataAnnotations?: Record;
+ };
+ passwordRequired?: boolean;
+ realm: { registrationEmailAsUsername: boolean };
+ url: {
+ resourcesPath: string;
+ };
+ };
+
+type KcContextLike_useGetErrors = KcContextLike_i18n & {
+ messagesPerField: Pick;
+ passwordPolicies?: PasswordPolicies;
+};
+
+assert<
+ Extract<
+ Extract,
+ { pageId: "register.ftl" }
+ > extends KcContextLike
+ ? true
+ : false
+>();
+
+export type UserProfileApi = {
+ getFormState: () => FormState;
+ subscribeToFormState: (callback: () => void) => { unsubscribe: () => void };
+ dispatchFormAction: (action: FormAction) => void;
+};
+
+const cachedUserProfileApiByKcContext = new WeakMap();
+
+export type ParamsOfGetUserProfileApi = {
+ kcContext: KcContextLike;
+ doMakeUserConfirmPassword: boolean;
+};
+
+export function getUserProfileApi(params: ParamsOfGetUserProfileApi): UserProfileApi {
+ const { kcContext } = params;
+
+ use_cache: {
+ const userProfileApi_cache = cachedUserProfileApiByKcContext.get(kcContext);
+
+ if (userProfileApi_cache === undefined) {
+ break use_cache;
+ }
+
+ return userProfileApi_cache;
+ }
+
+ const userProfileApi = getUserProfileApi_noCache(params);
+
+ cachedUserProfileApiByKcContext.set(kcContext, userProfileApi);
+
+ return userProfileApi;
+}
+
+export function getUserProfileApi_noCache(
+ params: ParamsOfGetUserProfileApi
+): UserProfileApi {
+ return null as any;
+}
diff --git a/src/login/lib/useUserProfileForm copy.tsx b/src/login/lib/useUserProfileForm copy.tsx
new file mode 100644
index 00000000..31ad939e
--- /dev/null
+++ b/src/login/lib/useUserProfileForm copy.tsx
@@ -0,0 +1,1403 @@
+import "keycloakify/tools/Array.prototype.every";
+import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
+import { assert, type Equals } from "tsafe/assert";
+import { id } from "tsafe/id";
+import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
+import { kcSanitize } from "keycloakify/lib/kcSanitize";
+import { useConstCallback } from "keycloakify/tools/useConstCallback";
+import { emailRegexp } from "keycloakify/tools/emailRegExp";
+import { formatNumber } from "keycloakify/tools/formatNumber";
+import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
+import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
+import type { KcContext } from "../KcContext";
+import type { MessageKey_defaultSet } from "keycloakify/login/i18n";
+import type { I18n } from "../i18n";
+
+export type FormFieldError = {
+ errorMessage: JSX.Element;
+ errorMessageStr: string;
+ source: FormFieldError.Source;
+ fieldIndex: number | undefined;
+};
+
+export namespace FormFieldError {
+ export type Source = Source.Validator | Source.PasswordPolicy | Source.Server | Source.Other;
+
+ export namespace Source {
+ export type Validator = {
+ type: "validator";
+ name: keyof Validators;
+ };
+ export type PasswordPolicy = {
+ type: "passwordPolicy";
+ name: keyof PasswordPolicies;
+ };
+ export type Server = {
+ type: "server";
+ };
+
+ export type Other = {
+ type: "other";
+ rule: "passwordConfirmMatchesPassword" | "requiredField";
+ };
+ }
+}
+
+export type FormFieldState = {
+ attribute: Attribute;
+ displayableErrors: FormFieldError[];
+ valueOrValues: string | string[];
+};
+
+export type FormState = {
+ isFormSubmittable: boolean;
+ formFieldStates: FormFieldState[];
+};
+
+export type FormAction =
+ | {
+ action: "update";
+ name: string;
+ valueOrValues: string | string[];
+ /** Default false */
+ displayErrorsImmediately?: boolean;
+ }
+ | {
+ action: "focus lost";
+ name: string;
+ fieldIndex: number | undefined;
+ };
+
+export type KcContextLike = KcContextLike_useGetErrors & {
+ profile: {
+ attributesByName: Record;
+ html5DataAnnotations?: Record;
+ };
+ passwordRequired?: boolean;
+ realm: { registrationEmailAsUsername: boolean };
+ url: {
+ resourcesPath: string;
+ };
+};
+
+assert, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
+
+export type UseUserProfileFormParams = {
+ kcContext: KcContextLike;
+ i18n: I18n;
+ doMakeUserConfirmPassword: boolean;
+};
+
+export type ReturnTypeOfUseUserProfileForm = {
+ formState: FormState;
+ dispatchFormAction: Dispatch;
+};
+
+namespace internal {
+ export type FormFieldState = {
+ attribute: Attribute;
+ errors: FormFieldError[];
+ hasLostFocusAtLeastOnce: boolean | boolean[];
+ valueOrValues: string | string[];
+ };
+
+ export type State = {
+ formFieldStates: FormFieldState[];
+ };
+}
+
+export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
+ const { kcContext, i18n, doMakeUserConfirmPassword } = params;
+
+ const { insertScriptTags } = useInsertScriptTags({
+ componentOrHookName: "useUserProfileForm",
+ scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
+ .filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
+ .map(key => ({
+ type: "module",
+ src: `${kcContext.url.resourcesPath}/js/${key}.js`
+ }))
+ });
+
+ useEffect(() => {
+ insertScriptTags();
+ }, []);
+
+ const { getErrors } = useGetErrors({
+ kcContext,
+ i18n
+ });
+
+ const initialState = useMemo((): internal.State => {
+ // NOTE: We don't use te kcContext.profile.attributes directly because
+ // they don't includes the password and password confirm fields and we want to add them.
+ // We also want to apply some retro-compatibility and consistency patches.
+ const attributes: Attribute[] = (() => {
+ mock_user_profile_attributes_for_older_keycloak_versions: {
+ if (
+ "profile" in kcContext &&
+ "attributesByName" in kcContext.profile &&
+ Object.keys(kcContext.profile.attributesByName).length !== 0
+ ) {
+ break mock_user_profile_attributes_for_older_keycloak_versions;
+ }
+
+ if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
+ //NOTE: Handle legacy register.ftl page
+ return (["firstName", "lastName", "email", "username"] as const)
+ .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
+ .map(name =>
+ id({
+ name: name,
+ displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
+ required: true,
+ value: (kcContext.register as any).formData[name] ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: (() => {
+ switch (name) {
+ case "email":
+ return "email";
+ case "username":
+ return "username";
+ default:
+ return undefined;
+ }
+ })()
+ })
+ );
+ }
+
+ if ("user" in kcContext && kcContext.user instanceof Object) {
+ //NOTE: Handle legacy login-update-profile.ftl
+ return (["username", "email", "firstName", "lastName"] as const)
+ .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
+ .map(name =>
+ id({
+ name: name,
+ displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
+ required: true,
+ value: (kcContext as any).user[name] ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: (() => {
+ switch (name) {
+ case "email":
+ return "email";
+ case "username":
+ return "username";
+ default:
+ return undefined;
+ }
+ })()
+ })
+ );
+ }
+
+ if ("email" in kcContext && kcContext.email instanceof Object) {
+ //NOTE: Handle legacy update-email.ftl
+ return [
+ id({
+ name: "email",
+ displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
+ required: true,
+ value: (kcContext.email as any).value ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: "email"
+ })
+ ];
+ }
+
+ assert(false, "Unable to mock user profile from the current kcContext");
+ }
+
+ return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
+ })();
+
+ // Retro-compatibility and consistency patches
+ attributes.forEach(attribute => {
+ patch_legacy_group: {
+ if (typeof attribute.group !== "string") {
+ break patch_legacy_group;
+ }
+
+ const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations } = attribute as Attribute & {
+ group: string;
+ groupDisplayHeader?: string;
+ groupDisplayDescription?: string;
+ groupAnnotations: Record;
+ };
+
+ delete attribute.group;
+ // @ts-expect-error
+ delete attribute.groupDisplayHeader;
+ // @ts-expect-error
+ delete attribute.groupDisplayDescription;
+ // @ts-expect-error
+ delete attribute.groupAnnotations;
+
+ if (group === "") {
+ break patch_legacy_group;
+ }
+
+ attribute.group = {
+ name: group,
+ displayHeader: groupDisplayHeader,
+ displayDescription: groupDisplayDescription,
+ annotations: groupAnnotations,
+ html5DataAnnotations: {}
+ };
+ }
+
+ // Attributes with options rendered by default as select inputs
+ if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
+ attribute.annotations.inputType = "select";
+ }
+
+ // Consistency patch on values/value property
+ {
+ if (getIsMultivaluedSingleField({ attribute })) {
+ attribute.multivalued = true;
+ }
+
+ if (attribute.multivalued) {
+ attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
+ delete attribute.value;
+ } else {
+ attribute.value ??= attribute.values?.[0];
+ delete attribute.values;
+ }
+ }
+ });
+
+ add_password_and_password_confirm: {
+ if (!kcContext.passwordRequired) {
+ break add_password_and_password_confirm;
+ }
+
+ attributes.forEach((attribute, i) => {
+ if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
+ // NOTE: We want to add password and password-confirm after the field that identifies the user.
+ // It's either email or username.
+ return;
+ }
+
+ attributes.splice(
+ i + 1,
+ 0,
+ {
+ name: "password",
+ displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"),
+ required: true,
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: "new-password",
+ html5DataAnnotations: {}
+ },
+ {
+ name: "password-confirm",
+ displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"),
+ required: true,
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ html5DataAnnotations: {},
+ autocomplete: "new-password"
+ }
+ );
+ });
+ }
+
+ const initialFormFieldState: {
+ attribute: Attribute;
+ valueOrValues: string | string[];
+ }[] = [];
+
+ for (const attribute of attributes) {
+ handle_multi_valued_attribute: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_attribute;
+ }
+
+ const values = attribute.values?.length ? attribute.values : [""];
+
+ apply_validator_min_range: {
+ if (getIsMultivaluedSingleField({ attribute })) {
+ break apply_validator_min_range;
+ }
+
+ const validator = attribute.validators.multivalued;
+
+ if (validator === undefined) {
+ break apply_validator_min_range;
+ }
+
+ const { min: minStr } = validator;
+
+ if (!minStr) {
+ break apply_validator_min_range;
+ }
+
+ const min = parseInt(`${minStr}`);
+
+ for (let index = values.length; index < min; index++) {
+ values.push("");
+ }
+ }
+
+ initialFormFieldState.push({
+ attribute,
+ valueOrValues: values
+ });
+
+ continue;
+ }
+
+ initialFormFieldState.push({
+ attribute,
+ valueOrValues: attribute.value ?? ""
+ });
+ }
+
+ const initialState: internal.State = {
+ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
+ attribute,
+ errors: getErrors({
+ attributeName: attribute.name,
+ formFieldStates: initialFormFieldState
+ }),
+ hasLostFocusAtLeastOnce:
+ valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
+ valueOrValues: valueOrValues
+ }))
+ };
+
+ return initialState;
+ }, []);
+
+ const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, formAction: FormAction): internal.State {
+ const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === formAction.name);
+
+ assert(formFieldState !== undefined);
+
+ (() => {
+ switch (formAction.action) {
+ case "update":
+ formFieldState.valueOrValues = formAction.valueOrValues;
+
+ apply_formatters: {
+ const { attribute } = formFieldState;
+
+ const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberFormat) {
+ break apply_formatters;
+ }
+
+ if (formFieldState.valueOrValues instanceof Array) {
+ formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
+ } else {
+ formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
+ }
+ }
+
+ formFieldState.errors = getErrors({
+ attributeName: formAction.name,
+ formFieldStates: state.formFieldStates
+ });
+
+ simulate_focus_lost: {
+ const { displayErrorsImmediately = false } = formAction;
+
+ if (!displayErrorsImmediately) {
+ break simulate_focus_lost;
+ }
+
+ for (const fieldIndex of formAction.valueOrValues instanceof Array
+ ? formAction.valueOrValues.map((...[, index]) => index)
+ : [undefined]) {
+ state = reducer(state, {
+ action: "focus lost",
+ name: formAction.name,
+ fieldIndex
+ });
+ }
+ }
+
+ update_password_confirm: {
+ if (doMakeUserConfirmPassword) {
+ break update_password_confirm;
+ }
+
+ if (formAction.name !== "password") {
+ break update_password_confirm;
+ }
+
+ state = reducer(state, {
+ action: "update",
+ name: "password-confirm",
+ valueOrValues: formAction.valueOrValues,
+ displayErrorsImmediately: formAction.displayErrorsImmediately
+ });
+ }
+
+ trigger_password_confirm_validation_on_password_change: {
+ if (!doMakeUserConfirmPassword) {
+ break trigger_password_confirm_validation_on_password_change;
+ }
+
+ if (formAction.name !== "password") {
+ break trigger_password_confirm_validation_on_password_change;
+ }
+
+ state = reducer(state, {
+ action: "update",
+ name: "password-confirm",
+ valueOrValues: (() => {
+ const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === "password-confirm");
+
+ assert(formFieldState !== undefined);
+
+ return formFieldState.valueOrValues;
+ })(),
+ displayErrorsImmediately: formAction.displayErrorsImmediately
+ });
+ }
+
+ return;
+ case "focus lost":
+ if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
+ const { fieldIndex } = formAction;
+ assert(fieldIndex !== undefined);
+ formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
+ return;
+ }
+
+ formFieldState.hasLostFocusAtLeastOnce = true;
+ return;
+ }
+ assert>(false);
+ })();
+
+ return { ...state };
+ }, initialState);
+
+ const formState: FormState = useMemo(
+ () => ({
+ formFieldStates: state.formFieldStates.map(
+ ({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
+ displayableErrors: errors.filter(error => {
+ const hasLostFocusAtLeastOnce =
+ typeof hasLostFocusAtLeastOnceOrArr === "boolean"
+ ? hasLostFocusAtLeastOnceOrArr
+ : error.fieldIndex !== undefined
+ ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
+ : hasLostFocusAtLeastOnceOrArr[hasLostFocusAtLeastOnceOrArr.length - 1];
+
+ switch (error.source.type) {
+ case "server":
+ return true;
+ case "other":
+ switch (error.source.rule) {
+ case "requiredField":
+ return hasLostFocusAtLeastOnce;
+ case "passwordConfirmMatchesPassword":
+ return hasLostFocusAtLeastOnce;
+ }
+ assert>(false);
+ case "passwordPolicy":
+ switch (error.source.name) {
+ case "length":
+ return hasLostFocusAtLeastOnce;
+ case "digits":
+ return hasLostFocusAtLeastOnce;
+ case "lowerCase":
+ return hasLostFocusAtLeastOnce;
+ case "upperCase":
+ return hasLostFocusAtLeastOnce;
+ case "specialChars":
+ return hasLostFocusAtLeastOnce;
+ case "notUsername":
+ return true;
+ case "notEmail":
+ return true;
+ }
+ assert>(false);
+ case "validator":
+ switch (error.source.name) {
+ case "length":
+ return hasLostFocusAtLeastOnce;
+ case "pattern":
+ return hasLostFocusAtLeastOnce;
+ case "email":
+ return hasLostFocusAtLeastOnce;
+ case "integer":
+ return hasLostFocusAtLeastOnce;
+ case "multivalued":
+ return hasLostFocusAtLeastOnce;
+ case "options":
+ return hasLostFocusAtLeastOnce;
+ }
+ assert>(false);
+ }
+ }),
+ attribute,
+ ...valueOrValuesWrap
+ })
+ ),
+ isFormSubmittable: state.formFieldStates.every(({ errors }) => errors.length === 0)
+ }),
+ [state]
+ );
+
+ return {
+ formState,
+ dispatchFormAction
+ };
+}
+
+type KcContextLike_useGetErrors = {
+ messagesPerField: Pick;
+ passwordPolicies?: PasswordPolicies;
+};
+
+assert();
+
+function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
+ const { kcContext, i18n } = params;
+
+ const { messagesPerField, passwordPolicies } = kcContext;
+
+ const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
+
+ const getErrors = useConstCallback(
+ (params: {
+ attributeName: string;
+ formFieldStates: {
+ attribute: Attribute;
+ valueOrValues: string | string[];
+ }[];
+ }): FormFieldError[] => {
+ const { attributeName, formFieldStates } = params;
+
+ const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
+
+ assert(formFieldState !== undefined);
+
+ const { attribute } = formFieldState;
+
+ const valueOrValues = (() => {
+ let { valueOrValues } = formFieldState;
+
+ unFormat_number: {
+ const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberUnFormat) {
+ break unFormat_number;
+ }
+
+ if (valueOrValues instanceof Array) {
+ valueOrValues = valueOrValues.map(value => formatNumber(value, kcNumberUnFormat));
+ } else {
+ valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
+ }
+ }
+
+ return valueOrValues;
+ })();
+
+ assert(attribute !== undefined);
+
+ server_side_error: {
+ if (attribute.multivalued) {
+ const defaultValues = attribute.values?.length ? attribute.values : [""];
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ if (JSON.stringify(defaultValues) !== JSON.stringify(values.slice(0, defaultValues.length))) {
+ break server_side_error;
+ }
+ } else {
+ const defaultValue = attribute.value ?? "";
+
+ assert(typeof valueOrValues === "string");
+
+ const value = valueOrValues;
+
+ if (defaultValue !== value) {
+ break server_side_error;
+ }
+ }
+
+ let doesErrorExist: boolean;
+
+ try {
+ doesErrorExist = messagesPerField.existsError(attributeName);
+ } catch {
+ break server_side_error;
+ }
+
+ if (!doesErrorExist) {
+ break server_side_error;
+ }
+
+ const errorMessageStr = messagesPerField.get(attributeName);
+
+ return [
+ {
+ errorMessageStr,
+ errorMessage: (
+
+ ),
+ fieldIndex: undefined,
+ source: {
+ type: "server"
+ }
+ }
+ ];
+ }
+
+ handle_multi_valued_multi_fields: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_multi_fields;
+ }
+
+ if (getIsMultivaluedSingleField({ attribute })) {
+ break handle_multi_valued_multi_fields;
+ }
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ const errors = values
+ .map((...[, index]) => {
+ const specificValueErrors = getErrors({
+ attributeName,
+ formFieldStates: formFieldStates.map(formFieldState => {
+ if (formFieldState.attribute.name === attributeName) {
+ assert(formFieldState.valueOrValues instanceof Array);
+ return {
+ attribute: {
+ ...attribute,
+ annotations: {
+ ...attribute.annotations,
+ inputType: undefined
+ },
+ multivalued: false
+ },
+ valueOrValues: formFieldState.valueOrValues[index]
+ };
+ }
+
+ return formFieldState;
+ })
+ });
+
+ return specificValueErrors
+ .filter(error => {
+ if (error.source.type === "other" && error.source.rule === "requiredField") {
+ return false;
+ }
+
+ return true;
+ })
+ .map(
+ (error): FormFieldError => ({
+ ...error,
+ fieldIndex: index
+ })
+ );
+ })
+ .reduce((acc, errors) => [...acc, ...errors], []);
+
+ required_field: {
+ if (!attribute.required) {
+ break required_field;
+ }
+
+ if (values.every(value => value !== "")) {
+ break required_field;
+ }
+
+ const msgArgs = ["error-user-attribute-required"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "requiredField"
+ }
+ });
+ }
+
+ return errors;
+ }
+
+ handle_multi_valued_single_field: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_single_field;
+ }
+
+ if (!getIsMultivaluedSingleField({ attribute })) {
+ break handle_multi_valued_single_field;
+ }
+
+ const validatorName = "multivalued";
+
+ const validator = attribute.validators[validatorName];
+
+ if (validator === undefined) {
+ return [];
+ }
+
+ const { min: minStr } = validator;
+
+ const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
+
+ assert(!isNaN(min));
+
+ const { max: maxStr } = validator;
+
+ const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
+
+ assert(!isNaN(max));
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ if (min <= values.length && values.length <= max) {
+ return [];
+ }
+
+ const msgArgs = ["error-invalid-multivalued-size", `${min}`, `${max}`] as const;
+
+ return [
+ {
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ }
+ ];
+ }
+
+ assert(typeof valueOrValues === "string");
+
+ const value = valueOrValues;
+
+ const errors: FormFieldError[] = [];
+
+ check_password_policies: {
+ if (attributeName !== "password") {
+ break check_password_policies;
+ }
+
+ if (passwordPolicies === undefined) {
+ break check_password_policies;
+ }
+
+ check_password_policy_x: {
+ const policyName = "length";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minLength = policy;
+
+ if (value.length >= minLength) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "digits";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfDigits = policy;
+
+ if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "lowerCase";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfLowerCaseChar = policy;
+
+ if (
+ value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
+ ) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "upperCase";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfUpperCaseChar = policy;
+
+ if (
+ value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
+ ) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "specialChars";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfSpecialChar = policy;
+
+ if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "notUsername";
+
+ const notUsername = passwordPolicies[policyName];
+
+ if (!notUsername) {
+ break check_password_policy_x;
+ }
+
+ const usernameFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "username");
+
+ if (!usernameFormFieldState) {
+ break check_password_policy_x;
+ }
+
+ const usernameValue = (() => {
+ let { valueOrValues } = usernameFormFieldState;
+
+ assert(typeof valueOrValues === "string");
+
+ unFormat_number: {
+ const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberUnFormat) {
+ break unFormat_number;
+ }
+
+ valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
+ }
+
+ return valueOrValues;
+ })();
+
+ if (usernameValue === "") {
+ break check_password_policy_x;
+ }
+
+ if (value !== usernameValue) {
+ break check_password_policy_x;
+ }
+
+ const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "notEmail";
+
+ const notEmail = passwordPolicies[policyName];
+
+ if (!notEmail) {
+ break check_password_policy_x;
+ }
+
+ const emailFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "email");
+
+ if (!emailFormFieldState) {
+ break check_password_policy_x;
+ }
+
+ assert(typeof emailFormFieldState.valueOrValues === "string");
+
+ {
+ const emailValue = emailFormFieldState.valueOrValues;
+
+ if (emailValue === "") {
+ break check_password_policy_x;
+ }
+
+ if (value !== emailValue) {
+ break check_password_policy_x;
+ }
+ }
+
+ const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+ }
+
+ password_confirm_matches_password: {
+ if (attributeName !== "password-confirm") {
+ break password_confirm_matches_password;
+ }
+
+ const passwordFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "password");
+
+ assert(passwordFormFieldState !== undefined);
+
+ assert(typeof passwordFormFieldState.valueOrValues === "string");
+
+ {
+ const passwordValue = passwordFormFieldState.valueOrValues;
+
+ if (value === passwordValue) {
+ break password_confirm_matches_password;
+ }
+ }
+
+ const msgArgs = ["invalidPasswordConfirmMessage"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "passwordConfirmMatchesPassword"
+ }
+ });
+ }
+
+ const { validators } = attribute;
+
+ required_field: {
+ if (!attribute.required) {
+ break required_field;
+ }
+
+ if (value !== "") {
+ break required_field;
+ }
+
+ const msgArgs = ["error-user-attribute-required"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "requiredField"
+ }
+ });
+ }
+
+ validator_x: {
+ const validatorName = "length";
+
+ const validator = validators[validatorName];
+
+ if (!validator) {
+ break validator_x;
+ }
+
+ const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ const source: FormFieldError.Source = {
+ type: "validator",
+ name: validatorName
+ };
+
+ if (max && value.length > parseInt(`${max}`)) {
+ const msgArgs = ["error-invalid-length-too-long", `${max}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source
+ });
+ }
+
+ if (min && value.length < parseInt(`${min}`)) {
+ const msgArgs = ["error-invalid-length-too-short", `${min}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source
+ });
+ }
+ }
+
+ validator_x: {
+ const validatorName = "pattern";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ if (new RegExp(pattern).test(value)) {
+ break validator_x;
+ }
+
+ const msgArgs = [errorMessageKey ?? id("shouldMatchPattern"), pattern] as const;
+
+ errors.push({
+ errorMessage: {advancedMsg(...msgArgs)},
+ errorMessageStr: advancedMsgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ validator_x: {
+ {
+ const lastError = errors[errors.length - 1];
+ if (lastError !== undefined && lastError.source.type === "validator" && lastError.source.name === "pattern") {
+ break validator_x;
+ }
+ }
+
+ const validatorName = "email";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ if (emailRegexp.test(value)) {
+ break validator_x;
+ }
+
+ const msgArgs = [id("invalidEmailMessage")] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ validator_x: {
+ const validatorName = "integer";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ const intValue = parseInt(value);
+
+ const source: FormFieldError.Source = {
+ type: "validator",
+ name: validatorName
+ };
+
+ if (isNaN(intValue)) {
+ const msgArgs = ["mustBeAnInteger"] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source
+ });
+
+ break validator_x;
+ }
+
+ if (max && intValue > parseInt(`${max}`)) {
+ const msgArgs = ["error-number-out-of-range-too-big", `${max}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source
+ });
+
+ break validator_x;
+ }
+
+ if (min && intValue < parseInt(`${min}`)) {
+ const msgArgs = ["error-number-out-of-range-too-small", `${min}`] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source
+ });
+
+ break validator_x;
+ }
+ }
+
+ validator_x: {
+ const validatorName = "options";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ if (value === "") {
+ break validator_x;
+ }
+
+ if (validator.options.indexOf(value) >= 0) {
+ break validator_x;
+ }
+
+ const msgArgs = [id("notAValidOption")] as const;
+
+ errors.push({
+ errorMessage: {msg(...msgArgs)},
+ errorMessageStr: msgStr(...msgArgs),
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ //TODO: Implement missing validators. See Validators type definition.
+
+ return errors;
+ }
+ );
+
+ return { getErrors };
+}
+
+function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
+ const { attribute } = params;
+
+ return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
+}
+
+export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
+ const { attribute, values, fieldIndex } = params;
+
+ const hasRemove = (() => {
+ if (values.length === 1) {
+ return false;
+ }
+
+ const minCount = (() => {
+ const { multivalued } = attribute.validators;
+
+ if (multivalued === undefined) {
+ return undefined;
+ }
+
+ const minStr = multivalued.min;
+
+ if (minStr === undefined) {
+ return undefined;
+ }
+
+ return parseInt(`${minStr}`);
+ })();
+
+ if (minCount === undefined) {
+ return true;
+ }
+
+ if (values.length === minCount) {
+ return false;
+ }
+
+ return true;
+ })();
+
+ const hasAdd = (() => {
+ if (fieldIndex + 1 !== values.length) {
+ return false;
+ }
+
+ const maxCount = (() => {
+ const { multivalued } = attribute.validators;
+
+ if (multivalued === undefined) {
+ return undefined;
+ }
+
+ const maxStr = multivalued.max;
+
+ if (maxStr === undefined) {
+ return undefined;
+ }
+
+ return parseInt(`${maxStr}`);
+ })();
+
+ if (maxCount === undefined) {
+ return true;
+ }
+
+ return values.length !== maxCount;
+ })();
+
+ return { hasRemove, hasAdd };
+}
diff --git a/src/login/lib/useUserProfileForm.tsx b/src/login/lib/useUserProfileForm.tsx
index 3dfce1b9..3d997549 100644
--- a/src/login/lib/useUserProfileForm.tsx
+++ b/src/login/lib/useUserProfileForm.tsx
@@ -1,17 +1,7 @@
-import "keycloakify/tools/Array.prototype.every";
-import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
-import { assert, type Equals } from "tsafe/assert";
-import { id } from "tsafe/id";
-import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
-import { kcSanitize } from "keycloakify/lib/kcSanitize";
-import { useConstCallback } from "keycloakify/tools/useConstCallback";
-import { emailRegexp } from "keycloakify/tools/emailRegExp";
-import { formatNumber } from "keycloakify/tools/formatNumber";
-import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
+import * as reactlessApi from "./getUserProfileApi";
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
-import type { KcContext } from "../KcContext";
-import type { MessageKey_defaultSet } from "keycloakify/login/i18n";
-import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
+import { useEffect, useState, useMemo, Fragment } from "react";
+import { assert, type Equals } from "tsafe/assert";
import type { I18n } from "../i18n";
export type FormFieldError = {
@@ -21,6 +11,13 @@ export type FormFieldError = {
fieldIndex: number | undefined;
};
+{
+ type A = Omit;
+ type B = Omit;
+
+ assert>();
+}
+
export namespace FormFieldError {
export type Source = Source.Validator | Source.PasswordPolicy | Source.Server | Source.Other;
@@ -44,17 +41,38 @@ export namespace FormFieldError {
}
}
+{
+ type A = FormFieldError.Source;
+ type B = reactlessApi.FormFieldError.Source;
+
+ assert>();
+}
+
export type FormFieldState = {
attribute: Attribute;
displayableErrors: FormFieldError[];
valueOrValues: string | string[];
};
+{
+ type A = Omit;
+ type B = Omit;
+
+ assert>();
+}
+
export type FormState = {
isFormSubmittable: boolean;
formFieldStates: FormFieldState[];
};
+{
+ type A = Omit;
+ type B = Omit;
+
+ assert>();
+}
+
export type FormAction =
| {
action: "update";
@@ -69,1337 +87,78 @@ export type FormAction =
fieldIndex: number | undefined;
};
-export type KcContextLike = KcContextLike_i18n &
- KcContextLike_useGetErrors & {
- profile: {
- attributesByName: Record;
- html5DataAnnotations?: Record;
- };
- passwordRequired?: boolean;
- realm: { registrationEmailAsUsername: boolean };
- url: {
- resourcesPath: string;
- };
- };
+{
+ type A = FormAction;
+ type B = reactlessApi.FormAction;
-assert, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
+ assert>();
+}
-export type UseUserProfileFormParams = {
+export type KcContextLike = reactlessApi.KcContextLike;
+
+export type I18nLike = Pick;
+
+export type ParamsOfUseUserProfileForm = {
kcContext: KcContextLike;
- i18n: I18n;
doMakeUserConfirmPassword: boolean;
+ i18n: I18nLike;
};
+{
+ type A = Omit;
+ type B = reactlessApi.ParamsOfGetUserProfileApi;
+
+ assert>();
+}
+
export type ReturnTypeOfUseUserProfileForm = {
formState: FormState;
- dispatchFormAction: Dispatch;
+ dispatchFormAction: (action: FormAction) => void;
};
-namespace internal {
- export type FormFieldState = {
- attribute: Attribute;
- errors: FormFieldError[];
- hasLostFocusAtLeastOnce: boolean | boolean[];
- valueOrValues: string | string[];
- };
+export function useUserProfileForm(params: ParamsOfUseUserProfileForm): ReturnTypeOfUseUserProfileForm {
+ const { doMakeUserConfirmPassword, i18n, kcContext } = params;
- export type State = {
- formFieldStates: FormFieldState[];
- };
-}
-
-export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
- const { kcContext, i18n, doMakeUserConfirmPassword } = params;
-
- const { insertScriptTags } = useInsertScriptTags({
- componentOrHookName: "useUserProfileForm",
- scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
- .filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
- .map(key => ({
- type: "module",
- src: `${kcContext.url.resourcesPath}/js/${key}.js`
- }))
+ const api = reactlessApi.getUserProfileApi({
+ kcContext,
+ doMakeUserConfirmPassword
});
+ const [formState_reactless, setFormState_reactless] = useState(() => api.getFormState());
+
useEffect(() => {
- insertScriptTags();
- }, []);
-
- const { getErrors } = useGetErrors({
- kcContext,
- i18n
- });
-
- const initialState = useMemo((): internal.State => {
- // NOTE: We don't use te kcContext.profile.attributes directly because
- // they don't includes the password and password confirm fields and we want to add them.
- // We also want to apply some retro-compatibility and consistency patches.
- const attributes: Attribute[] = (() => {
- mock_user_profile_attributes_for_older_keycloak_versions: {
- if (
- "profile" in kcContext &&
- "attributesByName" in kcContext.profile &&
- Object.keys(kcContext.profile.attributesByName).length !== 0
- ) {
- break mock_user_profile_attributes_for_older_keycloak_versions;
- }
-
- if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
- //NOTE: Handle legacy register.ftl page
- return (["firstName", "lastName", "email", "username"] as const)
- .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
- .map(name =>
- id({
- name: name,
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
- required: true,
- value: (kcContext.register as any).formData[name] ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: (() => {
- switch (name) {
- case "email":
- return "email";
- case "username":
- return "username";
- default:
- return undefined;
- }
- })()
- })
- );
- }
-
- if ("user" in kcContext && kcContext.user instanceof Object) {
- //NOTE: Handle legacy login-update-profile.ftl
- return (["username", "email", "firstName", "lastName"] as const)
- .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
- .map(name =>
- id({
- name: name,
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
- required: true,
- value: (kcContext as any).user[name] ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: (() => {
- switch (name) {
- case "email":
- return "email";
- case "username":
- return "username";
- default:
- return undefined;
- }
- })()
- })
- );
- }
-
- if ("email" in kcContext && kcContext.email instanceof Object) {
- //NOTE: Handle legacy update-email.ftl
- return [
- id({
- name: "email",
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
- required: true,
- value: (kcContext.email as any).value ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: "email"
- })
- ];
- }
-
- assert(false, "Unable to mock user profile from the current kcContext");
- }
-
- return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
- })();
-
- // Retro-compatibility and consistency patches
- attributes.forEach(attribute => {
- patch_legacy_group: {
- if (typeof attribute.group !== "string") {
- break patch_legacy_group;
- }
-
- const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations } = attribute as Attribute & {
- group: string;
- groupDisplayHeader?: string;
- groupDisplayDescription?: string;
- groupAnnotations: Record;
- };
-
- delete attribute.group;
- // @ts-expect-error
- delete attribute.groupDisplayHeader;
- // @ts-expect-error
- delete attribute.groupDisplayDescription;
- // @ts-expect-error
- delete attribute.groupAnnotations;
-
- if (group === "") {
- break patch_legacy_group;
- }
-
- attribute.group = {
- name: group,
- displayHeader: groupDisplayHeader,
- displayDescription: groupDisplayDescription,
- annotations: groupAnnotations,
- html5DataAnnotations: {}
- };
- }
-
- // Attributes with options rendered by default as select inputs
- if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
- attribute.annotations.inputType = "select";
- }
-
- // Consistency patch on values/value property
- {
- if (getIsMultivaluedSingleField({ attribute })) {
- attribute.multivalued = true;
- }
-
- if (attribute.multivalued) {
- attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
- delete attribute.value;
- } else {
- attribute.value ??= attribute.values?.[0];
- delete attribute.values;
- }
- }
+ const { unsubscribe } = api.subscribeToFormState(() => {
+ setFormState_reactless(api.getFormState());
});
- add_password_and_password_confirm: {
- if (!kcContext.passwordRequired) {
- break add_password_and_password_confirm;
- }
+ return () => unsubscribe();
+ }, [api]);
- attributes.forEach((attribute, i) => {
- if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
- // NOTE: We want to add password and password-confirm after the field that identifies the user.
- // It's either email or username.
- return;
- }
+ const { advancedMsg, advancedMsgStr } = i18n;
- attributes.splice(
- i + 1,
- 0,
- {
- name: "password",
- displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"),
- required: true,
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: "new-password",
- html5DataAnnotations: {}
- },
- {
- name: "password-confirm",
- displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"),
- required: true,
- readOnly: false,
- validators: {},
- annotations: {},
- html5DataAnnotations: {},
- autocomplete: "new-password"
- }
- );
- });
- }
-
- const initialFormFieldState: {
- attribute: Attribute;
- valueOrValues: string | string[];
- }[] = [];
-
- for (const attribute of attributes) {
- handle_multi_valued_attribute: {
- if (!attribute.multivalued) {
- break handle_multi_valued_attribute;
- }
-
- const values = attribute.values?.length ? attribute.values : [""];
-
- apply_validator_min_range: {
- if (getIsMultivaluedSingleField({ attribute })) {
- break apply_validator_min_range;
- }
-
- const validator = attribute.validators.multivalued;
-
- if (validator === undefined) {
- break apply_validator_min_range;
- }
-
- const { min: minStr } = validator;
-
- if (!minStr) {
- break apply_validator_min_range;
- }
-
- const min = parseInt(`${minStr}`);
-
- for (let index = values.length; index < min; index++) {
- values.push("");
- }
- }
-
- initialFormFieldState.push({
- attribute,
- valueOrValues: values
- });
-
- continue;
- }
-
- initialFormFieldState.push({
- attribute,
- valueOrValues: attribute.value ?? ""
- });
- }
-
- const initialState: internal.State = {
- formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
- attribute,
- errors: getErrors({
- attributeName: attribute.name,
- formFieldStates: initialFormFieldState
- }),
- hasLostFocusAtLeastOnce:
- valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
- valueOrValues: valueOrValues
+ const formState = useMemo(
+ (): FormState => ({
+ isFormSubmittable: formState_reactless.isFormSubmittable,
+ formFieldStates: formState_reactless.formFieldStates.map(formFieldState_reactless => ({
+ attribute: formFieldState_reactless.attribute,
+ valueOrValues: formFieldState_reactless.valueOrValues,
+ displayableErrors: formFieldState_reactless.displayableErrors.map((formFieldError_reactless, i) => ({
+ errorMessage: (
+
+ {advancedMsg(...formFieldError_reactless.advancedMsgArgs)}
+
+ ),
+ errorMessageStr: advancedMsgStr(...formFieldError_reactless.advancedMsgArgs),
+ source: formFieldError_reactless.source,
+ fieldIndex: formFieldError_reactless.fieldIndex
+ }))
}))
- };
-
- return initialState;
- }, []);
-
- const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, formAction: FormAction): internal.State {
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === formAction.name);
-
- assert(formFieldState !== undefined);
-
- (() => {
- switch (formAction.action) {
- case "update":
- formFieldState.valueOrValues = formAction.valueOrValues;
-
- apply_formatters: {
- const { attribute } = formFieldState;
-
- const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberFormat) {
- break apply_formatters;
- }
-
- if (formFieldState.valueOrValues instanceof Array) {
- formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
- } else {
- formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
- }
- }
-
- formFieldState.errors = getErrors({
- attributeName: formAction.name,
- formFieldStates: state.formFieldStates
- });
-
- simulate_focus_lost: {
- const { displayErrorsImmediately = false } = formAction;
-
- if (!displayErrorsImmediately) {
- break simulate_focus_lost;
- }
-
- for (const fieldIndex of formAction.valueOrValues instanceof Array
- ? formAction.valueOrValues.map((...[, index]) => index)
- : [undefined]) {
- state = reducer(state, {
- action: "focus lost",
- name: formAction.name,
- fieldIndex
- });
- }
- }
-
- update_password_confirm: {
- if (doMakeUserConfirmPassword) {
- break update_password_confirm;
- }
-
- if (formAction.name !== "password") {
- break update_password_confirm;
- }
-
- state = reducer(state, {
- action: "update",
- name: "password-confirm",
- valueOrValues: formAction.valueOrValues,
- displayErrorsImmediately: formAction.displayErrorsImmediately
- });
- }
-
- trigger_password_confirm_validation_on_password_change: {
- if (!doMakeUserConfirmPassword) {
- break trigger_password_confirm_validation_on_password_change;
- }
-
- if (formAction.name !== "password") {
- break trigger_password_confirm_validation_on_password_change;
- }
-
- state = reducer(state, {
- action: "update",
- name: "password-confirm",
- valueOrValues: (() => {
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === "password-confirm");
-
- assert(formFieldState !== undefined);
-
- return formFieldState.valueOrValues;
- })(),
- displayErrorsImmediately: formAction.displayErrorsImmediately
- });
- }
-
- return;
- case "focus lost":
- if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
- const { fieldIndex } = formAction;
- assert(fieldIndex !== undefined);
- formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
- return;
- }
-
- formFieldState.hasLostFocusAtLeastOnce = true;
- return;
- }
- assert>(false);
- })();
-
- return { ...state };
- }, initialState);
-
- const formState: FormState = useMemo(
- () => ({
- formFieldStates: state.formFieldStates.map(
- ({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
- displayableErrors: errors.filter(error => {
- const hasLostFocusAtLeastOnce =
- typeof hasLostFocusAtLeastOnceOrArr === "boolean"
- ? hasLostFocusAtLeastOnceOrArr
- : error.fieldIndex !== undefined
- ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
- : hasLostFocusAtLeastOnceOrArr[hasLostFocusAtLeastOnceOrArr.length - 1];
-
- switch (error.source.type) {
- case "server":
- return true;
- case "other":
- switch (error.source.rule) {
- case "requiredField":
- return hasLostFocusAtLeastOnce;
- case "passwordConfirmMatchesPassword":
- return hasLostFocusAtLeastOnce;
- }
- assert>(false);
- case "passwordPolicy":
- switch (error.source.name) {
- case "length":
- return hasLostFocusAtLeastOnce;
- case "digits":
- return hasLostFocusAtLeastOnce;
- case "lowerCase":
- return hasLostFocusAtLeastOnce;
- case "upperCase":
- return hasLostFocusAtLeastOnce;
- case "specialChars":
- return hasLostFocusAtLeastOnce;
- case "notUsername":
- return true;
- case "notEmail":
- return true;
- }
- assert>(false);
- case "validator":
- switch (error.source.name) {
- case "length":
- return hasLostFocusAtLeastOnce;
- case "pattern":
- return hasLostFocusAtLeastOnce;
- case "email":
- return hasLostFocusAtLeastOnce;
- case "integer":
- return hasLostFocusAtLeastOnce;
- case "multivalued":
- return hasLostFocusAtLeastOnce;
- case "options":
- return hasLostFocusAtLeastOnce;
- }
- assert>(false);
- }
- }),
- attribute,
- ...valueOrValuesWrap
- })
- ),
- isFormSubmittable: state.formFieldStates.every(({ errors }) => errors.length === 0)
}),
- [state]
+ [formState_reactless]
);
return {
formState,
- dispatchFormAction
+ dispatchFormAction: api.dispatchFormAction
};
}
-
-type KcContextLike_useGetErrors = KcContextLike_i18n & {
- messagesPerField: Pick;
- passwordPolicies?: PasswordPolicies;
-};
-
-assert();
-
-function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
- const { kcContext, i18n } = params;
-
- const { messagesPerField, passwordPolicies } = kcContext;
-
- const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
-
- const getErrors = useConstCallback(
- (params: {
- attributeName: string;
- formFieldStates: {
- attribute: Attribute;
- valueOrValues: string | string[];
- }[];
- }): FormFieldError[] => {
- const { attributeName, formFieldStates } = params;
-
- const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
-
- assert(formFieldState !== undefined);
-
- const { attribute } = formFieldState;
-
- const valueOrValues = (() => {
- let { valueOrValues } = formFieldState;
-
- unFormat_number: {
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberUnFormat) {
- break unFormat_number;
- }
-
- if (valueOrValues instanceof Array) {
- valueOrValues = valueOrValues.map(value => formatNumber(value, kcNumberUnFormat));
- } else {
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
- }
- }
-
- return valueOrValues;
- })();
-
- assert(attribute !== undefined);
-
- server_side_error: {
- if (attribute.multivalued) {
- const defaultValues = attribute.values?.length ? attribute.values : [""];
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- if (JSON.stringify(defaultValues) !== JSON.stringify(values.slice(0, defaultValues.length))) {
- break server_side_error;
- }
- } else {
- const defaultValue = attribute.value ?? "";
-
- assert(typeof valueOrValues === "string");
-
- const value = valueOrValues;
-
- if (defaultValue !== value) {
- break server_side_error;
- }
- }
-
- let doesErrorExist: boolean;
-
- try {
- doesErrorExist = messagesPerField.existsError(attributeName);
- } catch {
- break server_side_error;
- }
-
- if (!doesErrorExist) {
- break server_side_error;
- }
-
- const errorMessageStr = messagesPerField.get(attributeName);
-
- return [
- {
- errorMessageStr,
- errorMessage: (
-
- ),
- fieldIndex: undefined,
- source: {
- type: "server"
- }
- }
- ];
- }
-
- handle_multi_valued_multi_fields: {
- if (!attribute.multivalued) {
- break handle_multi_valued_multi_fields;
- }
-
- if (getIsMultivaluedSingleField({ attribute })) {
- break handle_multi_valued_multi_fields;
- }
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- const errors = values
- .map((...[, index]) => {
- const specificValueErrors = getErrors({
- attributeName,
- formFieldStates: formFieldStates.map(formFieldState => {
- if (formFieldState.attribute.name === attributeName) {
- assert(formFieldState.valueOrValues instanceof Array);
- return {
- attribute: {
- ...attribute,
- annotations: {
- ...attribute.annotations,
- inputType: undefined
- },
- multivalued: false
- },
- valueOrValues: formFieldState.valueOrValues[index]
- };
- }
-
- return formFieldState;
- })
- });
-
- return specificValueErrors
- .filter(error => {
- if (error.source.type === "other" && error.source.rule === "requiredField") {
- return false;
- }
-
- return true;
- })
- .map(
- (error): FormFieldError => ({
- ...error,
- fieldIndex: index
- })
- );
- })
- .reduce((acc, errors) => [...acc, ...errors], []);
-
- required_field: {
- if (!attribute.required) {
- break required_field;
- }
-
- if (values.every(value => value !== "")) {
- break required_field;
- }
-
- const msgArgs = ["error-user-attribute-required"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "requiredField"
- }
- });
- }
-
- return errors;
- }
-
- handle_multi_valued_single_field: {
- if (!attribute.multivalued) {
- break handle_multi_valued_single_field;
- }
-
- if (!getIsMultivaluedSingleField({ attribute })) {
- break handle_multi_valued_single_field;
- }
-
- const validatorName = "multivalued";
-
- const validator = attribute.validators[validatorName];
-
- if (validator === undefined) {
- return [];
- }
-
- const { min: minStr } = validator;
-
- const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
-
- assert(!isNaN(min));
-
- const { max: maxStr } = validator;
-
- const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
-
- assert(!isNaN(max));
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- if (min <= values.length && values.length <= max) {
- return [];
- }
-
- const msgArgs = ["error-invalid-multivalued-size", `${min}`, `${max}`] as const;
-
- return [
- {
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- }
- ];
- }
-
- assert(typeof valueOrValues === "string");
-
- const value = valueOrValues;
-
- const errors: FormFieldError[] = [];
-
- check_password_policies: {
- if (attributeName !== "password") {
- break check_password_policies;
- }
-
- if (passwordPolicies === undefined) {
- break check_password_policies;
- }
-
- check_password_policy_x: {
- const policyName = "length";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minLength = policy;
-
- if (value.length >= minLength) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "digits";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfDigits = policy;
-
- if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "lowerCase";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfLowerCaseChar = policy;
-
- if (
- value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
- ) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "upperCase";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfUpperCaseChar = policy;
-
- if (
- value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
- ) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "specialChars";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfSpecialChar = policy;
-
- if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "notUsername";
-
- const notUsername = passwordPolicies[policyName];
-
- if (!notUsername) {
- break check_password_policy_x;
- }
-
- const usernameFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "username");
-
- if (!usernameFormFieldState) {
- break check_password_policy_x;
- }
-
- const usernameValue = (() => {
- let { valueOrValues } = usernameFormFieldState;
-
- assert(typeof valueOrValues === "string");
-
- unFormat_number: {
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberUnFormat) {
- break unFormat_number;
- }
-
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
- }
-
- return valueOrValues;
- })();
-
- if (usernameValue === "") {
- break check_password_policy_x;
- }
-
- if (value !== usernameValue) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "notEmail";
-
- const notEmail = passwordPolicies[policyName];
-
- if (!notEmail) {
- break check_password_policy_x;
- }
-
- const emailFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "email");
-
- if (!emailFormFieldState) {
- break check_password_policy_x;
- }
-
- assert(typeof emailFormFieldState.valueOrValues === "string");
-
- {
- const emailValue = emailFormFieldState.valueOrValues;
-
- if (emailValue === "") {
- break check_password_policy_x;
- }
-
- if (value !== emailValue) {
- break check_password_policy_x;
- }
- }
-
- const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
- }
-
- password_confirm_matches_password: {
- if (attributeName !== "password-confirm") {
- break password_confirm_matches_password;
- }
-
- const passwordFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "password");
-
- assert(passwordFormFieldState !== undefined);
-
- assert(typeof passwordFormFieldState.valueOrValues === "string");
-
- {
- const passwordValue = passwordFormFieldState.valueOrValues;
-
- if (value === passwordValue) {
- break password_confirm_matches_password;
- }
- }
-
- const msgArgs = ["invalidPasswordConfirmMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "passwordConfirmMatchesPassword"
- }
- });
- }
-
- const { validators } = attribute;
-
- required_field: {
- if (!attribute.required) {
- break required_field;
- }
-
- if (value !== "") {
- break required_field;
- }
-
- const msgArgs = ["error-user-attribute-required"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "requiredField"
- }
- });
- }
-
- validator_x: {
- const validatorName = "length";
-
- const validator = validators[validatorName];
-
- if (!validator) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- const source: FormFieldError.Source = {
- type: "validator",
- name: validatorName
- };
-
- if (max && value.length > parseInt(`${max}`)) {
- const msgArgs = ["error-invalid-length-too-long", `${max}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
- }
-
- if (min && value.length < parseInt(`${min}`)) {
- const msgArgs = ["error-invalid-length-too-short", `${min}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
- }
- }
-
- validator_x: {
- const validatorName = "pattern";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- if (new RegExp(pattern).test(value)) {
- break validator_x;
- }
-
- const msgArgs = [errorMessageKey ?? id("shouldMatchPattern"), pattern] as const;
-
- errors.push({
- errorMessage: {advancedMsg(...msgArgs)},
- errorMessageStr: advancedMsgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- validator_x: {
- {
- const lastError = errors[errors.length - 1];
- if (lastError !== undefined && lastError.source.type === "validator" && lastError.source.name === "pattern") {
- break validator_x;
- }
- }
-
- const validatorName = "email";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- if (emailRegexp.test(value)) {
- break validator_x;
- }
-
- const msgArgs = [id("invalidEmailMessage")] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- validator_x: {
- const validatorName = "integer";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- const intValue = parseInt(value);
-
- const source: FormFieldError.Source = {
- type: "validator",
- name: validatorName
- };
-
- if (isNaN(intValue)) {
- const msgArgs = ["mustBeAnInteger"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
-
- if (max && intValue > parseInt(`${max}`)) {
- const msgArgs = ["error-number-out-of-range-too-big", `${max}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
-
- if (min && intValue < parseInt(`${min}`)) {
- const msgArgs = ["error-number-out-of-range-too-small", `${min}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
- }
-
- validator_x: {
- const validatorName = "options";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- if (value === "") {
- break validator_x;
- }
-
- if (validator.options.indexOf(value) >= 0) {
- break validator_x;
- }
-
- const msgArgs = [id("notAValidOption")] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- //TODO: Implement missing validators. See Validators type definition.
-
- return errors;
- }
- );
-
- return { getErrors };
-}
-
-function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
- const { attribute } = params;
-
- return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
-}
-
-export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
- const { attribute, values, fieldIndex } = params;
-
- const hasRemove = (() => {
- if (values.length === 1) {
- return false;
- }
-
- const minCount = (() => {
- const { multivalued } = attribute.validators;
-
- if (multivalued === undefined) {
- return undefined;
- }
-
- const minStr = multivalued.min;
-
- if (minStr === undefined) {
- return undefined;
- }
-
- return parseInt(`${minStr}`);
- })();
-
- if (minCount === undefined) {
- return true;
- }
-
- if (values.length === minCount) {
- return false;
- }
-
- return true;
- })();
-
- const hasAdd = (() => {
- if (fieldIndex + 1 !== values.length) {
- return false;
- }
-
- const maxCount = (() => {
- const { multivalued } = attribute.validators;
-
- if (multivalued === undefined) {
- return undefined;
- }
-
- const maxStr = multivalued.max;
-
- if (maxStr === undefined) {
- return undefined;
- }
-
- return parseInt(`${maxStr}`);
- })();
-
- if (maxCount === undefined) {
- return true;
- }
-
- return values.length !== maxCount;
- })();
-
- return { hasRemove, hasAdd };
-}
From 36dd3241392b6aacbbda8b84373419a3a07b24bb Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 10:18:22 +0200
Subject: [PATCH 02/82] complete decoupling of user profile form validation
logic
---
src/login/lib/getUserProfileApi.ts | 133 --
.../getUserProfileApi/getUserProfileApi.ts | 1561 +++++++++++++++++
src/login/lib/getUserProfileApi/index.ts | 1 +
.../lib/getUserProfileApi/kcNumberUnFormat.ts | 109 ++
src/login/lib/useUserProfileForm copy.tsx | 1403 ---------------
src/login/lib/useUserProfileForm.tsx | 3 +-
6 files changed, 1673 insertions(+), 1537 deletions(-)
delete mode 100644 src/login/lib/getUserProfileApi.ts
create mode 100644 src/login/lib/getUserProfileApi/getUserProfileApi.ts
create mode 100644 src/login/lib/getUserProfileApi/index.ts
create mode 100644 src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
delete mode 100644 src/login/lib/useUserProfileForm copy.tsx
diff --git a/src/login/lib/getUserProfileApi.ts b/src/login/lib/getUserProfileApi.ts
deleted file mode 100644
index 5cc88463..00000000
--- a/src/login/lib/getUserProfileApi.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import "keycloakify/tools/Array.prototype.every";
-import { assert } from "tsafe/assert";
-import type {
- PasswordPolicies,
- Attribute,
- Validators
-} from "keycloakify/login/KcContext";
-import type { KcContext } from "../KcContext";
-import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
-
-export type FormFieldError = {
- advancedMsgArgs: [string, ...string[]];
- source: FormFieldError.Source;
- fieldIndex: number | undefined;
-};
-
-export namespace FormFieldError {
- export type Source =
- | Source.Validator
- | Source.PasswordPolicy
- | Source.Server
- | Source.Other;
-
- export namespace Source {
- export type Validator = {
- type: "validator";
- name: keyof Validators;
- };
- export type PasswordPolicy = {
- type: "passwordPolicy";
- name: keyof PasswordPolicies;
- };
- export type Server = {
- type: "server";
- };
-
- export type Other = {
- type: "other";
- rule: "passwordConfirmMatchesPassword" | "requiredField";
- };
- }
-}
-
-export type FormFieldState = {
- attribute: Attribute;
- displayableErrors: FormFieldError[];
- valueOrValues: string | string[];
-};
-
-export type FormState = {
- isFormSubmittable: boolean;
- formFieldStates: FormFieldState[];
-};
-
-export type FormAction =
- | {
- action: "update";
- name: string;
- valueOrValues: string | string[];
- /** Default false */
- displayErrorsImmediately?: boolean;
- }
- | {
- action: "focus lost";
- name: string;
- fieldIndex: number | undefined;
- };
-
-export type KcContextLike = KcContextLike_i18n &
- KcContextLike_useGetErrors & {
- profile: {
- attributesByName: Record;
- html5DataAnnotations?: Record;
- };
- passwordRequired?: boolean;
- realm: { registrationEmailAsUsername: boolean };
- url: {
- resourcesPath: string;
- };
- };
-
-type KcContextLike_useGetErrors = KcContextLike_i18n & {
- messagesPerField: Pick;
- passwordPolicies?: PasswordPolicies;
-};
-
-assert<
- Extract<
- Extract,
- { pageId: "register.ftl" }
- > extends KcContextLike
- ? true
- : false
->();
-
-export type UserProfileApi = {
- getFormState: () => FormState;
- subscribeToFormState: (callback: () => void) => { unsubscribe: () => void };
- dispatchFormAction: (action: FormAction) => void;
-};
-
-const cachedUserProfileApiByKcContext = new WeakMap();
-
-export type ParamsOfGetUserProfileApi = {
- kcContext: KcContextLike;
- doMakeUserConfirmPassword: boolean;
-};
-
-export function getUserProfileApi(params: ParamsOfGetUserProfileApi): UserProfileApi {
- const { kcContext } = params;
-
- use_cache: {
- const userProfileApi_cache = cachedUserProfileApiByKcContext.get(kcContext);
-
- if (userProfileApi_cache === undefined) {
- break use_cache;
- }
-
- return userProfileApi_cache;
- }
-
- const userProfileApi = getUserProfileApi_noCache(params);
-
- cachedUserProfileApiByKcContext.set(kcContext, userProfileApi);
-
- return userProfileApi;
-}
-
-export function getUserProfileApi_noCache(
- params: ParamsOfGetUserProfileApi
-): UserProfileApi {
- return null as any;
-}
diff --git a/src/login/lib/getUserProfileApi/getUserProfileApi.ts b/src/login/lib/getUserProfileApi/getUserProfileApi.ts
new file mode 100644
index 00000000..31230ad3
--- /dev/null
+++ b/src/login/lib/getUserProfileApi/getUserProfileApi.ts
@@ -0,0 +1,1561 @@
+import "keycloakify/tools/Array.prototype.every";
+import { assert, type Equals } from "tsafe/assert";
+import type {
+ PasswordPolicies,
+ Attribute,
+ Validators
+} from "keycloakify/login/KcContext";
+import type { KcContext } from "../../KcContext";
+import type { KcContextLike as KcContextLike_i18n } from "keycloakify/login/i18n";
+import { formatNumber } from "keycloakify/tools/formatNumber";
+import type { MessageKey_defaultSet } from "keycloakify/login/i18n";
+import { emailRegexp } from "keycloakify/tools/emailRegExp";
+import { unFormatNumberOnSubmit } from "./kcNumberUnFormat";
+import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
+import { id } from "tsafe/id";
+
+export type FormFieldError = {
+ advancedMsgArgs: readonly [string, ...string[]];
+ source: FormFieldError.Source;
+ fieldIndex: number | undefined;
+};
+
+export namespace FormFieldError {
+ export type Source =
+ | Source.Validator
+ | Source.PasswordPolicy
+ | Source.Server
+ | Source.Other;
+
+ export namespace Source {
+ export type Validator = {
+ type: "validator";
+ name: keyof Validators;
+ };
+ export type PasswordPolicy = {
+ type: "passwordPolicy";
+ name: keyof PasswordPolicies;
+ };
+ export type Server = {
+ type: "server";
+ };
+
+ export type Other = {
+ type: "other";
+ rule: "passwordConfirmMatchesPassword" | "requiredField";
+ };
+ }
+}
+
+export type FormFieldState = {
+ attribute: Attribute;
+ displayableErrors: FormFieldError[];
+ valueOrValues: string | string[];
+};
+
+export type FormState = {
+ isFormSubmittable: boolean;
+ formFieldStates: FormFieldState[];
+};
+
+export type FormAction =
+ | {
+ action: "update";
+ name: string;
+ valueOrValues: string | string[];
+ /** Default false */
+ displayErrorsImmediately?: boolean;
+ }
+ | {
+ action: "focus lost";
+ name: string;
+ fieldIndex: number | undefined;
+ };
+
+export type KcContextLike = KcContextLike_i18n &
+ KcContextLike_useGetErrors & {
+ profile: {
+ attributesByName: Record;
+ html5DataAnnotations?: Record;
+ };
+ passwordRequired?: boolean;
+ realm: { registrationEmailAsUsername: boolean };
+ url: {
+ resourcesPath: string;
+ };
+ };
+
+type KcContextLike_useGetErrors = KcContextLike_i18n & {
+ messagesPerField: Pick;
+ passwordPolicies?: PasswordPolicies;
+};
+
+assert<
+ Extract<
+ Extract,
+ { pageId: "register.ftl" }
+ > extends KcContextLike
+ ? true
+ : false
+>();
+
+export type UserProfileApi = {
+ getFormState: () => FormState;
+ subscribeToFormState: (callback: () => void) => { unsubscribe: () => void };
+ dispatchFormAction: (action: FormAction) => void;
+};
+
+const cachedUserProfileApiByKcContext = new WeakMap();
+
+export type ParamsOfGetUserProfileApi = {
+ kcContext: KcContextLike;
+ doMakeUserConfirmPassword: boolean;
+};
+
+export function getUserProfileApi(params: ParamsOfGetUserProfileApi): UserProfileApi {
+ const { kcContext } = params;
+
+ use_cache: {
+ const userProfileApi_cache = cachedUserProfileApiByKcContext.get(kcContext);
+
+ if (userProfileApi_cache === undefined) {
+ break use_cache;
+ }
+
+ return userProfileApi_cache;
+ }
+
+ const userProfileApi = getUserProfileApi_noCache(params);
+
+ cachedUserProfileApiByKcContext.set(kcContext, userProfileApi);
+
+ return userProfileApi;
+}
+
+namespace internal {
+ export type FormFieldState = {
+ attribute: Attribute;
+ errors: FormFieldError[];
+ hasLostFocusAtLeastOnce: boolean | boolean[];
+ valueOrValues: string | string[];
+ };
+
+ export type State = {
+ formFieldStates: FormFieldState[];
+ };
+}
+
+export function getUserProfileApi_noCache(
+ params: ParamsOfGetUserProfileApi
+): UserProfileApi {
+ const { kcContext, doMakeUserConfirmPassword } = params;
+
+ unFormatNumberOnSubmit();
+
+ let state: internal.State = getInitialState({ kcContext });
+ const callbacks = new Set<() => void>();
+
+ return {
+ dispatchFormAction: action => {
+ state = reducer({ action, kcContext, doMakeUserConfirmPassword, state });
+
+ callbacks.forEach(callback => callback());
+ },
+ getFormState: () => formStateSelector({ state }),
+ subscribeToFormState: callback => {
+ callbacks.add(callback);
+ return {
+ unsubscribe: () => {
+ callbacks.delete(callback);
+ }
+ };
+ }
+ };
+}
+
+function getInitialState(params: { kcContext: KcContextLike }): internal.State {
+ const { kcContext } = params;
+
+ const { getErrors } = createGetErrors({ kcContext });
+
+ // NOTE: We don't use te kcContext.profile.attributes directly because
+ // they don't includes the password and password confirm fields and we want to add them.
+ // We also want to apply some retro-compatibility and consistency patches.
+ const attributes: Attribute[] = (() => {
+ mock_user_profile_attributes_for_older_keycloak_versions: {
+ if (
+ "profile" in kcContext &&
+ "attributesByName" in kcContext.profile &&
+ Object.keys(kcContext.profile.attributesByName).length !== 0
+ ) {
+ break mock_user_profile_attributes_for_older_keycloak_versions;
+ }
+
+ if (
+ "register" in kcContext &&
+ kcContext.register instanceof Object &&
+ "formData" in kcContext.register
+ ) {
+ //NOTE: Handle legacy register.ftl page
+ return (["firstName", "lastName", "email", "username"] as const)
+ .filter(name =>
+ name !== "username"
+ ? true
+ : !kcContext.realm.registrationEmailAsUsername
+ )
+ .map(name =>
+ id({
+ name: name,
+ displayName: id<`\${${MessageKey_defaultSet}}`>(
+ `\${${name}}`
+ ),
+ required: true,
+ value: (kcContext.register as any).formData[name] ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: (() => {
+ switch (name) {
+ case "email":
+ return "email";
+ case "username":
+ return "username";
+ default:
+ return undefined;
+ }
+ })()
+ })
+ );
+ }
+
+ if ("user" in kcContext && kcContext.user instanceof Object) {
+ //NOTE: Handle legacy login-update-profile.ftl
+ return (["username", "email", "firstName", "lastName"] as const)
+ .filter(name =>
+ name !== "username"
+ ? true
+ : (kcContext.user as any).editUsernameAllowed
+ )
+ .map(name =>
+ id({
+ name: name,
+ displayName: id<`\${${MessageKey_defaultSet}}`>(
+ `\${${name}}`
+ ),
+ required: true,
+ value: (kcContext as any).user[name] ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: (() => {
+ switch (name) {
+ case "email":
+ return "email";
+ case "username":
+ return "username";
+ default:
+ return undefined;
+ }
+ })()
+ })
+ );
+ }
+
+ if ("email" in kcContext && kcContext.email instanceof Object) {
+ //NOTE: Handle legacy update-email.ftl
+ return [
+ id({
+ name: "email",
+ displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
+ required: true,
+ value: (kcContext.email as any).value ?? "",
+ html5DataAnnotations: {},
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: "email"
+ })
+ ];
+ }
+
+ assert(false, "Unable to mock user profile from the current kcContext");
+ }
+
+ return Object.values(kcContext.profile.attributesByName).map(
+ structuredCloneButFunctions
+ );
+ })();
+
+ // Retro-compatibility and consistency patches
+ attributes.forEach(attribute => {
+ patch_legacy_group: {
+ if (typeof attribute.group !== "string") {
+ break patch_legacy_group;
+ }
+
+ const {
+ group,
+ groupDisplayHeader,
+ groupDisplayDescription,
+ groupAnnotations
+ } = attribute as Attribute & {
+ group: string;
+ groupDisplayHeader?: string;
+ groupDisplayDescription?: string;
+ groupAnnotations: Record;
+ };
+
+ delete attribute.group;
+ // @ts-expect-error
+ delete attribute.groupDisplayHeader;
+ // @ts-expect-error
+ delete attribute.groupDisplayDescription;
+ // @ts-expect-error
+ delete attribute.groupAnnotations;
+
+ if (group === "") {
+ break patch_legacy_group;
+ }
+
+ attribute.group = {
+ name: group,
+ displayHeader: groupDisplayHeader,
+ displayDescription: groupDisplayDescription,
+ annotations: groupAnnotations,
+ html5DataAnnotations: {}
+ };
+ }
+
+ // Attributes with options rendered by default as select inputs
+ if (
+ attribute.validators.options !== undefined &&
+ attribute.annotations.inputType === undefined
+ ) {
+ attribute.annotations.inputType = "select";
+ }
+
+ // Consistency patch on values/value property
+ {
+ if (getIsMultivaluedSingleField({ attribute })) {
+ attribute.multivalued = true;
+ }
+
+ if (attribute.multivalued) {
+ attribute.values ??=
+ attribute.value !== undefined ? [attribute.value] : [];
+ delete attribute.value;
+ } else {
+ attribute.value ??= attribute.values?.[0];
+ delete attribute.values;
+ }
+ }
+ });
+
+ add_password_and_password_confirm: {
+ if (!kcContext.passwordRequired) {
+ break add_password_and_password_confirm;
+ }
+
+ attributes.forEach((attribute, i) => {
+ if (
+ attribute.name !==
+ (kcContext.realm.registrationEmailAsUsername ? "email" : "username")
+ ) {
+ // NOTE: We want to add password and password-confirm after the field that identifies the user.
+ // It's either email or username.
+ return;
+ }
+
+ attributes.splice(
+ i + 1,
+ 0,
+ {
+ name: "password",
+ displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"),
+ required: true,
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ autocomplete: "new-password",
+ html5DataAnnotations: {}
+ },
+ {
+ name: "password-confirm",
+ displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"),
+ required: true,
+ readOnly: false,
+ validators: {},
+ annotations: {},
+ html5DataAnnotations: {},
+ autocomplete: "new-password"
+ }
+ );
+ });
+ }
+
+ const initialFormFieldState: {
+ attribute: Attribute;
+ valueOrValues: string | string[];
+ }[] = [];
+
+ for (const attribute of attributes) {
+ handle_multi_valued_attribute: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_attribute;
+ }
+
+ const values = attribute.values?.length ? attribute.values : [""];
+
+ apply_validator_min_range: {
+ if (getIsMultivaluedSingleField({ attribute })) {
+ break apply_validator_min_range;
+ }
+
+ const validator = attribute.validators.multivalued;
+
+ if (validator === undefined) {
+ break apply_validator_min_range;
+ }
+
+ const { min: minStr } = validator;
+
+ if (!minStr) {
+ break apply_validator_min_range;
+ }
+
+ const min = parseInt(`${minStr}`);
+
+ for (let index = values.length; index < min; index++) {
+ values.push("");
+ }
+ }
+
+ initialFormFieldState.push({
+ attribute,
+ valueOrValues: values
+ });
+
+ continue;
+ }
+
+ initialFormFieldState.push({
+ attribute,
+ valueOrValues: attribute.value ?? ""
+ });
+ }
+
+ const initialState: internal.State = {
+ formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
+ attribute,
+ errors: getErrors({
+ attributeName: attribute.name,
+ formFieldStates: initialFormFieldState
+ }),
+ hasLostFocusAtLeastOnce:
+ valueOrValues instanceof Array &&
+ !getIsMultivaluedSingleField({ attribute })
+ ? valueOrValues.map(() => false)
+ : false,
+ valueOrValues: valueOrValues
+ }))
+ };
+
+ return initialState;
+}
+
+const formStateByState = new WeakMap();
+
+function formStateSelector(params: { state: internal.State }): FormState {
+ const { state } = params;
+
+ use_memoized_value: {
+ const formState = formStateByState.get(state);
+ if (formState === undefined) {
+ break use_memoized_value;
+ }
+ return formState;
+ }
+
+ return {
+ formFieldStates: state.formFieldStates.map(
+ ({
+ errors,
+ hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr,
+ attribute,
+ ...valueOrValuesWrap
+ }) => ({
+ displayableErrors: errors.filter(error => {
+ const hasLostFocusAtLeastOnce =
+ typeof hasLostFocusAtLeastOnceOrArr === "boolean"
+ ? hasLostFocusAtLeastOnceOrArr
+ : error.fieldIndex !== undefined
+ ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
+ : hasLostFocusAtLeastOnceOrArr[
+ hasLostFocusAtLeastOnceOrArr.length - 1
+ ];
+
+ switch (error.source.type) {
+ case "server":
+ return true;
+ case "other":
+ switch (error.source.rule) {
+ case "requiredField":
+ return hasLostFocusAtLeastOnce;
+ case "passwordConfirmMatchesPassword":
+ return hasLostFocusAtLeastOnce;
+ }
+ assert>(false);
+ case "passwordPolicy":
+ switch (error.source.name) {
+ case "length":
+ return hasLostFocusAtLeastOnce;
+ case "digits":
+ return hasLostFocusAtLeastOnce;
+ case "lowerCase":
+ return hasLostFocusAtLeastOnce;
+ case "upperCase":
+ return hasLostFocusAtLeastOnce;
+ case "specialChars":
+ return hasLostFocusAtLeastOnce;
+ case "notUsername":
+ return true;
+ case "notEmail":
+ return true;
+ }
+ assert>(false);
+ case "validator":
+ switch (error.source.name) {
+ case "length":
+ return hasLostFocusAtLeastOnce;
+ case "pattern":
+ return hasLostFocusAtLeastOnce;
+ case "email":
+ return hasLostFocusAtLeastOnce;
+ case "integer":
+ return hasLostFocusAtLeastOnce;
+ case "multivalued":
+ return hasLostFocusAtLeastOnce;
+ case "options":
+ return hasLostFocusAtLeastOnce;
+ }
+ assert>(false);
+ }
+ }),
+ attribute,
+ ...valueOrValuesWrap
+ })
+ ),
+ isFormSubmittable: state.formFieldStates.every(
+ ({ errors }) => errors.length === 0
+ )
+ };
+}
+
+function reducer(params: {
+ state: internal.State;
+ kcContext: KcContextLike;
+ doMakeUserConfirmPassword: boolean;
+ action: FormAction;
+}): internal.State {
+ const { kcContext, doMakeUserConfirmPassword, action } = params;
+ let { state } = params;
+
+ const { getErrors } = createGetErrors({ kcContext });
+
+ const formFieldState = state.formFieldStates.find(
+ ({ attribute }) => attribute.name === action.name
+ );
+
+ assert(formFieldState !== undefined);
+
+ (() => {
+ switch (action.action) {
+ case "update":
+ formFieldState.valueOrValues = action.valueOrValues;
+
+ apply_formatters: {
+ const { attribute } = formFieldState;
+
+ const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberFormat) {
+ break apply_formatters;
+ }
+
+ if (formFieldState.valueOrValues instanceof Array) {
+ formFieldState.valueOrValues = formFieldState.valueOrValues.map(
+ value => formatNumber(value, kcNumberFormat)
+ );
+ } else {
+ formFieldState.valueOrValues = formatNumber(
+ formFieldState.valueOrValues,
+ kcNumberFormat
+ );
+ }
+ }
+
+ formFieldState.errors = getErrors({
+ attributeName: action.name,
+ formFieldStates: state.formFieldStates
+ });
+
+ simulate_focus_lost: {
+ const { displayErrorsImmediately = false } = action;
+
+ if (!displayErrorsImmediately) {
+ break simulate_focus_lost;
+ }
+
+ for (const fieldIndex of action.valueOrValues instanceof Array
+ ? action.valueOrValues.map((...[, index]) => index)
+ : [undefined]) {
+ state = reducer({
+ state,
+ kcContext,
+ doMakeUserConfirmPassword,
+ action: {
+ action: "focus lost",
+ name: action.name,
+ fieldIndex
+ }
+ });
+ }
+ }
+
+ update_password_confirm: {
+ if (doMakeUserConfirmPassword) {
+ break update_password_confirm;
+ }
+
+ if (action.name !== "password") {
+ break update_password_confirm;
+ }
+
+ state = reducer({
+ state,
+ kcContext,
+ doMakeUserConfirmPassword,
+ action: {
+ action: "update",
+ name: "password-confirm",
+ valueOrValues: action.valueOrValues,
+ displayErrorsImmediately: action.displayErrorsImmediately
+ }
+ });
+ }
+
+ trigger_password_confirm_validation_on_password_change: {
+ if (!doMakeUserConfirmPassword) {
+ break trigger_password_confirm_validation_on_password_change;
+ }
+
+ if (action.name !== "password") {
+ break trigger_password_confirm_validation_on_password_change;
+ }
+
+ state = reducer({
+ state,
+ kcContext,
+ doMakeUserConfirmPassword,
+ action: {
+ action: "update",
+ name: "password-confirm",
+ valueOrValues: (() => {
+ const formFieldState = state.formFieldStates.find(
+ ({ attribute }) =>
+ attribute.name === "password-confirm"
+ );
+
+ assert(formFieldState !== undefined);
+
+ return formFieldState.valueOrValues;
+ })(),
+ displayErrorsImmediately: action.displayErrorsImmediately
+ }
+ });
+ }
+
+ return;
+ case "focus lost":
+ if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
+ const { fieldIndex } = action;
+ assert(fieldIndex !== undefined);
+ formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
+ return;
+ }
+
+ formFieldState.hasLostFocusAtLeastOnce = true;
+ return;
+ }
+ assert>(false);
+ })();
+
+ return { ...state };
+}
+
+function createGetErrors(params: { kcContext: KcContextLike_useGetErrors }) {
+ const { kcContext } = params;
+
+ const { messagesPerField, passwordPolicies } = kcContext;
+
+ function getErrors(params: {
+ attributeName: string;
+ formFieldStates: {
+ attribute: Attribute;
+ valueOrValues: string | string[];
+ }[];
+ }): FormFieldError[] {
+ const { attributeName, formFieldStates } = params;
+
+ const formFieldState = formFieldStates.find(
+ ({ attribute }) => attribute.name === attributeName
+ );
+
+ assert(formFieldState !== undefined);
+
+ const { attribute } = formFieldState;
+
+ const valueOrValues = (() => {
+ let { valueOrValues } = formFieldState;
+
+ unFormat_number: {
+ const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberUnFormat) {
+ break unFormat_number;
+ }
+
+ if (valueOrValues instanceof Array) {
+ valueOrValues = valueOrValues.map(value =>
+ formatNumber(value, kcNumberUnFormat)
+ );
+ } else {
+ valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
+ }
+ }
+
+ return valueOrValues;
+ })();
+
+ assert(attribute !== undefined);
+
+ server_side_error: {
+ if (attribute.multivalued) {
+ const defaultValues = attribute.values?.length ? attribute.values : [""];
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ if (
+ JSON.stringify(defaultValues) !==
+ JSON.stringify(values.slice(0, defaultValues.length))
+ ) {
+ break server_side_error;
+ }
+ } else {
+ const defaultValue = attribute.value ?? "";
+
+ assert(typeof valueOrValues === "string");
+
+ const value = valueOrValues;
+
+ if (defaultValue !== value) {
+ break server_side_error;
+ }
+ }
+
+ let doesErrorExist: boolean;
+
+ try {
+ doesErrorExist = messagesPerField.existsError(attributeName);
+ } catch {
+ break server_side_error;
+ }
+
+ if (!doesErrorExist) {
+ break server_side_error;
+ }
+
+ const errorMessageStr = messagesPerField.get(attributeName);
+
+ return [
+ {
+ advancedMsgArgs: [errorMessageStr],
+ fieldIndex: undefined,
+ source: {
+ type: "server"
+ }
+ }
+ ];
+ }
+
+ handle_multi_valued_multi_fields: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_multi_fields;
+ }
+
+ if (getIsMultivaluedSingleField({ attribute })) {
+ break handle_multi_valued_multi_fields;
+ }
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ const errors = values
+ .map((...[, index]) => {
+ const specificValueErrors = getErrors({
+ attributeName,
+ formFieldStates: formFieldStates.map(formFieldState => {
+ if (formFieldState.attribute.name === attributeName) {
+ assert(formFieldState.valueOrValues instanceof Array);
+ return {
+ attribute: {
+ ...attribute,
+ annotations: {
+ ...attribute.annotations,
+ inputType: undefined
+ },
+ multivalued: false
+ },
+ valueOrValues: formFieldState.valueOrValues[index]
+ };
+ }
+
+ return formFieldState;
+ })
+ });
+
+ return specificValueErrors
+ .filter(error => {
+ if (
+ error.source.type === "other" &&
+ error.source.rule === "requiredField"
+ ) {
+ return false;
+ }
+
+ return true;
+ })
+ .map(
+ (error): FormFieldError => ({
+ ...error,
+ fieldIndex: index
+ })
+ );
+ })
+ .reduce((acc, errors) => [...acc, ...errors], []);
+
+ required_field: {
+ if (!attribute.required) {
+ break required_field;
+ }
+
+ if (values.every(value => value !== "")) {
+ break required_field;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "error-user-attribute-required" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "requiredField"
+ }
+ });
+ }
+
+ return errors;
+ }
+
+ handle_multi_valued_single_field: {
+ if (!attribute.multivalued) {
+ break handle_multi_valued_single_field;
+ }
+
+ if (!getIsMultivaluedSingleField({ attribute })) {
+ break handle_multi_valued_single_field;
+ }
+
+ const validatorName = "multivalued";
+
+ const validator = attribute.validators[validatorName];
+
+ if (validator === undefined) {
+ return [];
+ }
+
+ const { min: minStr } = validator;
+
+ const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
+
+ assert(!isNaN(min));
+
+ const { max: maxStr } = validator;
+
+ const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
+
+ assert(!isNaN(max));
+
+ assert(valueOrValues instanceof Array);
+
+ const values = valueOrValues;
+
+ if (min <= values.length && values.length <= max) {
+ return [];
+ }
+
+ return [
+ {
+ advancedMsgArgs: [
+ "error-invalid-multivalued-size" satisfies MessageKey_defaultSet,
+ `${min}`,
+ `${max}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ }
+ ];
+ }
+
+ assert(typeof valueOrValues === "string");
+
+ const value = valueOrValues;
+
+ const errors: FormFieldError[] = [];
+
+ check_password_policies: {
+ if (attributeName !== "password") {
+ break check_password_policies;
+ }
+
+ if (passwordPolicies === undefined) {
+ break check_password_policies;
+ }
+
+ check_password_policy_x: {
+ const policyName = "length";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minLength = policy;
+
+ if (value.length >= minLength) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordMinLengthMessage" satisfies MessageKey_defaultSet,
+ `${minLength}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "digits";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfDigits = policy;
+
+ if (
+ value.split("").filter(char => !isNaN(parseInt(char))).length >=
+ minNumberOfDigits
+ ) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordMinDigitsMessage" satisfies MessageKey_defaultSet,
+ `${minNumberOfDigits}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "lowerCase";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfLowerCaseChar = policy;
+
+ if (
+ value
+ .split("")
+ .filter(
+ char =>
+ char === char.toLowerCase() && char !== char.toUpperCase()
+ ).length >= minNumberOfLowerCaseChar
+ ) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordMinLowerCaseCharsMessage" satisfies MessageKey_defaultSet,
+ `${minNumberOfLowerCaseChar}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "upperCase";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfUpperCaseChar = policy;
+
+ if (
+ value
+ .split("")
+ .filter(
+ char =>
+ char === char.toUpperCase() && char !== char.toLowerCase()
+ ).length >= minNumberOfUpperCaseChar
+ ) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordMinUpperCaseCharsMessage" satisfies MessageKey_defaultSet,
+ `${minNumberOfUpperCaseChar}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "specialChars";
+
+ const policy = passwordPolicies[policyName];
+
+ if (!policy) {
+ break check_password_policy_x;
+ }
+
+ const minNumberOfSpecialChar = policy;
+
+ if (
+ value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >=
+ minNumberOfSpecialChar
+ ) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordMinSpecialCharsMessage" satisfies MessageKey_defaultSet,
+ `${minNumberOfSpecialChar}`
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "notUsername";
+
+ const notUsername = passwordPolicies[policyName];
+
+ if (!notUsername) {
+ break check_password_policy_x;
+ }
+
+ const usernameFormFieldState = formFieldStates.find(
+ formFieldState => formFieldState.attribute.name === "username"
+ );
+
+ if (!usernameFormFieldState) {
+ break check_password_policy_x;
+ }
+
+ const usernameValue = (() => {
+ let { valueOrValues } = usernameFormFieldState;
+
+ assert(typeof valueOrValues === "string");
+
+ unFormat_number: {
+ const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
+
+ if (!kcNumberUnFormat) {
+ break unFormat_number;
+ }
+
+ valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
+ }
+
+ return valueOrValues;
+ })();
+
+ if (usernameValue === "") {
+ break check_password_policy_x;
+ }
+
+ if (value !== usernameValue) {
+ break check_password_policy_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordNotUsernameMessage" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+
+ check_password_policy_x: {
+ const policyName = "notEmail";
+
+ const notEmail = passwordPolicies[policyName];
+
+ if (!notEmail) {
+ break check_password_policy_x;
+ }
+
+ const emailFormFieldState = formFieldStates.find(
+ formFieldState => formFieldState.attribute.name === "email"
+ );
+
+ if (!emailFormFieldState) {
+ break check_password_policy_x;
+ }
+
+ assert(typeof emailFormFieldState.valueOrValues === "string");
+
+ {
+ const emailValue = emailFormFieldState.valueOrValues;
+
+ if (emailValue === "") {
+ break check_password_policy_x;
+ }
+
+ if (value !== emailValue) {
+ break check_password_policy_x;
+ }
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordNotEmailMessage" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "passwordPolicy",
+ name: policyName
+ }
+ });
+ }
+ }
+
+ password_confirm_matches_password: {
+ if (attributeName !== "password-confirm") {
+ break password_confirm_matches_password;
+ }
+
+ const passwordFormFieldState = formFieldStates.find(
+ formFieldState => formFieldState.attribute.name === "password"
+ );
+
+ assert(passwordFormFieldState !== undefined);
+
+ assert(typeof passwordFormFieldState.valueOrValues === "string");
+
+ {
+ const passwordValue = passwordFormFieldState.valueOrValues;
+
+ if (value === passwordValue) {
+ break password_confirm_matches_password;
+ }
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidPasswordConfirmMessage" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "passwordConfirmMatchesPassword"
+ }
+ });
+ }
+
+ const { validators } = attribute;
+
+ required_field: {
+ if (!attribute.required) {
+ break required_field;
+ }
+
+ if (value !== "") {
+ break required_field;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "error-user-attribute-required" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "other",
+ rule: "requiredField"
+ }
+ });
+ }
+
+ validator_x: {
+ const validatorName = "length";
+
+ const validator = validators[validatorName];
+
+ if (!validator) {
+ break validator_x;
+ }
+
+ const {
+ "ignore.empty.value": ignoreEmptyValue = false,
+ max,
+ min
+ } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ const source: FormFieldError.Source = {
+ type: "validator",
+ name: validatorName
+ };
+
+ if (max && value.length > parseInt(`${max}`)) {
+ errors.push({
+ advancedMsgArgs: [
+ "error-invalid-length-too-long" satisfies MessageKey_defaultSet,
+ `${max}`
+ ] as const,
+ fieldIndex: undefined,
+ source
+ });
+ }
+
+ if (min && value.length < parseInt(`${min}`)) {
+ errors.push({
+ advancedMsgArgs: [
+ "error-invalid-length-too-short" satisfies MessageKey_defaultSet,
+ `${min}`
+ ] as const,
+ fieldIndex: undefined,
+ source
+ });
+ }
+ }
+
+ validator_x: {
+ const validatorName = "pattern";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const {
+ "ignore.empty.value": ignoreEmptyValue = false,
+ pattern,
+ "error-message": errorMessageKey
+ } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ if (new RegExp(pattern).test(value)) {
+ break validator_x;
+ }
+
+ const msgArgs = [
+ errorMessageKey ?? ("shouldMatchPattern" satisfies MessageKey_defaultSet),
+ pattern
+ ] as const;
+
+ errors.push({
+ advancedMsgArgs: msgArgs,
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ validator_x: {
+ {
+ const lastError = errors[errors.length - 1];
+ if (
+ lastError !== undefined &&
+ lastError.source.type === "validator" &&
+ lastError.source.name === "pattern"
+ ) {
+ break validator_x;
+ }
+ }
+
+ const validatorName = "email";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ if (emailRegexp.test(value)) {
+ break validator_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "invalidEmailMessage" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ validator_x: {
+ const validatorName = "integer";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ const {
+ "ignore.empty.value": ignoreEmptyValue = false,
+ max,
+ min
+ } = validator;
+
+ if (ignoreEmptyValue && value === "") {
+ break validator_x;
+ }
+
+ const intValue = parseInt(value);
+
+ const source: FormFieldError.Source = {
+ type: "validator",
+ name: validatorName
+ };
+
+ if (isNaN(intValue)) {
+ const msgArgs = ["mustBeAnInteger"] as const;
+
+ errors.push({
+ advancedMsgArgs: msgArgs,
+ fieldIndex: undefined,
+ source
+ });
+
+ break validator_x;
+ }
+
+ if (max && intValue > parseInt(`${max}`)) {
+ errors.push({
+ advancedMsgArgs: [
+ "error-number-out-of-range-too-big" satisfies MessageKey_defaultSet,
+ `${max}`
+ ] as const,
+ fieldIndex: undefined,
+ source
+ });
+
+ break validator_x;
+ }
+
+ if (min && intValue < parseInt(`${min}`)) {
+ errors.push({
+ advancedMsgArgs: [
+ "error-number-out-of-range-too-small" satisfies MessageKey_defaultSet,
+ `${min}`
+ ] as const,
+ fieldIndex: undefined,
+ source
+ });
+ break validator_x;
+ }
+ }
+
+ validator_x: {
+ const validatorName = "options";
+
+ const validator = validators[validatorName];
+
+ if (validator === undefined) {
+ break validator_x;
+ }
+
+ if (value === "") {
+ break validator_x;
+ }
+
+ if (validator.options.indexOf(value) >= 0) {
+ break validator_x;
+ }
+
+ errors.push({
+ advancedMsgArgs: [
+ "notAValidOption" satisfies MessageKey_defaultSet
+ ] as const,
+ fieldIndex: undefined,
+ source: {
+ type: "validator",
+ name: validatorName
+ }
+ });
+ }
+
+ //TODO: Implement missing validators. See Validators type definition.
+
+ return errors;
+ }
+
+ return { getErrors };
+}
+
+function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
+ const { attribute } = params;
+
+ return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
+}
+
+export function getButtonToDisplayForMultivaluedAttributeField(params: {
+ attribute: Attribute;
+ values: string[];
+ fieldIndex: number;
+}) {
+ const { attribute, values, fieldIndex } = params;
+
+ const hasRemove = (() => {
+ if (values.length === 1) {
+ return false;
+ }
+
+ const minCount = (() => {
+ const { multivalued } = attribute.validators;
+
+ if (multivalued === undefined) {
+ return undefined;
+ }
+
+ const minStr = multivalued.min;
+
+ if (minStr === undefined) {
+ return undefined;
+ }
+
+ return parseInt(`${minStr}`);
+ })();
+
+ if (minCount === undefined) {
+ return true;
+ }
+
+ if (values.length === minCount) {
+ return false;
+ }
+
+ return true;
+ })();
+
+ const hasAdd = (() => {
+ if (fieldIndex + 1 !== values.length) {
+ return false;
+ }
+
+ const maxCount = (() => {
+ const { multivalued } = attribute.validators;
+
+ if (multivalued === undefined) {
+ return undefined;
+ }
+
+ const maxStr = multivalued.max;
+
+ if (maxStr === undefined) {
+ return undefined;
+ }
+
+ return parseInt(`${maxStr}`);
+ })();
+
+ if (maxCount === undefined) {
+ return true;
+ }
+
+ return values.length !== maxCount;
+ })();
+
+ return { hasRemove, hasAdd };
+}
diff --git a/src/login/lib/getUserProfileApi/index.ts b/src/login/lib/getUserProfileApi/index.ts
new file mode 100644
index 00000000..2a716ac3
--- /dev/null
+++ b/src/login/lib/getUserProfileApi/index.ts
@@ -0,0 +1 @@
+export * from "./getUserProfileApi";
diff --git a/src/login/lib/getUserProfileApi/kcNumberUnFormat.ts b/src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
new file mode 100644
index 00000000..e852fcde
--- /dev/null
+++ b/src/login/lib/getUserProfileApi/kcNumberUnFormat.ts
@@ -0,0 +1,109 @@
+import { assert } from "keycloakify/tools/assert";
+let cleanup: (() => void) | undefined;
+const handledElements = new WeakSet();
+const KC_NUMBER_UNFORMAT = "kcNumberUnFormat";
+const SELECTOR = `input[data-${KC_NUMBER_UNFORMAT}]`;
+
+export function unFormatNumberOnSubmit() {
+ cleanup?.();
+
+ const handleElement = (element: HTMLInputElement) => {
+ if (handledElements.has(element)) {
+ return;
+ }
+
+ const form = element.closest("form");
+
+ if (form === null) {
+ return;
+ }
+
+ form.addEventListener("submit", () => {
+ const rawFormat = element.getAttribute(`data-${KC_NUMBER_UNFORMAT}`);
+ if (rawFormat) {
+ element.value = formatNumber(element.value, rawFormat);
+ }
+ });
+
+ handledElements.add(element);
+ };
+
+ document.querySelectorAll(SELECTOR).forEach(element => {
+ assert(element instanceof HTMLInputElement);
+ handleElement(element);
+ });
+
+ const observer = new MutationObserver(mutationsList => {
+ for (const mutation of mutationsList) {
+ if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
+ mutation.addedNodes.forEach(node => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = (node as HTMLElement).querySelector(SELECTOR);
+ if (element !== null) {
+ assert(element instanceof HTMLInputElement);
+ handleElement(element);
+ }
+ }
+ });
+ }
+ }
+ });
+
+ observer.observe(document.body, { childList: true, subtree: true });
+
+ cleanup = () => observer.disconnect();
+}
+
+// NOTE: Keycloak code
+const formatNumber = (input: string, format: string) => {
+ if (!input) {
+ return "";
+ }
+
+ // array holding the patterns for the number of expected digits in each part
+ const digitPattern = format.match(/{\d+}/g);
+
+ if (!digitPattern) {
+ return "";
+ }
+
+ // calculate the maximum size of the given pattern based on the sum of the expected digits
+ const maxSize = digitPattern.reduce(
+ (total, p) => total + parseInt(p.replace("{", "").replace("}", "")),
+ 0
+ );
+
+ // keep only digits
+ let rawValue = input.replace(/\D+/g, "");
+
+ // make sure the value is a number
+ //@ts-expect-error
+ if (parseInt(rawValue) != rawValue) {
+ return "";
+ }
+
+ // make sure the number of digits does not exceed the maximum size
+ if (rawValue.length > maxSize) {
+ rawValue = rawValue.substring(0, maxSize);
+ }
+
+ // build the regex based based on the expected digits in each part
+ const formatter = digitPattern.reduce((result, p) => result + `(\\d${p})`, "^");
+
+ // if the current digits match the pattern we have each group of digits in an array
+ let digits = new RegExp(formatter).exec(rawValue);
+
+ // no match, return the raw value without any format
+ if (!digits) {
+ return input;
+ }
+
+ let result = format;
+
+ // finally format the current digits accordingly to the given format
+ for (let i = 0; i < digitPattern.length; i++) {
+ result = result.replace(digitPattern[i], digits[i + 1]);
+ }
+
+ return result;
+};
diff --git a/src/login/lib/useUserProfileForm copy.tsx b/src/login/lib/useUserProfileForm copy.tsx
deleted file mode 100644
index 31ad939e..00000000
--- a/src/login/lib/useUserProfileForm copy.tsx
+++ /dev/null
@@ -1,1403 +0,0 @@
-import "keycloakify/tools/Array.prototype.every";
-import { useMemo, useReducer, useEffect, Fragment, type Dispatch } from "react";
-import { assert, type Equals } from "tsafe/assert";
-import { id } from "tsafe/id";
-import { structuredCloneButFunctions } from "keycloakify/tools/structuredCloneButFunctions";
-import { kcSanitize } from "keycloakify/lib/kcSanitize";
-import { useConstCallback } from "keycloakify/tools/useConstCallback";
-import { emailRegexp } from "keycloakify/tools/emailRegExp";
-import { formatNumber } from "keycloakify/tools/formatNumber";
-import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
-import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
-import type { KcContext } from "../KcContext";
-import type { MessageKey_defaultSet } from "keycloakify/login/i18n";
-import type { I18n } from "../i18n";
-
-export type FormFieldError = {
- errorMessage: JSX.Element;
- errorMessageStr: string;
- source: FormFieldError.Source;
- fieldIndex: number | undefined;
-};
-
-export namespace FormFieldError {
- export type Source = Source.Validator | Source.PasswordPolicy | Source.Server | Source.Other;
-
- export namespace Source {
- export type Validator = {
- type: "validator";
- name: keyof Validators;
- };
- export type PasswordPolicy = {
- type: "passwordPolicy";
- name: keyof PasswordPolicies;
- };
- export type Server = {
- type: "server";
- };
-
- export type Other = {
- type: "other";
- rule: "passwordConfirmMatchesPassword" | "requiredField";
- };
- }
-}
-
-export type FormFieldState = {
- attribute: Attribute;
- displayableErrors: FormFieldError[];
- valueOrValues: string | string[];
-};
-
-export type FormState = {
- isFormSubmittable: boolean;
- formFieldStates: FormFieldState[];
-};
-
-export type FormAction =
- | {
- action: "update";
- name: string;
- valueOrValues: string | string[];
- /** Default false */
- displayErrorsImmediately?: boolean;
- }
- | {
- action: "focus lost";
- name: string;
- fieldIndex: number | undefined;
- };
-
-export type KcContextLike = KcContextLike_useGetErrors & {
- profile: {
- attributesByName: Record;
- html5DataAnnotations?: Record;
- };
- passwordRequired?: boolean;
- realm: { registrationEmailAsUsername: boolean };
- url: {
- resourcesPath: string;
- };
-};
-
-assert, { pageId: "register.ftl" }> extends KcContextLike ? true : false>();
-
-export type UseUserProfileFormParams = {
- kcContext: KcContextLike;
- i18n: I18n;
- doMakeUserConfirmPassword: boolean;
-};
-
-export type ReturnTypeOfUseUserProfileForm = {
- formState: FormState;
- dispatchFormAction: Dispatch;
-};
-
-namespace internal {
- export type FormFieldState = {
- attribute: Attribute;
- errors: FormFieldError[];
- hasLostFocusAtLeastOnce: boolean | boolean[];
- valueOrValues: string | string[];
- };
-
- export type State = {
- formFieldStates: FormFieldState[];
- };
-}
-
-export function useUserProfileForm(params: UseUserProfileFormParams): ReturnTypeOfUseUserProfileForm {
- const { kcContext, i18n, doMakeUserConfirmPassword } = params;
-
- const { insertScriptTags } = useInsertScriptTags({
- componentOrHookName: "useUserProfileForm",
- scriptTags: Object.keys(kcContext.profile?.html5DataAnnotations ?? {})
- .filter(key => key !== "kcMultivalued" && key !== "kcNumberFormat") // NOTE: Keycloakify handles it.
- .map(key => ({
- type: "module",
- src: `${kcContext.url.resourcesPath}/js/${key}.js`
- }))
- });
-
- useEffect(() => {
- insertScriptTags();
- }, []);
-
- const { getErrors } = useGetErrors({
- kcContext,
- i18n
- });
-
- const initialState = useMemo((): internal.State => {
- // NOTE: We don't use te kcContext.profile.attributes directly because
- // they don't includes the password and password confirm fields and we want to add them.
- // We also want to apply some retro-compatibility and consistency patches.
- const attributes: Attribute[] = (() => {
- mock_user_profile_attributes_for_older_keycloak_versions: {
- if (
- "profile" in kcContext &&
- "attributesByName" in kcContext.profile &&
- Object.keys(kcContext.profile.attributesByName).length !== 0
- ) {
- break mock_user_profile_attributes_for_older_keycloak_versions;
- }
-
- if ("register" in kcContext && kcContext.register instanceof Object && "formData" in kcContext.register) {
- //NOTE: Handle legacy register.ftl page
- return (["firstName", "lastName", "email", "username"] as const)
- .filter(name => (name !== "username" ? true : !kcContext.realm.registrationEmailAsUsername))
- .map(name =>
- id({
- name: name,
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
- required: true,
- value: (kcContext.register as any).formData[name] ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: (() => {
- switch (name) {
- case "email":
- return "email";
- case "username":
- return "username";
- default:
- return undefined;
- }
- })()
- })
- );
- }
-
- if ("user" in kcContext && kcContext.user instanceof Object) {
- //NOTE: Handle legacy login-update-profile.ftl
- return (["username", "email", "firstName", "lastName"] as const)
- .filter(name => (name !== "username" ? true : (kcContext.user as any).editUsernameAllowed))
- .map(name =>
- id({
- name: name,
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${${name}}`),
- required: true,
- value: (kcContext as any).user[name] ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: (() => {
- switch (name) {
- case "email":
- return "email";
- case "username":
- return "username";
- default:
- return undefined;
- }
- })()
- })
- );
- }
-
- if ("email" in kcContext && kcContext.email instanceof Object) {
- //NOTE: Handle legacy update-email.ftl
- return [
- id({
- name: "email",
- displayName: id<`\${${MessageKey_defaultSet}}`>(`\${email}`),
- required: true,
- value: (kcContext.email as any).value ?? "",
- html5DataAnnotations: {},
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: "email"
- })
- ];
- }
-
- assert(false, "Unable to mock user profile from the current kcContext");
- }
-
- return Object.values(kcContext.profile.attributesByName).map(structuredCloneButFunctions);
- })();
-
- // Retro-compatibility and consistency patches
- attributes.forEach(attribute => {
- patch_legacy_group: {
- if (typeof attribute.group !== "string") {
- break patch_legacy_group;
- }
-
- const { group, groupDisplayHeader, groupDisplayDescription, groupAnnotations } = attribute as Attribute & {
- group: string;
- groupDisplayHeader?: string;
- groupDisplayDescription?: string;
- groupAnnotations: Record;
- };
-
- delete attribute.group;
- // @ts-expect-error
- delete attribute.groupDisplayHeader;
- // @ts-expect-error
- delete attribute.groupDisplayDescription;
- // @ts-expect-error
- delete attribute.groupAnnotations;
-
- if (group === "") {
- break patch_legacy_group;
- }
-
- attribute.group = {
- name: group,
- displayHeader: groupDisplayHeader,
- displayDescription: groupDisplayDescription,
- annotations: groupAnnotations,
- html5DataAnnotations: {}
- };
- }
-
- // Attributes with options rendered by default as select inputs
- if (attribute.validators.options !== undefined && attribute.annotations.inputType === undefined) {
- attribute.annotations.inputType = "select";
- }
-
- // Consistency patch on values/value property
- {
- if (getIsMultivaluedSingleField({ attribute })) {
- attribute.multivalued = true;
- }
-
- if (attribute.multivalued) {
- attribute.values ??= attribute.value !== undefined ? [attribute.value] : [];
- delete attribute.value;
- } else {
- attribute.value ??= attribute.values?.[0];
- delete attribute.values;
- }
- }
- });
-
- add_password_and_password_confirm: {
- if (!kcContext.passwordRequired) {
- break add_password_and_password_confirm;
- }
-
- attributes.forEach((attribute, i) => {
- if (attribute.name !== (kcContext.realm.registrationEmailAsUsername ? "email" : "username")) {
- // NOTE: We want to add password and password-confirm after the field that identifies the user.
- // It's either email or username.
- return;
- }
-
- attributes.splice(
- i + 1,
- 0,
- {
- name: "password",
- displayName: id<`\${${MessageKey_defaultSet}}`>("${password}"),
- required: true,
- readOnly: false,
- validators: {},
- annotations: {},
- autocomplete: "new-password",
- html5DataAnnotations: {}
- },
- {
- name: "password-confirm",
- displayName: id<`\${${MessageKey_defaultSet}}`>("${passwordConfirm}"),
- required: true,
- readOnly: false,
- validators: {},
- annotations: {},
- html5DataAnnotations: {},
- autocomplete: "new-password"
- }
- );
- });
- }
-
- const initialFormFieldState: {
- attribute: Attribute;
- valueOrValues: string | string[];
- }[] = [];
-
- for (const attribute of attributes) {
- handle_multi_valued_attribute: {
- if (!attribute.multivalued) {
- break handle_multi_valued_attribute;
- }
-
- const values = attribute.values?.length ? attribute.values : [""];
-
- apply_validator_min_range: {
- if (getIsMultivaluedSingleField({ attribute })) {
- break apply_validator_min_range;
- }
-
- const validator = attribute.validators.multivalued;
-
- if (validator === undefined) {
- break apply_validator_min_range;
- }
-
- const { min: minStr } = validator;
-
- if (!minStr) {
- break apply_validator_min_range;
- }
-
- const min = parseInt(`${minStr}`);
-
- for (let index = values.length; index < min; index++) {
- values.push("");
- }
- }
-
- initialFormFieldState.push({
- attribute,
- valueOrValues: values
- });
-
- continue;
- }
-
- initialFormFieldState.push({
- attribute,
- valueOrValues: attribute.value ?? ""
- });
- }
-
- const initialState: internal.State = {
- formFieldStates: initialFormFieldState.map(({ attribute, valueOrValues }) => ({
- attribute,
- errors: getErrors({
- attributeName: attribute.name,
- formFieldStates: initialFormFieldState
- }),
- hasLostFocusAtLeastOnce:
- valueOrValues instanceof Array && !getIsMultivaluedSingleField({ attribute }) ? valueOrValues.map(() => false) : false,
- valueOrValues: valueOrValues
- }))
- };
-
- return initialState;
- }, []);
-
- const [state, dispatchFormAction] = useReducer(function reducer(state: internal.State, formAction: FormAction): internal.State {
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === formAction.name);
-
- assert(formFieldState !== undefined);
-
- (() => {
- switch (formAction.action) {
- case "update":
- formFieldState.valueOrValues = formAction.valueOrValues;
-
- apply_formatters: {
- const { attribute } = formFieldState;
-
- const { kcNumberFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberFormat) {
- break apply_formatters;
- }
-
- if (formFieldState.valueOrValues instanceof Array) {
- formFieldState.valueOrValues = formFieldState.valueOrValues.map(value => formatNumber(value, kcNumberFormat));
- } else {
- formFieldState.valueOrValues = formatNumber(formFieldState.valueOrValues, kcNumberFormat);
- }
- }
-
- formFieldState.errors = getErrors({
- attributeName: formAction.name,
- formFieldStates: state.formFieldStates
- });
-
- simulate_focus_lost: {
- const { displayErrorsImmediately = false } = formAction;
-
- if (!displayErrorsImmediately) {
- break simulate_focus_lost;
- }
-
- for (const fieldIndex of formAction.valueOrValues instanceof Array
- ? formAction.valueOrValues.map((...[, index]) => index)
- : [undefined]) {
- state = reducer(state, {
- action: "focus lost",
- name: formAction.name,
- fieldIndex
- });
- }
- }
-
- update_password_confirm: {
- if (doMakeUserConfirmPassword) {
- break update_password_confirm;
- }
-
- if (formAction.name !== "password") {
- break update_password_confirm;
- }
-
- state = reducer(state, {
- action: "update",
- name: "password-confirm",
- valueOrValues: formAction.valueOrValues,
- displayErrorsImmediately: formAction.displayErrorsImmediately
- });
- }
-
- trigger_password_confirm_validation_on_password_change: {
- if (!doMakeUserConfirmPassword) {
- break trigger_password_confirm_validation_on_password_change;
- }
-
- if (formAction.name !== "password") {
- break trigger_password_confirm_validation_on_password_change;
- }
-
- state = reducer(state, {
- action: "update",
- name: "password-confirm",
- valueOrValues: (() => {
- const formFieldState = state.formFieldStates.find(({ attribute }) => attribute.name === "password-confirm");
-
- assert(formFieldState !== undefined);
-
- return formFieldState.valueOrValues;
- })(),
- displayErrorsImmediately: formAction.displayErrorsImmediately
- });
- }
-
- return;
- case "focus lost":
- if (formFieldState.hasLostFocusAtLeastOnce instanceof Array) {
- const { fieldIndex } = formAction;
- assert(fieldIndex !== undefined);
- formFieldState.hasLostFocusAtLeastOnce[fieldIndex] = true;
- return;
- }
-
- formFieldState.hasLostFocusAtLeastOnce = true;
- return;
- }
- assert>(false);
- })();
-
- return { ...state };
- }, initialState);
-
- const formState: FormState = useMemo(
- () => ({
- formFieldStates: state.formFieldStates.map(
- ({ errors, hasLostFocusAtLeastOnce: hasLostFocusAtLeastOnceOrArr, attribute, ...valueOrValuesWrap }) => ({
- displayableErrors: errors.filter(error => {
- const hasLostFocusAtLeastOnce =
- typeof hasLostFocusAtLeastOnceOrArr === "boolean"
- ? hasLostFocusAtLeastOnceOrArr
- : error.fieldIndex !== undefined
- ? hasLostFocusAtLeastOnceOrArr[error.fieldIndex]
- : hasLostFocusAtLeastOnceOrArr[hasLostFocusAtLeastOnceOrArr.length - 1];
-
- switch (error.source.type) {
- case "server":
- return true;
- case "other":
- switch (error.source.rule) {
- case "requiredField":
- return hasLostFocusAtLeastOnce;
- case "passwordConfirmMatchesPassword":
- return hasLostFocusAtLeastOnce;
- }
- assert>(false);
- case "passwordPolicy":
- switch (error.source.name) {
- case "length":
- return hasLostFocusAtLeastOnce;
- case "digits":
- return hasLostFocusAtLeastOnce;
- case "lowerCase":
- return hasLostFocusAtLeastOnce;
- case "upperCase":
- return hasLostFocusAtLeastOnce;
- case "specialChars":
- return hasLostFocusAtLeastOnce;
- case "notUsername":
- return true;
- case "notEmail":
- return true;
- }
- assert>(false);
- case "validator":
- switch (error.source.name) {
- case "length":
- return hasLostFocusAtLeastOnce;
- case "pattern":
- return hasLostFocusAtLeastOnce;
- case "email":
- return hasLostFocusAtLeastOnce;
- case "integer":
- return hasLostFocusAtLeastOnce;
- case "multivalued":
- return hasLostFocusAtLeastOnce;
- case "options":
- return hasLostFocusAtLeastOnce;
- }
- assert>(false);
- }
- }),
- attribute,
- ...valueOrValuesWrap
- })
- ),
- isFormSubmittable: state.formFieldStates.every(({ errors }) => errors.length === 0)
- }),
- [state]
- );
-
- return {
- formState,
- dispatchFormAction
- };
-}
-
-type KcContextLike_useGetErrors = {
- messagesPerField: Pick;
- passwordPolicies?: PasswordPolicies;
-};
-
-assert();
-
-function useGetErrors(params: { kcContext: KcContextLike_useGetErrors; i18n: I18n }) {
- const { kcContext, i18n } = params;
-
- const { messagesPerField, passwordPolicies } = kcContext;
-
- const { msg, msgStr, advancedMsg, advancedMsgStr } = i18n;
-
- const getErrors = useConstCallback(
- (params: {
- attributeName: string;
- formFieldStates: {
- attribute: Attribute;
- valueOrValues: string | string[];
- }[];
- }): FormFieldError[] => {
- const { attributeName, formFieldStates } = params;
-
- const formFieldState = formFieldStates.find(({ attribute }) => attribute.name === attributeName);
-
- assert(formFieldState !== undefined);
-
- const { attribute } = formFieldState;
-
- const valueOrValues = (() => {
- let { valueOrValues } = formFieldState;
-
- unFormat_number: {
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberUnFormat) {
- break unFormat_number;
- }
-
- if (valueOrValues instanceof Array) {
- valueOrValues = valueOrValues.map(value => formatNumber(value, kcNumberUnFormat));
- } else {
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
- }
- }
-
- return valueOrValues;
- })();
-
- assert(attribute !== undefined);
-
- server_side_error: {
- if (attribute.multivalued) {
- const defaultValues = attribute.values?.length ? attribute.values : [""];
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- if (JSON.stringify(defaultValues) !== JSON.stringify(values.slice(0, defaultValues.length))) {
- break server_side_error;
- }
- } else {
- const defaultValue = attribute.value ?? "";
-
- assert(typeof valueOrValues === "string");
-
- const value = valueOrValues;
-
- if (defaultValue !== value) {
- break server_side_error;
- }
- }
-
- let doesErrorExist: boolean;
-
- try {
- doesErrorExist = messagesPerField.existsError(attributeName);
- } catch {
- break server_side_error;
- }
-
- if (!doesErrorExist) {
- break server_side_error;
- }
-
- const errorMessageStr = messagesPerField.get(attributeName);
-
- return [
- {
- errorMessageStr,
- errorMessage: (
-
- ),
- fieldIndex: undefined,
- source: {
- type: "server"
- }
- }
- ];
- }
-
- handle_multi_valued_multi_fields: {
- if (!attribute.multivalued) {
- break handle_multi_valued_multi_fields;
- }
-
- if (getIsMultivaluedSingleField({ attribute })) {
- break handle_multi_valued_multi_fields;
- }
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- const errors = values
- .map((...[, index]) => {
- const specificValueErrors = getErrors({
- attributeName,
- formFieldStates: formFieldStates.map(formFieldState => {
- if (formFieldState.attribute.name === attributeName) {
- assert(formFieldState.valueOrValues instanceof Array);
- return {
- attribute: {
- ...attribute,
- annotations: {
- ...attribute.annotations,
- inputType: undefined
- },
- multivalued: false
- },
- valueOrValues: formFieldState.valueOrValues[index]
- };
- }
-
- return formFieldState;
- })
- });
-
- return specificValueErrors
- .filter(error => {
- if (error.source.type === "other" && error.source.rule === "requiredField") {
- return false;
- }
-
- return true;
- })
- .map(
- (error): FormFieldError => ({
- ...error,
- fieldIndex: index
- })
- );
- })
- .reduce((acc, errors) => [...acc, ...errors], []);
-
- required_field: {
- if (!attribute.required) {
- break required_field;
- }
-
- if (values.every(value => value !== "")) {
- break required_field;
- }
-
- const msgArgs = ["error-user-attribute-required"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "requiredField"
- }
- });
- }
-
- return errors;
- }
-
- handle_multi_valued_single_field: {
- if (!attribute.multivalued) {
- break handle_multi_valued_single_field;
- }
-
- if (!getIsMultivaluedSingleField({ attribute })) {
- break handle_multi_valued_single_field;
- }
-
- const validatorName = "multivalued";
-
- const validator = attribute.validators[validatorName];
-
- if (validator === undefined) {
- return [];
- }
-
- const { min: minStr } = validator;
-
- const min = minStr ? parseInt(`${minStr}`) : attribute.required ? 1 : 0;
-
- assert(!isNaN(min));
-
- const { max: maxStr } = validator;
-
- const max = !maxStr ? Infinity : parseInt(`${maxStr}`);
-
- assert(!isNaN(max));
-
- assert(valueOrValues instanceof Array);
-
- const values = valueOrValues;
-
- if (min <= values.length && values.length <= max) {
- return [];
- }
-
- const msgArgs = ["error-invalid-multivalued-size", `${min}`, `${max}`] as const;
-
- return [
- {
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- }
- ];
- }
-
- assert(typeof valueOrValues === "string");
-
- const value = valueOrValues;
-
- const errors: FormFieldError[] = [];
-
- check_password_policies: {
- if (attributeName !== "password") {
- break check_password_policies;
- }
-
- if (passwordPolicies === undefined) {
- break check_password_policies;
- }
-
- check_password_policy_x: {
- const policyName = "length";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minLength = policy;
-
- if (value.length >= minLength) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinLengthMessage", `${minLength}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "digits";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfDigits = policy;
-
- if (value.split("").filter(char => !isNaN(parseInt(char))).length >= minNumberOfDigits) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinDigitsMessage", `${minNumberOfDigits}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "lowerCase";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfLowerCaseChar = policy;
-
- if (
- value.split("").filter(char => char === char.toLowerCase() && char !== char.toUpperCase()).length >= minNumberOfLowerCaseChar
- ) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinLowerCaseCharsMessage", `${minNumberOfLowerCaseChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "upperCase";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfUpperCaseChar = policy;
-
- if (
- value.split("").filter(char => char === char.toUpperCase() && char !== char.toLowerCase()).length >= minNumberOfUpperCaseChar
- ) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinUpperCaseCharsMessage", `${minNumberOfUpperCaseChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "specialChars";
-
- const policy = passwordPolicies[policyName];
-
- if (!policy) {
- break check_password_policy_x;
- }
-
- const minNumberOfSpecialChar = policy;
-
- if (value.split("").filter(char => !char.match(/[a-zA-Z0-9]/)).length >= minNumberOfSpecialChar) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordMinSpecialCharsMessage", `${minNumberOfSpecialChar}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "notUsername";
-
- const notUsername = passwordPolicies[policyName];
-
- if (!notUsername) {
- break check_password_policy_x;
- }
-
- const usernameFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "username");
-
- if (!usernameFormFieldState) {
- break check_password_policy_x;
- }
-
- const usernameValue = (() => {
- let { valueOrValues } = usernameFormFieldState;
-
- assert(typeof valueOrValues === "string");
-
- unFormat_number: {
- const { kcNumberUnFormat } = attribute.html5DataAnnotations ?? {};
-
- if (!kcNumberUnFormat) {
- break unFormat_number;
- }
-
- valueOrValues = formatNumber(valueOrValues, kcNumberUnFormat);
- }
-
- return valueOrValues;
- })();
-
- if (usernameValue === "") {
- break check_password_policy_x;
- }
-
- if (value !== usernameValue) {
- break check_password_policy_x;
- }
-
- const msgArgs = ["invalidPasswordNotUsernameMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
-
- check_password_policy_x: {
- const policyName = "notEmail";
-
- const notEmail = passwordPolicies[policyName];
-
- if (!notEmail) {
- break check_password_policy_x;
- }
-
- const emailFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "email");
-
- if (!emailFormFieldState) {
- break check_password_policy_x;
- }
-
- assert(typeof emailFormFieldState.valueOrValues === "string");
-
- {
- const emailValue = emailFormFieldState.valueOrValues;
-
- if (emailValue === "") {
- break check_password_policy_x;
- }
-
- if (value !== emailValue) {
- break check_password_policy_x;
- }
- }
-
- const msgArgs = ["invalidPasswordNotEmailMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "passwordPolicy",
- name: policyName
- }
- });
- }
- }
-
- password_confirm_matches_password: {
- if (attributeName !== "password-confirm") {
- break password_confirm_matches_password;
- }
-
- const passwordFormFieldState = formFieldStates.find(formFieldState => formFieldState.attribute.name === "password");
-
- assert(passwordFormFieldState !== undefined);
-
- assert(typeof passwordFormFieldState.valueOrValues === "string");
-
- {
- const passwordValue = passwordFormFieldState.valueOrValues;
-
- if (value === passwordValue) {
- break password_confirm_matches_password;
- }
- }
-
- const msgArgs = ["invalidPasswordConfirmMessage"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "passwordConfirmMatchesPassword"
- }
- });
- }
-
- const { validators } = attribute;
-
- required_field: {
- if (!attribute.required) {
- break required_field;
- }
-
- if (value !== "") {
- break required_field;
- }
-
- const msgArgs = ["error-user-attribute-required"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "other",
- rule: "requiredField"
- }
- });
- }
-
- validator_x: {
- const validatorName = "length";
-
- const validator = validators[validatorName];
-
- if (!validator) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- const source: FormFieldError.Source = {
- type: "validator",
- name: validatorName
- };
-
- if (max && value.length > parseInt(`${max}`)) {
- const msgArgs = ["error-invalid-length-too-long", `${max}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
- }
-
- if (min && value.length < parseInt(`${min}`)) {
- const msgArgs = ["error-invalid-length-too-short", `${min}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
- }
- }
-
- validator_x: {
- const validatorName = "pattern";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, pattern, "error-message": errorMessageKey } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- if (new RegExp(pattern).test(value)) {
- break validator_x;
- }
-
- const msgArgs = [errorMessageKey ?? id("shouldMatchPattern"), pattern] as const;
-
- errors.push({
- errorMessage: {advancedMsg(...msgArgs)},
- errorMessageStr: advancedMsgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- validator_x: {
- {
- const lastError = errors[errors.length - 1];
- if (lastError !== undefined && lastError.source.type === "validator" && lastError.source.name === "pattern") {
- break validator_x;
- }
- }
-
- const validatorName = "email";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- if (emailRegexp.test(value)) {
- break validator_x;
- }
-
- const msgArgs = [id("invalidEmailMessage")] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- validator_x: {
- const validatorName = "integer";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- const { "ignore.empty.value": ignoreEmptyValue = false, max, min } = validator;
-
- if (ignoreEmptyValue && value === "") {
- break validator_x;
- }
-
- const intValue = parseInt(value);
-
- const source: FormFieldError.Source = {
- type: "validator",
- name: validatorName
- };
-
- if (isNaN(intValue)) {
- const msgArgs = ["mustBeAnInteger"] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
-
- if (max && intValue > parseInt(`${max}`)) {
- const msgArgs = ["error-number-out-of-range-too-big", `${max}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
-
- if (min && intValue < parseInt(`${min}`)) {
- const msgArgs = ["error-number-out-of-range-too-small", `${min}`] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source
- });
-
- break validator_x;
- }
- }
-
- validator_x: {
- const validatorName = "options";
-
- const validator = validators[validatorName];
-
- if (validator === undefined) {
- break validator_x;
- }
-
- if (value === "") {
- break validator_x;
- }
-
- if (validator.options.indexOf(value) >= 0) {
- break validator_x;
- }
-
- const msgArgs = [id("notAValidOption")] as const;
-
- errors.push({
- errorMessage: {msg(...msgArgs)},
- errorMessageStr: msgStr(...msgArgs),
- fieldIndex: undefined,
- source: {
- type: "validator",
- name: validatorName
- }
- });
- }
-
- //TODO: Implement missing validators. See Validators type definition.
-
- return errors;
- }
- );
-
- return { getErrors };
-}
-
-function getIsMultivaluedSingleField(params: { attribute: Attribute }) {
- const { attribute } = params;
-
- return attribute.annotations.inputType?.startsWith("multiselect") ?? false;
-}
-
-export function getButtonToDisplayForMultivaluedAttributeField(params: { attribute: Attribute; values: string[]; fieldIndex: number }) {
- const { attribute, values, fieldIndex } = params;
-
- const hasRemove = (() => {
- if (values.length === 1) {
- return false;
- }
-
- const minCount = (() => {
- const { multivalued } = attribute.validators;
-
- if (multivalued === undefined) {
- return undefined;
- }
-
- const minStr = multivalued.min;
-
- if (minStr === undefined) {
- return undefined;
- }
-
- return parseInt(`${minStr}`);
- })();
-
- if (minCount === undefined) {
- return true;
- }
-
- if (values.length === minCount) {
- return false;
- }
-
- return true;
- })();
-
- const hasAdd = (() => {
- if (fieldIndex + 1 !== values.length) {
- return false;
- }
-
- const maxCount = (() => {
- const { multivalued } = attribute.validators;
-
- if (multivalued === undefined) {
- return undefined;
- }
-
- const maxStr = multivalued.max;
-
- if (maxStr === undefined) {
- return undefined;
- }
-
- return parseInt(`${maxStr}`);
- })();
-
- if (maxCount === undefined) {
- return true;
- }
-
- return values.length !== maxCount;
- })();
-
- return { hasRemove, hasAdd };
-}
diff --git a/src/login/lib/useUserProfileForm.tsx b/src/login/lib/useUserProfileForm.tsx
index 3d997549..c01eafea 100644
--- a/src/login/lib/useUserProfileForm.tsx
+++ b/src/login/lib/useUserProfileForm.tsx
@@ -1,8 +1,9 @@
-import * as reactlessApi from "./getUserProfileApi";
+import * as reactlessApi from "./getUserProfileApi/index";
import type { PasswordPolicies, Attribute, Validators } from "keycloakify/login/KcContext";
import { useEffect, useState, useMemo, Fragment } from "react";
import { assert, type Equals } from "tsafe/assert";
import type { I18n } from "../i18n";
+export { getButtonToDisplayForMultivaluedAttributeField } from "./getUserProfileApi/index";
export type FormFieldError = {
errorMessage: JSX.Element;
From 06e33196bb12d5a405e8b9bd0927dbac5ae9fd07 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 00:31:27 +0200
Subject: [PATCH 03/82] Refactor: Make ClassKey importable without having react
as a dependency
---
src/account/TemplateProps.ts | 16 +----
src/account/lib/kcClsx.ts | 18 ++++-
src/login/TemplateProps.ts | 127 +---------------------------------
src/login/lib/kcClsx.ts | 129 ++++++++++++++++++++++++++++++++++-
4 files changed, 145 insertions(+), 145 deletions(-)
diff --git a/src/account/TemplateProps.ts b/src/account/TemplateProps.ts
index 0e5196d2..d8f26cbd 100644
--- a/src/account/TemplateProps.ts
+++ b/src/account/TemplateProps.ts
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
+import type { ClassKey } from "keycloakify/account/lib/kcClsx";
export type TemplateProps = {
kcContext: KcContext;
@@ -10,17 +11,4 @@ export type TemplateProps = {
active: string;
};
-export type ClassKey =
- | "kcHtmlClass"
- | "kcBodyClass"
- | "kcButtonClass"
- | "kcButtonPrimaryClass"
- | "kcButtonLargeClass"
- | "kcButtonDefaultClass"
- | "kcContentWrapperClass"
- | "kcFormClass"
- | "kcFormGroupClass"
- | "kcInputWrapperClass"
- | "kcLabelClass"
- | "kcInputClass"
- | "kcInputErrorMessageClass";
+export type { ClassKey };
diff --git a/src/account/lib/kcClsx.ts b/src/account/lib/kcClsx.ts
index 330e0c4f..33df4515 100644
--- a/src/account/lib/kcClsx.ts
+++ b/src/account/lib/kcClsx.ts
@@ -1,5 +1,19 @@
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
-import type { ClassKey } from "keycloakify/account/TemplateProps";
+
+export type ClassKey =
+ | "kcHtmlClass"
+ | "kcBodyClass"
+ | "kcButtonClass"
+ | "kcButtonPrimaryClass"
+ | "kcButtonLargeClass"
+ | "kcButtonDefaultClass"
+ | "kcContentWrapperClass"
+ | "kcFormClass"
+ | "kcFormGroupClass"
+ | "kcInputWrapperClass"
+ | "kcLabelClass"
+ | "kcInputClass"
+ | "kcInputErrorMessageClass";
export const { getKcClsx } = createGetKcClsx({
defaultClasses: {
@@ -20,6 +34,4 @@ export const { getKcClsx } = createGetKcClsx({
}
});
-export type { ClassKey };
-
export type KcClsx = ReturnType["kcClsx"];
diff --git a/src/login/TemplateProps.ts b/src/login/TemplateProps.ts
index 0e3b2895..b3b8f7c6 100644
--- a/src/login/TemplateProps.ts
+++ b/src/login/TemplateProps.ts
@@ -1,4 +1,5 @@
import type { ReactNode } from "react";
+import type { ClassKey } from "keycloakify/login/lib/kcClsx";
export type TemplateProps = {
kcContext: KcContext;
@@ -18,128 +19,4 @@ export type TemplateProps = {
bodyClassName?: string;
};
-export type ClassKey =
- | "kcBodyClass"
- | "kcHeaderWrapperClass"
- | "kcLocaleWrapperClass"
- | "kcInfoAreaWrapperClass"
- | "kcFormButtonsWrapperClass"
- | "kcFormOptionsWrapperClass"
- | "kcCheckboxInputClass"
- | "kcLocaleDropDownClass"
- | "kcLocaleListItemClass"
- | "kcContentWrapperClass"
- | "kcLogoIdP-facebook"
- | "kcAuthenticatorOTPClass"
- | "kcLogoIdP-bitbucket"
- | "kcAuthenticatorWebAuthnClass"
- | "kcWebAuthnDefaultIcon"
- | "kcLogoIdP-stackoverflow"
- | "kcSelectAuthListItemClass"
- | "kcLogoIdP-microsoft"
- | "kcLoginOTPListItemHeaderClass"
- | "kcLocaleItemClass"
- | "kcLoginOTPListItemIconBodyClass"
- | "kcInputHelperTextAfterClass"
- | "kcFormClass"
- | "kcSelectAuthListClass"
- | "kcInputClassRadioCheckboxLabelDisabled"
- | "kcSelectAuthListItemIconClass"
- | "kcRecoveryCodesWarning"
- | "kcFormSettingClass"
- | "kcWebAuthnBLE"
- | "kcInputWrapperClass"
- | "kcSelectAuthListItemArrowIconClass"
- | "kcFeedbackAreaClass"
- | "kcFormPasswordVisibilityButtonClass"
- | "kcLogoIdP-google"
- | "kcCheckLabelClass"
- | "kcSelectAuthListItemFillClass"
- | "kcAuthenticatorDefaultClass"
- | "kcLogoIdP-gitlab"
- | "kcFormAreaClass"
- | "kcFormButtonsClass"
- | "kcInputClassRadioLabel"
- | "kcAuthenticatorWebAuthnPasswordlessClass"
- | "kcSelectAuthListItemHeadingClass"
- | "kcInfoAreaClass"
- | "kcLogoLink"
- | "kcContainerClass"
- | "kcSelectAuthListItemTitle"
- | "kcHtmlClass"
- | "kcLoginOTPListItemTitleClass"
- | "kcLogoIdP-openshift-v4"
- | "kcWebAuthnUnknownIcon"
- | "kcFormSocialAccountNameClass"
- | "kcLogoIdP-openshift-v3"
- | "kcLoginOTPListInputClass"
- | "kcWebAuthnUSB"
- | "kcInputClassRadio"
- | "kcWebAuthnKeyIcon"
- | "kcFeedbackInfoIcon"
- | "kcCommonLogoIdP"
- | "kcRecoveryCodesActions"
- | "kcFormGroupHeader"
- | "kcFormSocialAccountSectionClass"
- | "kcLogoIdP-instagram"
- | "kcAlertClass"
- | "kcHeaderClass"
- | "kcLabelWrapperClass"
- | "kcFormPasswordVisibilityIconShow"
- | "kcFormSocialAccountLinkClass"
- | "kcLocaleMainClass"
- | "kcInputGroup"
- | "kcTextareaClass"
- | "kcButtonBlockClass"
- | "kcButtonClass"
- | "kcWebAuthnNFC"
- | "kcLocaleClass"
- | "kcInputClassCheckboxInput"
- | "kcFeedbackErrorIcon"
- | "kcInputLargeClass"
- | "kcInputErrorMessageClass"
- | "kcRecoveryCodesList"
- | "kcFormSocialAccountListClass"
- | "kcAlertTitleClass"
- | "kcAuthenticatorPasswordClass"
- | "kcCheckInputClass"
- | "kcLogoIdP-linkedin"
- | "kcLogoIdP-twitter"
- | "kcFeedbackWarningIcon"
- | "kcResetFlowIcon"
- | "kcSelectAuthListItemIconPropertyClass"
- | "kcFeedbackSuccessIcon"
- | "kcLoginOTPListClass"
- | "kcSrOnlyClass"
- | "kcFormSocialAccountListGridClass"
- | "kcButtonDefaultClass"
- | "kcFormGroupErrorClass"
- | "kcSelectAuthListItemDescriptionClass"
- | "kcSelectAuthListItemBodyClass"
- | "kcWebAuthnInternal"
- | "kcSelectAuthListItemArrowClass"
- | "kcCheckClass"
- | "kcContentClass"
- | "kcLogoClass"
- | "kcLoginOTPListItemIconClass"
- | "kcLoginClass"
- | "kcSignUpClass"
- | "kcButtonLargeClass"
- | "kcFormCardClass"
- | "kcLocaleListClass"
- | "kcInputClass"
- | "kcFormGroupClass"
- | "kcLogoIdP-paypal"
- | "kcInputClassCheckbox"
- | "kcRecoveryCodesConfirmation"
- | "kcFormPasswordVisibilityIconHide"
- | "kcInputClassRadioInput"
- | "kcFormSocialAccountListButtonClass"
- | "kcInputClassCheckboxLabel"
- | "kcFormOptionsClass"
- | "kcFormHeaderClass"
- | "kcFormSocialAccountGridItem"
- | "kcButtonPrimaryClass"
- | "kcInputHelperTextBeforeClass"
- | "kcLogoIdP-github"
- | "kcLabelClass";
+export type { ClassKey };
diff --git a/src/login/lib/kcClsx.ts b/src/login/lib/kcClsx.ts
index a140671f..9198f778 100644
--- a/src/login/lib/kcClsx.ts
+++ b/src/login/lib/kcClsx.ts
@@ -1,5 +1,130 @@
import { createGetKcClsx } from "keycloakify/lib/getKcClsx";
-import type { ClassKey } from "keycloakify/login/TemplateProps";
+
+export type ClassKey =
+ | "kcBodyClass"
+ | "kcHeaderWrapperClass"
+ | "kcLocaleWrapperClass"
+ | "kcInfoAreaWrapperClass"
+ | "kcFormButtonsWrapperClass"
+ | "kcFormOptionsWrapperClass"
+ | "kcCheckboxInputClass"
+ | "kcLocaleDropDownClass"
+ | "kcLocaleListItemClass"
+ | "kcContentWrapperClass"
+ | "kcLogoIdP-facebook"
+ | "kcAuthenticatorOTPClass"
+ | "kcLogoIdP-bitbucket"
+ | "kcAuthenticatorWebAuthnClass"
+ | "kcWebAuthnDefaultIcon"
+ | "kcLogoIdP-stackoverflow"
+ | "kcSelectAuthListItemClass"
+ | "kcLogoIdP-microsoft"
+ | "kcLoginOTPListItemHeaderClass"
+ | "kcLocaleItemClass"
+ | "kcLoginOTPListItemIconBodyClass"
+ | "kcInputHelperTextAfterClass"
+ | "kcFormClass"
+ | "kcSelectAuthListClass"
+ | "kcInputClassRadioCheckboxLabelDisabled"
+ | "kcSelectAuthListItemIconClass"
+ | "kcRecoveryCodesWarning"
+ | "kcFormSettingClass"
+ | "kcWebAuthnBLE"
+ | "kcInputWrapperClass"
+ | "kcSelectAuthListItemArrowIconClass"
+ | "kcFeedbackAreaClass"
+ | "kcFormPasswordVisibilityButtonClass"
+ | "kcLogoIdP-google"
+ | "kcCheckLabelClass"
+ | "kcSelectAuthListItemFillClass"
+ | "kcAuthenticatorDefaultClass"
+ | "kcLogoIdP-gitlab"
+ | "kcFormAreaClass"
+ | "kcFormButtonsClass"
+ | "kcInputClassRadioLabel"
+ | "kcAuthenticatorWebAuthnPasswordlessClass"
+ | "kcSelectAuthListItemHeadingClass"
+ | "kcInfoAreaClass"
+ | "kcLogoLink"
+ | "kcContainerClass"
+ | "kcSelectAuthListItemTitle"
+ | "kcHtmlClass"
+ | "kcLoginOTPListItemTitleClass"
+ | "kcLogoIdP-openshift-v4"
+ | "kcWebAuthnUnknownIcon"
+ | "kcFormSocialAccountNameClass"
+ | "kcLogoIdP-openshift-v3"
+ | "kcLoginOTPListInputClass"
+ | "kcWebAuthnUSB"
+ | "kcInputClassRadio"
+ | "kcWebAuthnKeyIcon"
+ | "kcFeedbackInfoIcon"
+ | "kcCommonLogoIdP"
+ | "kcRecoveryCodesActions"
+ | "kcFormGroupHeader"
+ | "kcFormSocialAccountSectionClass"
+ | "kcLogoIdP-instagram"
+ | "kcAlertClass"
+ | "kcHeaderClass"
+ | "kcLabelWrapperClass"
+ | "kcFormPasswordVisibilityIconShow"
+ | "kcFormSocialAccountLinkClass"
+ | "kcLocaleMainClass"
+ | "kcInputGroup"
+ | "kcTextareaClass"
+ | "kcButtonBlockClass"
+ | "kcButtonClass"
+ | "kcWebAuthnNFC"
+ | "kcLocaleClass"
+ | "kcInputClassCheckboxInput"
+ | "kcFeedbackErrorIcon"
+ | "kcInputLargeClass"
+ | "kcInputErrorMessageClass"
+ | "kcRecoveryCodesList"
+ | "kcFormSocialAccountListClass"
+ | "kcAlertTitleClass"
+ | "kcAuthenticatorPasswordClass"
+ | "kcCheckInputClass"
+ | "kcLogoIdP-linkedin"
+ | "kcLogoIdP-twitter"
+ | "kcFeedbackWarningIcon"
+ | "kcResetFlowIcon"
+ | "kcSelectAuthListItemIconPropertyClass"
+ | "kcFeedbackSuccessIcon"
+ | "kcLoginOTPListClass"
+ | "kcSrOnlyClass"
+ | "kcFormSocialAccountListGridClass"
+ | "kcButtonDefaultClass"
+ | "kcFormGroupErrorClass"
+ | "kcSelectAuthListItemDescriptionClass"
+ | "kcSelectAuthListItemBodyClass"
+ | "kcWebAuthnInternal"
+ | "kcSelectAuthListItemArrowClass"
+ | "kcCheckClass"
+ | "kcContentClass"
+ | "kcLogoClass"
+ | "kcLoginOTPListItemIconClass"
+ | "kcLoginClass"
+ | "kcSignUpClass"
+ | "kcButtonLargeClass"
+ | "kcFormCardClass"
+ | "kcLocaleListClass"
+ | "kcInputClass"
+ | "kcFormGroupClass"
+ | "kcLogoIdP-paypal"
+ | "kcInputClassCheckbox"
+ | "kcRecoveryCodesConfirmation"
+ | "kcFormPasswordVisibilityIconHide"
+ | "kcInputClassRadioInput"
+ | "kcFormSocialAccountListButtonClass"
+ | "kcInputClassCheckboxLabel"
+ | "kcFormOptionsClass"
+ | "kcFormHeaderClass"
+ | "kcFormSocialAccountGridItem"
+ | "kcButtonPrimaryClass"
+ | "kcInputHelperTextBeforeClass"
+ | "kcLogoIdP-github"
+ | "kcLabelClass";
export const { getKcClsx } = createGetKcClsx({
defaultClasses: {
@@ -138,6 +263,4 @@ export const { getKcClsx } = createGetKcClsx({
}
});
-export type { ClassKey };
-
export type KcClsx = ReturnType["kcClsx"];
From 9af542ec89ef945bd561c52d7b515cf7c8de4803 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 01:10:45 +0200
Subject: [PATCH 04/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 42f474a3..f28b02d8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.0",
+ "version": "11.2.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 835833a61bafe8d8919c13c8df49b7a11c8879e0 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 01:22:37 +0200
Subject: [PATCH 05/82] Remove unessesary reference to react specific construct
in KcContext
---
src/login/KcContext/KcContext.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts
index 4b9c91e6..14d1169f 100644
--- a/src/login/KcContext/KcContext.ts
+++ b/src/login/KcContext/KcContext.ts
@@ -2,7 +2,7 @@ import type { ThemeType, LoginThemePageId } from "keycloakify/bin/shared/constan
import type { ValueOf } from "keycloakify/tools/ValueOf";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";
-import type { ClassKey } from "keycloakify/login/TemplateProps";
+import type { ClassKey } from "keycloakify/login/lib/kcClsx";
export type ExtendKcContext<
KcContextExtension extends { properties?: Record },
From c84dc281a20f6d23595bc940b6fe574c9103e524 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 01:22:49 +0200
Subject: [PATCH 06/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index f28b02d8..ef9b65d9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.1",
+ "version": "11.2.2",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From ab0c281d9818bd2701e0967b24f7fbf640ecf3dd Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 11:48:57 +0200
Subject: [PATCH 07/82] Fix allegated vulnerability
---
.../keycloakify/generateFtl/kcContextDeclarationTemplate.ftl | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
index 6b74955f..5cfc2755 100644
--- a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
+++ b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
@@ -166,7 +166,7 @@ function decodeHtmlEntities(htmlStr){
areSamePath(path, []) &&
["login-idp-link-confirm.ftl", "login-idp-link-email.ftl" ]?seq_contains(xKeycloakify.pageId)
) || (
- ["masterAdminClient", "delegateForUpdate", "defaultRole"]?seq_contains(key) &&
+ ["masterAdminClient", "delegateForUpdate", "defaultRole", "smtpConfig"]?seq_contains(key) &&
areSamePath(path, ["realm"])
) || (
xKeycloakify.pageId == "error.ftl" &&
From 613167f3a690c77883dbf6a7b9f1dfeca928c493 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 11:49:33 +0200
Subject: [PATCH 08/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index ef9b65d9..880782d1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.2",
+ "version": "11.2.3",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From d6436a58a2cc381a9cd17e4f39a6a16806a816f6 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 17:57:41 +0200
Subject: [PATCH 09/82] update ci
---
.github/workflows/ci.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 9fca0a23..6ec87504 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -112,7 +112,7 @@ jobs:
registry-url: https://registry.npmjs.org/
- uses: bahmutov/npm-install@v1
- run: npm run build
- - run: npx -y -p denoify@1.6.12 enable_short_npm_import_path
+ - run: npx -y -p denoify@1.6.13 enable_short_npm_import_path
env:
DRY_RUN: "0"
- uses: garronej/ts-ci@v2.1.2
From 9c44d13f73b147a01a87696b77a7d3a3bec42e87 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 18:10:09 +0200
Subject: [PATCH 10/82] Update tsafe (provide ESM distribution)
---
package.json | 2 +-
yarn.lock | 5 +++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 880782d1..4b2ad41f 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
],
"homepage": "https://www.keycloakify.dev",
"dependencies": {
- "tsafe": "^1.6.6"
+ "tsafe": "^1.7.4"
},
"devDependencies": {
"@babel/core": "^7.24.5",
diff --git a/yarn.lock b/yarn.lock
index abab9a2c..d05e77af 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12579,6 +12579,11 @@ tsafe@^1.6.6:
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.6.tgz#fd93e64d6eb13ef83ed1650669cc24bad4f5df9f"
integrity sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==
+tsafe@^1.7.4:
+ version "1.7.4"
+ resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.7.4.tgz#7dd288b1a1be8d9c25e84ab8dd8a7df6094168d7"
+ integrity sha512-4BrLklZMJ14dEtA+CkhY9OtID3al4+/GJhaeocWPtUuoZPr4SJkaqoPemyFgkLC1Y3LRNXF9zxa94SwssRGMaQ==
+
tsc-alias@^1.8.10:
version "1.8.10"
resolved "https://registry.yarnpkg.com/tsc-alias/-/tsc-alias-1.8.10.tgz#279f9bf0dd8bc10fb27820393d4881db5a303938"
From 2fd04cfb6165c89feb8019aa7baaf50a63669cb3 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 30 Sep 2024 18:10:26 +0200
Subject: [PATCH 11/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 4b2ad41f..2aac84f8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.3",
+ "version": "11.2.4",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 7203c742bef82ea1a120f5a8fb22db56dbbf6c58 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Tue, 1 Oct 2024 11:52:40 +0200
Subject: [PATCH 12/82] Avoid modifying BASE_URL for App context
---
src/vite-plugin/vite-plugin.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts
index 1ccfaf89..f22cb62a 100644
--- a/src/vite-plugin/vite-plugin.ts
+++ b/src/vite-plugin/vite-plugin.ts
@@ -166,7 +166,7 @@ export function keycloakify(params: keycloakify.Params) {
[
`(`,
`(window.kcContext === undefined || import.meta.env.MODE === "development")?`,
- `"${urlPathname ?? "/"}":`,
+ `import.meta.env.BASE_URL:`,
`(window.kcContext["x-keycloakify"].resourcesPath + "/${WELL_KNOWN_DIRECTORY_BASE_NAME.DIST}/")`,
`)`
].join("")
From 810dc6ceb5877cd9d1a08f1f17954caf923439cc Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Tue, 1 Oct 2024 11:59:39 +0200
Subject: [PATCH 13/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 2aac84f8..760e7453 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.4",
+ "version": "11.2.5",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From a0e3dc163a587e856762b983e11c8de8304ea0cc Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 2 Oct 2024 08:59:48 +0000
Subject: [PATCH 14/82] docs: update README.md [skip ci]
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index ddc36c80..82e44eba 100644
--- a/README.md
+++ b/README.md
@@ -134,6 +134,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Omid ⚠️ 💻 |
+  Katharina Eiserfey 💻 ⚠️ 📖 |
From 1f2a755a97645ab8c2e935b235f01926334c0547 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 2 Oct 2024 08:59:49 +0000
Subject: [PATCH 15/82] docs: update .all-contributorsrc [skip ci]
---
.all-contributorsrc | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 864f118b..b6496bfb 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -269,6 +269,17 @@
"test",
"code"
]
+ },
+ {
+ "login": "kathari00",
+ "name": "Katharina Eiserfey",
+ "avatar_url": "https://avatars.githubusercontent.com/u/42547712?v=4",
+ "profile": "https://github.com/kathari00",
+ "contributions": [
+ "code",
+ "test",
+ "doc"
+ ]
}
],
"contributorsPerLine": 7,
From 6c4dc711d21689ddbdb8ea531c49806c85d76226 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 2 Oct 2024 11:02:25 +0200
Subject: [PATCH 16/82] Put Kathi as first contributor
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 82e44eba..50c221ce 100644
--- a/README.md
+++ b/README.md
@@ -97,13 +97,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
From fa934da4429483607b13384c7f0cbbbdf94b49d6 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 2 Oct 2024 09:05:34 +0000
Subject: [PATCH 17/82] docs: update README.md [skip ci]
---
README.md | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 50c221ce..78872df3 100644
--- a/README.md
+++ b/README.md
@@ -97,13 +97,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
From 87198f6e568aaa85e61319a99aeefaf3b7648432 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Wed, 2 Oct 2024 09:05:35 +0000
Subject: [PATCH 18/82] docs: update .all-contributorsrc [skip ci]
---
.all-contributorsrc | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index b6496bfb..fdede216 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -280,6 +280,16 @@
"test",
"doc"
]
+ },
+ {
+ "login": "luca-peruzzo",
+ "name": "Luca Peruzzo",
+ "avatar_url": "https://avatars.githubusercontent.com/u/69015314?v=4",
+ "profile": "https://github.com/luca-peruzzo",
+ "contributions": [
+ "code",
+ "test"
+ ]
}
],
"contributorsPerLine": 7,
From 8565eb3fb8b09dddb16133d124cf19862e829d27 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 2 Oct 2024 13:42:38 +0200
Subject: [PATCH 19/82] Update tsafe
---
package.json | 2 +-
yarn.lock | 8 ++++----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index 760e7453..14a5622b 100644
--- a/package.json
+++ b/package.json
@@ -62,7 +62,7 @@
],
"homepage": "https://www.keycloakify.dev",
"dependencies": {
- "tsafe": "^1.7.4"
+ "tsafe": "^1.7.5"
},
"devDependencies": {
"@babel/core": "^7.24.5",
diff --git a/yarn.lock b/yarn.lock
index d05e77af..c8b469f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -12579,10 +12579,10 @@ tsafe@^1.6.6:
resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.6.6.tgz#fd93e64d6eb13ef83ed1650669cc24bad4f5df9f"
integrity sha512-gzkapsdbMNwBnTIjgO758GujLCj031IgHK/PKr2mrmkCSJMhSOR5FeOuSxKLMUoYc0vAA4RGEYYbjt/v6afD3g==
-tsafe@^1.7.4:
- version "1.7.4"
- resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.7.4.tgz#7dd288b1a1be8d9c25e84ab8dd8a7df6094168d7"
- integrity sha512-4BrLklZMJ14dEtA+CkhY9OtID3al4+/GJhaeocWPtUuoZPr4SJkaqoPemyFgkLC1Y3LRNXF9zxa94SwssRGMaQ==
+tsafe@^1.7.5:
+ version "1.7.5"
+ resolved "https://registry.yarnpkg.com/tsafe/-/tsafe-1.7.5.tgz#0d3a31202b5ef87c7ba997e66e03fd80801278ef"
+ integrity sha512-tbNyyBSbwfbilFfiuXkSOj82a6++ovgANwcoqBAcO9/REPoZMEQoE8kWPeO0dy5A2D/2Lajr8Ohue5T0ifIvLQ==
tsc-alias@^1.8.10:
version "1.8.10"
From 7241f0c741bc2fc5d3db2cb1b93902b4652c364b Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 2 Oct 2024 13:44:22 +0200
Subject: [PATCH 20/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 14a5622b..e6c02daa 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.5",
+ "version": "11.2.6",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 80d8a0c4e399eba7201c623bb338bc04dcc607a5 Mon Sep 17 00:00:00 2001
From: johanjk
Date: Wed, 2 Oct 2024 16:16:16 +0200
Subject: [PATCH 21/82] ['select-radiobuttons'/'multiselect-checkboxes'] fixed
'inputOptionLabels'
---
src/login/UserProfileFormFields.tsx | 38 ++++++++++++------------
stories/login/pages/Register.stories.tsx | 32 ++++++++++++++++++++
2 files changed, 51 insertions(+), 19 deletions(-)
diff --git a/src/login/UserProfileFormFields.tsx b/src/login/UserProfileFormFields.tsx
index 445bda6f..93a99ad7 100644
--- a/src/login/UserProfileFormFields.tsx
+++ b/src/login/UserProfileFormFields.tsx
@@ -434,9 +434,7 @@ function AddRemoveButtonsMultiValuedAttribute(props: {
}
function InputTagSelects(props: InputFieldByTypeProps) {
- const { attribute, dispatchFormAction, kcClsx, valueOrValues } = props;
-
- const { advancedMsg } = props.i18n;
+ const { attribute, dispatchFormAction, kcClsx, i18n, valueOrValues } = props;
const { classDiv, classInput, classLabel, inputType } = (() => {
const { inputType } = attribute.annotations;
@@ -533,7 +531,7 @@ function InputTagSelects(props: InputFieldByTypeProps) {
htmlFor={`${attribute.name}-${option}`}
className={`${classLabel}${attribute.readOnly ? ` ${kcClsx("kcInputClassRadioCheckboxLabelDisabled")}` : ""}`}
>
- {advancedMsg(option)}
+ {inputLabel(i18n, attribute, option)}
))}
@@ -580,8 +578,6 @@ function TextareaTag(props: InputFieldByTypeProps) {
function SelectTag(props: InputFieldByTypeProps) {
const { attribute, dispatchFormAction, kcClsx, displayableErrors, i18n, valueOrValues } = props;
- const { advancedMsgStr } = i18n;
-
const isMultiple = attribute.annotations.inputType === "multiselect";
return (
@@ -645,22 +641,26 @@ function SelectTag(props: InputFieldByTypeProps) {
return options.map(option => (
));
})()}
);
}
+
+function inputLabel(i18n: I18n, attribute: Attribute, option: string) {
+ const { advancedMsg } = i18n;
+
+ if (attribute.annotations.inputOptionLabels !== undefined) {
+ const { inputOptionLabels } = attribute.annotations;
+
+ return advancedMsg(inputOptionLabels[option] ?? option);
+ }
+
+ if (attribute.annotations.inputOptionLabelsI18nPrefix !== undefined) {
+ return advancedMsg(`${attribute.annotations.inputOptionLabelsI18nPrefix}.${option}`);
+ }
+
+ return option;
+}
diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx
index 0a8c395f..f25568cc 100644
--- a/stories/login/pages/Register.stories.tsx
+++ b/stories/login/pages/Register.stories.tsx
@@ -115,6 +115,38 @@ export const WithFavoritePet: Story = {
)
};
+
+export const WithNewsletter: Story = {
+ render: () => (
+
+ )
+};
+
+
export const WithEmailAsUsername: Story = {
render: () => (
Date: Wed, 2 Oct 2024 23:36:58 +0200
Subject: [PATCH 22/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index e6c02daa..ff7fbe2a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.6",
+ "version": "11.2.8",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From ec297249974386d9d62f9ea422b1a718a375ba20 Mon Sep 17 00:00:00 2001
From: pnzrr <93841792+pnzrr@users.noreply.github.com>
Date: Thu, 3 Oct 2024 21:04:02 -0600
Subject: [PATCH 23/82] Fix link in CONTRIBUTING.md
---
CONTRIBUTING.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 733042ec..25b079a7 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,3 @@
Looking to contribute? Thank you! PR are more than welcome.
-Please refers to [this documentation page](https://docs.keycloakify.dev/contributing) that will help you get started.
+Please refers to [this documentation page](https://docs.keycloakify.dev/faq-and-help/contributing) that will help you get started.
From 908e083deeb3d7b0f867957a2e1a4bdee2f2903b Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:17:08 +0200
Subject: [PATCH 24/82] Update version target range
---
.../getKeycloakVersionRangeForJar.ts | 4 +-
src/bin/shared/KeycloakVersionRange.ts | 2 +-
src/bin/shared/buildContext.ts | 36 +-
src/bin/start-keycloak/myrealm-realm-26.json | 2400 +++++++++++++++++
4 files changed, 2431 insertions(+), 11 deletions(-)
create mode 100644 src/bin/start-keycloak/myrealm-realm-26.json
diff --git a/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts b/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
index 3999ce8f..6c301223 100644
--- a/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
+++ b/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
@@ -75,9 +75,9 @@ export function getKeycloakVersionRangeForJar(params: {
}
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
- return "21-and-below";
+ return "all-other-versions";
case "1.1.5":
- return "22-and-above";
+ return "22-to-25";
}
assert>(
false
diff --git a/src/bin/shared/KeycloakVersionRange.ts b/src/bin/shared/KeycloakVersionRange.ts
index 2827d34e..1e118ea1 100644
--- a/src/bin/shared/KeycloakVersionRange.ts
+++ b/src/bin/shared/KeycloakVersionRange.ts
@@ -3,7 +3,7 @@ export type KeycloakVersionRange =
| KeycloakVersionRange.WithoutAccountV1Theme;
export namespace KeycloakVersionRange {
- export type WithoutAccountV1Theme = "21-and-below" | "22-and-above";
+ export type WithoutAccountV1Theme = "22-to-25" | "all-other-versions";
export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
}
diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts
index b4a998b7..852bd1fd 100644
--- a/src/bin/shared/buildContext.ts
+++ b/src/bin/shared/buildContext.ts
@@ -25,6 +25,7 @@ import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
+import { is } from "tsafe/is";
export type BuildContext = {
themeVersion: string;
@@ -297,8 +298,8 @@ export function getBuildContext(params: {
]),
keycloakVersionTargets: z
.object({
- "21-and-below": z.union([z.boolean(), z.string()]),
- "22-and-above": z.union([z.boolean(), z.string()])
+ "22-to-25": z.union([z.boolean(), z.string()]),
+ "all-other-versions": z.union([z.boolean(), z.string()])
})
.optional()
});
@@ -779,11 +780,14 @@ export function getBuildContext(params: {
return keycloakVersionRange;
} else {
const keycloakVersionRange = (() => {
- if (buildForKeycloakMajorVersionNumber <= 21) {
- return "21-and-below" as const;
+ if (
+ buildForKeycloakMajorVersionNumber <= 21 ||
+ buildForKeycloakMajorVersionNumber >= 26
+ ) {
+ return "all-other-versions" as const;
}
- return "22-and-above" as const;
+ return "22-to-25" as const;
})();
assert<
@@ -801,6 +805,12 @@ export function getBuildContext(params: {
use_custom_jar_basename: {
const { keycloakVersionTargets } = buildOptions;
+ assert(
+ is>(
+ keycloakVersionTargets
+ )
+ );
+
if (keycloakVersionTargets === undefined) {
break use_custom_jar_basename;
}
@@ -861,8 +871,8 @@ export function getBuildContext(params: {
}
} else {
for (const keycloakVersionRange of [
- "21-and-below",
- "22-and-above"
+ "22-to-25",
+ "all-other-versions"
] as const) {
assert<
Equals<
@@ -888,7 +898,17 @@ export function getBuildContext(params: {
const jarTargets: BuildContext["jarTargets"] = [];
for (const [keycloakVersionRange, jarNameOrBoolean] of objectEntries(
- buildOptions.keycloakVersionTargets
+ (() => {
+ const { keycloakVersionTargets } = buildOptions;
+
+ assert(
+ is>(
+ keycloakVersionTargets
+ )
+ );
+
+ return keycloakVersionTargets;
+ })()
)) {
if (jarNameOrBoolean === false) {
continue;
diff --git a/src/bin/start-keycloak/myrealm-realm-26.json b/src/bin/start-keycloak/myrealm-realm-26.json
new file mode 100644
index 00000000..21b95023
--- /dev/null
+++ b/src/bin/start-keycloak/myrealm-realm-26.json
@@ -0,0 +1,2400 @@
+{
+ "id": "5d0dd960-0478-4ca6-b64a-810a3f6f4071",
+ "realm": "myrealm",
+ "notBefore": 0,
+ "defaultSignatureAlgorithm": "RS256",
+ "revokeRefreshToken": false,
+ "refreshTokenMaxReuse": 0,
+ "accessTokenLifespan": 300,
+ "accessTokenLifespanForImplicitFlow": 900,
+ "ssoSessionIdleTimeout": 1800,
+ "ssoSessionMaxLifespan": 36000,
+ "ssoSessionIdleTimeoutRememberMe": 0,
+ "ssoSessionMaxLifespanRememberMe": 0,
+ "offlineSessionIdleTimeout": 2592000,
+ "offlineSessionMaxLifespanEnabled": false,
+ "offlineSessionMaxLifespan": 5184000,
+ "clientSessionIdleTimeout": 0,
+ "clientSessionMaxLifespan": 0,
+ "clientOfflineSessionIdleTimeout": 0,
+ "clientOfflineSessionMaxLifespan": 0,
+ "accessCodeLifespan": 60,
+ "accessCodeLifespanUserAction": 300,
+ "accessCodeLifespanLogin": 1800,
+ "actionTokenGeneratedByAdminLifespan": 43200,
+ "actionTokenGeneratedByUserLifespan": 300,
+ "oauth2DeviceCodeLifespan": 600,
+ "oauth2DevicePollingInterval": 5,
+ "enabled": true,
+ "sslRequired": "external",
+ "registrationAllowed": true,
+ "registrationEmailAsUsername": false,
+ "rememberMe": true,
+ "verifyEmail": false,
+ "loginWithEmailAllowed": true,
+ "duplicateEmailsAllowed": false,
+ "resetPasswordAllowed": true,
+ "editUsernameAllowed": false,
+ "bruteForceProtected": false,
+ "permanentLockout": false,
+ "maxTemporaryLockouts": 0,
+ "maxFailureWaitSeconds": 900,
+ "minimumQuickLoginWaitSeconds": 60,
+ "waitIncrementSeconds": 60,
+ "quickLoginCheckMilliSeconds": 1000,
+ "maxDeltaTimeSeconds": 43200,
+ "failureFactor": 30,
+ "roles": {
+ "realm": [
+ {
+ "id": "cc4b5045-3bff-4aa7-889e-1492630c3002",
+ "name": "uma_authorization",
+ "description": "${role_uma_authorization}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "5d0dd960-0478-4ca6-b64a-810a3f6f4071",
+ "attributes": {}
+ },
+ {
+ "id": "e92017b2-18a0-49cd-956c-fad64f16b26b",
+ "name": "default-roles-myrealm",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "composites": {
+ "realm": ["offline_access", "uma_authorization"],
+ "client": {
+ "account": ["delete-account", "manage-account", "view-profile"]
+ }
+ },
+ "clientRole": false,
+ "containerId": "5d0dd960-0478-4ca6-b64a-810a3f6f4071",
+ "attributes": {}
+ },
+ {
+ "id": "e8616113-e302-4abe-bd5c-d51f8221046b",
+ "name": "offline_access",
+ "description": "${role_offline-access}",
+ "composite": false,
+ "clientRole": false,
+ "containerId": "5d0dd960-0478-4ca6-b64a-810a3f6f4071",
+ "attributes": {}
+ }
+ ],
+ "client": {
+ "myclient": [],
+ "realm-management": [
+ {
+ "id": "b27b272d-d153-4ae7-9fe7-fd96582f057d",
+ "name": "manage-events",
+ "description": "${role_manage-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "40fdfec8-f1b9-4c2b-81c5-a775bc047840",
+ "name": "manage-users",
+ "description": "${role_manage-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "5f446f9a-d008-4067-8325-f4658a32d964",
+ "name": "view-authorization",
+ "description": "${role_view-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "82bf956d-1fd1-4d20-a5a9-62b3e77e9d88",
+ "name": "create-client",
+ "description": "${role_create-client}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "b41e1ce8-d63f-4cf4-9966-e6c9eab5da11",
+ "name": "manage-clients",
+ "description": "${role_manage-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "3198743d-fdfa-4a9c-a229-5fb979847ec2",
+ "name": "view-users",
+ "description": "${role_view-users}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": ["query-users", "query-groups"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "e83c21cb-c84c-4824-9f7d-ce3574921800",
+ "name": "query-users",
+ "description": "${role_query-users}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "3f6e2e81-e40d-40ff-a5f3-12ba2614fba5",
+ "name": "query-groups",
+ "description": "${role_query-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "63111288-7f3d-4570-838f-48405d70e212",
+ "name": "view-realm",
+ "description": "${role_view-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "a7f8f8ad-057b-485e-abfa-8a98e5e0c4ea",
+ "name": "manage-realm",
+ "description": "${role_manage-realm}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "7783b160-2f1a-48c9-89fb-623a29f26c9a",
+ "name": "query-realms",
+ "description": "${role_query-realms}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "b8b5341f-f44f-40a2-9ba4-e2d621b11b2f",
+ "name": "impersonation",
+ "description": "${role_impersonation}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "6b9d72e9-949f-4897-b11a-c8aa9252f3f2",
+ "name": "query-clients",
+ "description": "${role_query-clients}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "bfa94ba9-1d70-4259-b928-906e8bb815b2",
+ "name": "view-events",
+ "description": "${role_view-events}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "96bb9322-5c1f-48f0-aa05-65521c77e742",
+ "name": "realm-admin",
+ "description": "${role_realm-admin}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": [
+ "manage-users",
+ "view-authorization",
+ "manage-events",
+ "create-client",
+ "view-users",
+ "manage-clients",
+ "query-users",
+ "query-groups",
+ "view-realm",
+ "manage-realm",
+ "query-realms",
+ "query-clients",
+ "impersonation",
+ "view-events",
+ "manage-authorization",
+ "manage-identity-providers",
+ "view-identity-providers",
+ "view-clients"
+ ]
+ }
+ },
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "6e0ca5ce-f5db-4580-90e5-27c35804fc34",
+ "name": "manage-authorization",
+ "description": "${role_manage-authorization}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "7499eb46-cf4a-4813-9bf9-42b1bbcadc0d",
+ "name": "manage-identity-providers",
+ "description": "${role_manage-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "fcc99ef9-347d-4c21-b25c-8229e906a1a3",
+ "name": "view-clients",
+ "description": "${role_view-clients}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "realm-management": ["query-clients"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ },
+ {
+ "id": "7b024069-57d8-4368-9942-8790507c156d",
+ "name": "view-identity-providers",
+ "description": "${role_view-identity-providers}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "attributes": {}
+ }
+ ],
+ "security-admin-console": [],
+ "admin-cli": [],
+ "account-console": [],
+ "broker": [
+ {
+ "id": "3050eb8a-9a47-4a27-aece-be2e60fc7f73",
+ "name": "read-token",
+ "description": "${role_read-token}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "f5e032da-c8ab-48c2-959c-8466ad1e6a09",
+ "attributes": {}
+ }
+ ],
+ "account": [
+ {
+ "id": "d554d15b-d098-47a0-bdd5-d656b20f5643",
+ "name": "delete-account",
+ "description": "${role_delete-account}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "aaf4946d-2cd4-43ba-ad7d-86be56b9ad2c",
+ "name": "view-applications",
+ "description": "${role_view-applications}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "b417b187-18b7-41fa-9537-3313cf9b8ed4",
+ "name": "manage-account",
+ "description": "${role_manage-account}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": ["manage-account-links"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "8bb5480d-83a3-4ea2-8e91-237b8870acec",
+ "name": "view-consent",
+ "description": "${role_view-consent}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "e341c1b8-eaf7-467d-9986-d3f2356a60b9",
+ "name": "view-profile",
+ "description": "${role_view-profile}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "98ccac20-3906-436f-8dc3-ae8d8ae25cbc",
+ "name": "view-groups",
+ "description": "${role_view-groups}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "adfba539-826f-4fa7-86f5-8c1287152ed6",
+ "name": "manage-account-links",
+ "description": "${role_manage-account-links}",
+ "composite": false,
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ },
+ {
+ "id": "2516ab58-490c-444c-9e7d-0dd8b87a69f0",
+ "name": "manage-consent",
+ "description": "${role_manage-consent}",
+ "composite": true,
+ "composites": {
+ "client": {
+ "account": ["view-consent"]
+ }
+ },
+ "clientRole": true,
+ "containerId": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "attributes": {}
+ }
+ ]
+ }
+ },
+ "groups": [],
+ "defaultRole": {
+ "id": "e92017b2-18a0-49cd-956c-fad64f16b26b",
+ "name": "default-roles-myrealm",
+ "description": "${role_default-roles}",
+ "composite": true,
+ "clientRole": false,
+ "containerId": "5d0dd960-0478-4ca6-b64a-810a3f6f4071"
+ },
+ "requiredCredentials": ["password"],
+ "otpPolicyType": "totp",
+ "otpPolicyAlgorithm": "HmacSHA1",
+ "otpPolicyInitialCounter": 0,
+ "otpPolicyDigits": 6,
+ "otpPolicyLookAheadWindow": 1,
+ "otpPolicyPeriod": 30,
+ "otpPolicyCodeReusable": false,
+ "otpSupportedApplications": [
+ "totpAppFreeOTPName",
+ "totpAppGoogleName",
+ "totpAppMicrosoftAuthenticatorName"
+ ],
+ "localizationTexts": {
+ "de": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "no": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "fi": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "ru": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "pt": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "lt": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "lv": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "fr": {
+ "profile.attributes.favourite_pet": "Animal de compagnie préféré",
+ "profile.attributes.favourite_pet.cat": "Chat",
+ "profile.attributes.favourite_pet.dog": "Chien",
+ "profile.attributes.favourite_pet.bird": "Oiseau"
+ },
+ "hu": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "zh-CN": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "uk": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "sk": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "ca": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "sv": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "zh-TW": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "pt-BR": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "en": {
+ "profile.attributes.favourite_pet": "Favourite Pet",
+ "profile.attributes.favourite_pet.cat": "Cat",
+ "profile.attributes.favourite_pet.dog": "Dog",
+ "profile.attributes.favourite_pet.bird": "Bird"
+ },
+ "it": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "es": {
+ "profile.attributes.favourite_pet": "Mascota favorita",
+ "profile.attributes.favourite_pet.cat": "Gato",
+ "profile.attributes.favourite_pet.dog": "Perro",
+ "profile.attributes.favourite_pet.bird": "Pájaro"
+ },
+ "cs": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "ar": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "th": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "ja": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "fa": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "pl": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "da": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "nl": {
+ "profile.attributes.favourite_pet": ""
+ },
+ "tr": {
+ "profile.attributes.favourite_pet": ""
+ }
+ },
+ "webAuthnPolicyRpEntityName": "keycloak",
+ "webAuthnPolicySignatureAlgorithms": ["ES256"],
+ "webAuthnPolicyRpId": "",
+ "webAuthnPolicyAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyRequireResidentKey": "not specified",
+ "webAuthnPolicyUserVerificationRequirement": "not specified",
+ "webAuthnPolicyCreateTimeout": 0,
+ "webAuthnPolicyAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyAcceptableAaguids": [],
+ "webAuthnPolicyExtraOrigins": [],
+ "webAuthnPolicyPasswordlessRpEntityName": "keycloak",
+ "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"],
+ "webAuthnPolicyPasswordlessRpId": "",
+ "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified",
+ "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified",
+ "webAuthnPolicyPasswordlessRequireResidentKey": "not specified",
+ "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified",
+ "webAuthnPolicyPasswordlessCreateTimeout": 0,
+ "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false,
+ "webAuthnPolicyPasswordlessAcceptableAaguids": [],
+ "webAuthnPolicyPasswordlessExtraOrigins": [],
+ "users": [
+ {
+ "id": "d93e1772-4916-4243-850f-a6d9b2615716",
+ "username": "testuser",
+ "firstName": "Test",
+ "lastName": "User",
+ "email": "testuser@gmail.com",
+ "emailVerified": true,
+ "attributes": {
+ "additional_emails": ["test.user@protonmail.com", "testuser@hotmail.com"],
+ "gender": ["prefer_not_to_say"],
+ "favorite_pet": ["cats"],
+ "favourite_pet": ["cat"],
+ "bio": ["Hello I'm Test User and I do not exist."],
+ "phone_number": ["1111111111"],
+ "locale": ["en"],
+ "favorite_media": ["movies", "series"]
+ },
+ "createdTimestamp": 1716183898408,
+ "enabled": true,
+ "totp": false,
+ "credentials": [
+ {
+ "id": "576982e2-6fb3-4752-8724-5ff390ea8301",
+ "type": "password",
+ "userLabel": "My password",
+ "createdDate": 1716183916529,
+ "secretData": "{\"value\":\"9hwJ989FAr0UgT0MfffNYSI6Zf/3qT/y17DTUcwbiEM=\",\"salt\":\"C3ZnHzgPd+0Lemw4olCOgA==\",\"additionalParameters\":{}}",
+ "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}"
+ }
+ ],
+ "disableableCredentialTypes": [],
+ "requiredActions": [],
+ "realmRoles": ["default-roles-myrealm"],
+ "notBefore": 0,
+ "groups": []
+ }
+ ],
+ "scopeMappings": [
+ {
+ "clientScope": "offline_access",
+ "roles": ["offline_access"]
+ }
+ ],
+ "clientScopeMappings": {
+ "account": [
+ {
+ "client": "account-console",
+ "roles": ["manage-account", "view-groups"]
+ }
+ ]
+ },
+ "clients": [
+ {
+ "id": "7221ef76-9d96-49ad-88a6-9f72eeeb0aa7",
+ "clientId": "account",
+ "name": "${client_account}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/myrealm/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/realms/myrealm/account/*"],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "d8f14dc4-5f0f-4a1d-8c0b-cfe78ee55cb3",
+ "clientId": "account-console",
+ "name": "${client_account-console}",
+ "rootUrl": "${authBaseUrl}",
+ "baseUrl": "/realms/myrealm/account/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/realms/myrealm/account/*"],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+",
+ "pkce.code.challenge.method": "S256"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "08d7bc08-2ff3-44ea-9d65-fa1c4ca35646",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {}
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "953c597f-faef-4abc-88dc-4fbc9501170c",
+ "clientId": "admin-cli",
+ "name": "${client_admin-cli}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": false,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "f5e032da-c8ab-48c2-959c-8466ad1e6a09",
+ "clientId": "broker",
+ "name": "${client_broker}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": true,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "8fba88fa-61e9-45a4-893d-ab102973ebf6",
+ "clientId": "myclient",
+ "name": "",
+ "description": "",
+ "rootUrl": "https://my-theme.keycloakify.dev",
+ "adminUrl": "https://my-theme.keycloakify.dev",
+ "baseUrl": "https://my-theme.keycloakify.dev",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [
+ "https://my-theme.keycloakify.dev/*",
+ "http://localhost*",
+ "http://127.0.0.1*"
+ ],
+ "webOrigins": ["*"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": true,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": true,
+ "protocol": "openid-connect",
+ "attributes": {
+ "oidc.ciba.grant.enabled": "false",
+ "backchannel.logout.session.required": "true",
+ "login_theme": "keycloakify-starter",
+ "post.logout.redirect.uris": "https://my-theme.keycloakify.dev/*##http://localhost*##http://127.0.0.1*",
+ "oauth2.device.authorization.grant.enabled": "false",
+ "display.on.consent.screen": "false",
+ "backchannel.logout.revoke.offline.tokens": "false"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": true,
+ "nodeReRegistrationTimeout": -1,
+ "protocolMappers": [
+ {
+ "id": "91a196c1-f93c-48a5-aced-b8d60fb09b62",
+ "name": "Favourite Pet",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "favourite_pet",
+ "id.token.claim": "true",
+ "lightweight.claim": "false",
+ "access.token.claim": "true",
+ "claim.name": "favourite_pet",
+ "jsonType.label": "String"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "e05cc68c-5e53-4796-ae3a-a1bfbf5c51bb",
+ "clientId": "realm-management",
+ "name": "${client_realm-management}",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": [],
+ "webOrigins": [],
+ "notBefore": 0,
+ "bearerOnly": true,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": false,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ },
+ {
+ "id": "fce8a109-6f32-4814-9a20-2ff2435d2da6",
+ "clientId": "security-admin-console",
+ "name": "${client_security-admin-console}",
+ "rootUrl": "${authAdminUrl}",
+ "baseUrl": "/admin/myrealm/console/",
+ "surrogateAuthRequired": false,
+ "enabled": true,
+ "alwaysDisplayInConsole": false,
+ "clientAuthenticatorType": "client-secret",
+ "redirectUris": ["/admin/myrealm/console/*"],
+ "webOrigins": ["+"],
+ "notBefore": 0,
+ "bearerOnly": false,
+ "consentRequired": false,
+ "standardFlowEnabled": true,
+ "implicitFlowEnabled": false,
+ "directAccessGrantsEnabled": false,
+ "serviceAccountsEnabled": false,
+ "publicClient": true,
+ "frontchannelLogout": false,
+ "protocol": "openid-connect",
+ "attributes": {
+ "post.logout.redirect.uris": "+",
+ "pkce.code.challenge.method": "S256"
+ },
+ "authenticationFlowBindingOverrides": {},
+ "fullScopeAllowed": false,
+ "nodeReRegistrationTimeout": 0,
+ "protocolMappers": [
+ {
+ "id": "52192d19-0406-41b7-b995-b099bdbaa448",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String"
+ }
+ }
+ ],
+ "defaultClientScopes": [
+ "web-origins",
+ "acr",
+ "profile",
+ "roles",
+ "basic",
+ "email"
+ ],
+ "optionalClientScopes": [
+ "address",
+ "phone",
+ "offline_access",
+ "microprofile-jwt"
+ ]
+ }
+ ],
+ "clientScopes": [
+ {
+ "id": "6a955b1e-f0e2-49fa-b3c9-bd59ed1fcd4f",
+ "name": "web-origins",
+ "description": "OpenID Connect scope for add allowed web origins to the access token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "consent.screen.text": "",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "3a392f70-ed70-424a-b60b-82db32b83df8",
+ "name": "allowed web origins",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-allowed-origins-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "9cda058d-9935-4c8b-844d-c163d10f7c3c",
+ "name": "address",
+ "description": "OpenID Connect built-in scope: address",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${addressScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "a053d8ec-b267-4e5a-a424-3b14bef9cd15",
+ "name": "address",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-address-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute.formatted": "formatted",
+ "user.attribute.country": "country",
+ "introspection.token.claim": "true",
+ "user.attribute.postal_code": "postal_code",
+ "userinfo.token.claim": "true",
+ "user.attribute.street": "street",
+ "id.token.claim": "true",
+ "user.attribute.region": "region",
+ "access.token.claim": "true",
+ "user.attribute.locality": "locality"
+ }
+ }
+ ]
+ },
+ {
+ "id": "6225f4c7-ad5c-42ea-b7d4-5bb4e7c77459",
+ "name": "phone",
+ "description": "OpenID Connect built-in scope: phone",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${phoneScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "5052be82-243f-41b0-a214-4f01935180e5",
+ "name": "phone number",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "phoneNumber",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "phone_number",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "4d31d278-e6ef-4b8b-97cb-4da9626d0e93",
+ "name": "phone number verified",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "phoneNumberVerified",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "phone_number_verified",
+ "jsonType.label": "boolean"
+ }
+ }
+ ]
+ },
+ {
+ "id": "9357440c-6200-41a1-a447-0ec97895763e",
+ "name": "basic",
+ "description": "OpenID Connect scope for add all basic claims to the token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "bf9cb6c6-71a4-4bf9-8c60-ed58adcc2258",
+ "name": "auth_time",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usersessionmodel-note-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.session.note": "AUTH_TIME",
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "auth_time",
+ "jsonType.label": "long"
+ }
+ },
+ {
+ "id": "679c8292-1abb-4d96-bacc-671303765f9b",
+ "name": "sub",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-sub-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "0ec225e7-253b-4a01-85e1-68daf3df3eba",
+ "name": "role_list",
+ "description": "SAML role list",
+ "protocol": "saml",
+ "attributes": {
+ "consent.screen.text": "${samlRoleListScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "a55cf74e-ce68-4ebd-9c24-dc3fd6a9cfa5",
+ "name": "role list",
+ "protocol": "saml",
+ "protocolMapper": "saml-role-list-mapper",
+ "consentRequired": false,
+ "config": {
+ "single": "false",
+ "attribute.nameformat": "Basic",
+ "attribute.name": "Role"
+ }
+ }
+ ]
+ },
+ {
+ "id": "e2f1dd86-00a2-4374-b888-7211f748c58d",
+ "name": "offline_access",
+ "description": "OpenID Connect built-in scope: offline_access",
+ "protocol": "openid-connect",
+ "attributes": {
+ "consent.screen.text": "${offlineAccessScopeConsentText}",
+ "display.on.consent.screen": "true"
+ }
+ },
+ {
+ "id": "e86456b8-0663-448e-ad16-7d520d0c448e",
+ "name": "profile",
+ "description": "OpenID Connect built-in scope: profile",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${profileScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "569c799d-79f2-4b2b-a1ec-3661e3d8d433",
+ "name": "gender",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "gender",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "gender",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "2d01eb48-77c3-4c83-a864-755699cb7e7c",
+ "name": "updated at",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "updatedAt",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "updated_at",
+ "jsonType.label": "long"
+ }
+ },
+ {
+ "id": "a9700270-006f-4a85-8458-f39644659029",
+ "name": "locale",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "locale",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "locale",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "3a7bca96-0839-4d1e-b37d-6e624f37facb",
+ "name": "profile",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "profile",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "profile",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "2a41be1c-872a-4b3e-9051-71ebd5d140c1",
+ "name": "website",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "website",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "website",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "9fe5e57d-ee79-4b8b-9ab2-345093a1fdbf",
+ "name": "full name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-full-name-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ },
+ {
+ "id": "bda9e4e7-4de0-455d-bace-4e94b1dab5ad",
+ "name": "nickname",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "nickname",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "nickname",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "312a0b4d-46b8-42e0-b162-e5869b317b36",
+ "name": "zoneinfo",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "zoneinfo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "zoneinfo",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "4f8ac9bc-e32d-4ebb-bb85-b9a94a459aa1",
+ "name": "username",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "preferred_username",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "bebdf0c7-6f0f-4b08-a327-50af837c82b9",
+ "name": "family name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "lastName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "family_name",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "d96d9686-f4e0-479a-9855-cfc526a35294",
+ "name": "middle name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "middleName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "middle_name",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "66ad8239-e1df-4f9d-9cb7-d35f23f95f37",
+ "name": "given name",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "firstName",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "given_name",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "ece8245b-16ae-4322-bc78-f8d5f671640a",
+ "name": "picture",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "picture",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "picture",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "384cf049-0fed-47e2-8b11-06cf6c03465d",
+ "name": "birthdate",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "birthdate",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "birthdate",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "49e85de9-edd1-4a9e-a2b0-e9c663d4dd9a",
+ "name": "email",
+ "description": "OpenID Connect built-in scope: email",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "consent.screen.text": "${emailScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "d458e6fc-b414-4b45-b9e1-99342d7d2bba",
+ "name": "email",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "email",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "2b73ce63-0443-46dc-b35c-1148edb976ab",
+ "name": "email verified",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-property-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "emailVerified",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "email_verified",
+ "jsonType.label": "boolean"
+ }
+ }
+ ]
+ },
+ {
+ "id": "71303f6d-348a-4892-9d6f-dc9a2d2e4b14",
+ "name": "microprofile-jwt",
+ "description": "Microprofile - JWT built-in scope",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "true",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "498cbff6-a650-4a09-8192-5defaa50f33b",
+ "name": "upn",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-attribute-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "username",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "upn",
+ "jsonType.label": "String"
+ }
+ },
+ {
+ "id": "eb8585bc-ca30-410e-9f92-0d63665f5ed6",
+ "name": "groups",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "multivalued": "true",
+ "userinfo.token.claim": "true",
+ "user.attribute": "foo",
+ "id.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "groups",
+ "jsonType.label": "String"
+ }
+ }
+ ]
+ },
+ {
+ "id": "62b8c264-2c10-48c6-803f-b7606a89e0d9",
+ "name": "roles",
+ "description": "OpenID Connect scope for add user roles to the access token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "consent.screen.text": "${rolesScopeConsentText}",
+ "display.on.consent.screen": "true"
+ },
+ "protocolMappers": [
+ {
+ "id": "0c18ca55-df63-4071-81f9-43f5d077c015",
+ "name": "realm roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-realm-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "foo",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "realm_access.roles",
+ "jsonType.label": "String",
+ "multivalued": "true"
+ }
+ },
+ {
+ "id": "6de6510d-d7f3-4289-a10f-4c21289313a4",
+ "name": "audience resolve",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-audience-resolve-mapper",
+ "consentRequired": false,
+ "config": {
+ "introspection.token.claim": "true",
+ "access.token.claim": "true"
+ }
+ },
+ {
+ "id": "a5851eb2-bfc5-4a0a-8a49-92f4fc8c5041",
+ "name": "client roles",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-usermodel-client-role-mapper",
+ "consentRequired": false,
+ "config": {
+ "user.attribute": "foo",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "claim.name": "resource_access.${client_id}.roles",
+ "jsonType.label": "String",
+ "multivalued": "true"
+ }
+ }
+ ]
+ },
+ {
+ "id": "bfc69775-83af-4816-82fd-d1c42687fb5e",
+ "name": "acr",
+ "description": "OpenID Connect scope for add acr (authentication context class reference) to the token",
+ "protocol": "openid-connect",
+ "attributes": {
+ "include.in.token.scope": "false",
+ "display.on.consent.screen": "false"
+ },
+ "protocolMappers": [
+ {
+ "id": "8e2027d5-32dd-4a87-a7ec-00e5316c5617",
+ "name": "acr loa level",
+ "protocol": "openid-connect",
+ "protocolMapper": "oidc-acr-mapper",
+ "consentRequired": false,
+ "config": {
+ "id.token.claim": "true",
+ "introspection.token.claim": "true",
+ "access.token.claim": "true",
+ "userinfo.token.claim": "true"
+ }
+ }
+ ]
+ }
+ ],
+ "defaultDefaultClientScopes": [
+ "role_list",
+ "profile",
+ "email",
+ "roles",
+ "web-origins",
+ "acr",
+ "basic"
+ ],
+ "defaultOptionalClientScopes": [
+ "offline_access",
+ "address",
+ "phone",
+ "microprofile-jwt"
+ ],
+ "browserSecurityHeaders": {
+ "contentSecurityPolicyReportOnly": "",
+ "xContentTypeOptions": "nosniff",
+ "referrerPolicy": "no-referrer",
+ "xRobotsTag": "none",
+ "xFrameOptions": "SAMEORIGIN",
+ "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
+ "xXSSProtection": "1; mode=block",
+ "strictTransportSecurity": "max-age=31536000; includeSubDomains"
+ },
+ "smtpServer": {},
+ "loginTheme": "keycloakify-starter",
+ "accountTheme": "keycloakify-starter",
+ "adminTheme": "",
+ "emailTheme": "",
+ "eventsEnabled": false,
+ "eventsListeners": ["jboss-logging"],
+ "enabledEventTypes": [],
+ "adminEventsEnabled": false,
+ "adminEventsDetailsEnabled": false,
+ "identityProviders": [],
+ "identityProviderMappers": [],
+ "components": {
+ "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [
+ {
+ "id": "67526992-f0ce-42ff-a0fb-af267192ff70",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": ["true"]
+ }
+ },
+ {
+ "id": "64a2f718-da10-45d9-a75a-69c156a7ccd8",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "authenticated",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "oidc-full-name-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "oidc-address-mapper",
+ "saml-user-attribute-mapper",
+ "oidc-usermodel-property-mapper",
+ "saml-user-property-mapper",
+ "saml-role-list-mapper",
+ "oidc-sha256-pairwise-sub-mapper"
+ ]
+ }
+ },
+ {
+ "id": "4d3e104f-6fdf-45eb-b756-5fef6840fbed",
+ "name": "Consent Required",
+ "providerId": "consent-required",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ },
+ {
+ "id": "c647e85f-6700-4d66-84f2-4a869e467735",
+ "name": "Max Clients Limit",
+ "providerId": "max-clients",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "max-clients": ["200"]
+ }
+ },
+ {
+ "id": "51f41974-f7e5-4e7d-b486-5bd652a98e93",
+ "name": "Allowed Protocol Mapper Types",
+ "providerId": "allowed-protocol-mappers",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allowed-protocol-mapper-types": [
+ "oidc-sha256-pairwise-sub-mapper",
+ "oidc-usermodel-property-mapper",
+ "oidc-address-mapper",
+ "oidc-usermodel-attribute-mapper",
+ "oidc-full-name-mapper",
+ "saml-user-attribute-mapper",
+ "saml-user-property-mapper",
+ "saml-role-list-mapper"
+ ]
+ }
+ },
+ {
+ "id": "8f7d6ece-e956-4e48-95ab-5ab72b2b7c9a",
+ "name": "Allowed Client Scopes",
+ "providerId": "allowed-client-templates",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "allow-default-scopes": ["true"]
+ }
+ },
+ {
+ "id": "e60b1167-cdee-4173-be99-3dad6a536b4a",
+ "name": "Trusted Hosts",
+ "providerId": "trusted-hosts",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {
+ "host-sending-registration-request-must-match": ["true"],
+ "client-uris-must-match": ["true"]
+ }
+ },
+ {
+ "id": "5ba8b893-ab01-430b-9092-32646a50a662",
+ "name": "Full Scope Disabled",
+ "providerId": "scope",
+ "subType": "anonymous",
+ "subComponents": {},
+ "config": {}
+ }
+ ],
+ "org.keycloak.userprofile.UserProfileProvider": [
+ {
+ "id": "237022c6-9443-46b3-902e-210e14c3c9a8",
+ "providerId": "declarative-user-profile",
+ "subComponents": {},
+ "config": {
+ "kc.user.profile.config": [
+ "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"favourite_pet\",\"displayName\":\"${profile.attributes.favourite_pet}\",\"validations\":{\"options\":{\"options\":[\"cat\",\"dog\",\"bird\"]}},\"annotations\":{\"inputType\":\"select\",\"inputOptionLabelsI18nPrefix\":\"profile.attributes.favourite_pet\"},\"required\":{\"roles\":[\"admin\",\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}]}"
+ ]
+ }
+ }
+ ],
+ "org.keycloak.keys.KeyProvider": [
+ {
+ "id": "5f3c1765-8810-419f-9c18-4a2db0e874e7",
+ "name": "rsa-generated",
+ "providerId": "rsa-generated",
+ "subComponents": {},
+ "config": {
+ "privateKey": [
+ "MIIEowIBAAKCAQEAsYUWzVfZMd6ywpBmLJYeF1U9Mgd/z3xWvl1Yq76oRPPfpcqQitN+cktWqu0hPerCVSl2ltwXDMrUwFzswG9MiM9hb+BLEld7kYiYkcFNt3lCtmmeRQEae7JwWimzeNV96Qlz0tHY8f9Zh0ffPDsLTN1HGAeRJJhI7mNQm6qCJNMCfVA/O5SWumsIn2XLnSMiQ05AACVHOLUq6rAZ2zCCaYmXTmJkuSOb8e26V303P6l63DSe5HSNXDdI00tjfFFf37q870zhvfsotrjjx0RMijy9Kjj8OZF+pFHpDRaGEi8tpQxZDnCTofTieB/Vp3QP+aTlvAyD3Q1ZnJxGQCLygwIDAQABAoIBABUJ9XMJGNQzamiVwuOWN7ht4UP8ezYvgdEA8NaLUO0PIYVIKyD7l4OwkHPPM9PfRACM2qG0MZp8sCyg4WxIeepy+D979oRqJYUmNRLSipqWlASuItRXIPjiY99uYXdjh2R8Os5pvCD+MZxPX9KHGuaVXmzSJMO7YAAPeYkMHcLYTp/U0c65Ztaaz1zz1FeyvpjkLr9SHiMcIN51zFmhvT1tcRIqy4zidisjrTSUr/KPVxeJtrEfyhTGk3z41yJf5YbeaxaMjJR5x0WXzt1fWVmA/V1bWa2Zlj9d8AxDReA1p7Lpstz34PRoCMj9bmFguI2+RTw6K0D++Jydfxmh8vUCgYEA5Zwk2r3TFO3i3V70LOn6CLzn15yLeuSIJ9p2os70jQOmFMCreLdcUbCaiUe7UV/IIVftbcxhFm9zECXZXX0wubcmHZqyptlbuAn1de4QkLJixXo1A7ZQXBEZk22WN2naXHQF5oK6lh/VSLcZBajTsyvBm5JWXrd8djjG06MugA8CgYEAxexKI5IwcLhpMDV9UPQb/+lDWHVqCT2xwYxnZ85y+5gmrOyyT7mIChz3DFYiaw4CHJWmBkIDBaiDgLEgQk4QXWzYshXawShBHnv1h08bVMMw98Ivec7ZRkV+/ET30YRwC2Uyk4bm4HpwVV5GCFhC4aAvRcCA1CIJk3MwcOwksk0CgYEAqxyaOomMbOR7VQ4WWgJkW26sOHppV8RH06tzDhG9HfnCI2USZHwBSL+b6wKSDiqbMn4cat8M23NjBH2wZ4OMdFqRBS7sRHtnZtfFHYW0wqCuCwzvxTxw1qvHq57Xe6RfHtc4LnjuJELE59PLyfPvEG9jcVS1GREUp+XYBpBtbvECgYAMhWBDU9JAr0noRNoCrw6+Z9Fc3UCyCPcf2XQJOyRHCl8X/XliVchna2GtpB1VTHORv13bc32hdAGtuIbj6vBaGLK0wXEvWw6TkR/9SWHfQOHuKpi6Sf2w1mCsMOjElm5IKkTC1Hvyo4xLukUP7hV9FJcpAH6l7OlSLK1Z13aS2QKBgB6w4gvmVEQruHV5+K60OatuFojr+kxJwmzCb5uKOULUFezT2pA3p3l6IWxGL2XtM+LD0SiZE3KZJUzf+LatYlBU9ek4F1krkVNUTRZpzUa0oADbymCL1chM4oPIs7sISQlFIH2wOSZt6Blvcw0E0wfjd9Gv/LHxcMnlRb1t1sLk"
+ ],
+ "keyUse": ["SIG"],
+ "certificate": [
+ "MIICnTCCAYUCBgGQBsyplzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALGFFs1X2THessKQZiyWHhdVPTIHf898Vr5dWKu+qETz36XKkIrTfnJLVqrtIT3qwlUpdpbcFwzK1MBc7MBvTIjPYW/gSxJXe5GImJHBTbd5QrZpnkUBGnuycFops3jVfekJc9LR2PH/WYdH3zw7C0zdRxgHkSSYSO5jUJuqgiTTAn1QPzuUlrprCJ9ly50jIkNOQAAlRzi1KuqwGdswgmmJl05iZLkjm/Htuld9Nz+petw0nuR0jVw3SNNLY3xRX9+6vO9M4b37KLa448dETIo8vSo4/DmRfqRR6Q0WhhIvLaUMWQ5wk6H04ngf1ad0D/mk5bwMg90NWZycRkAi8oMCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAVS+gJshIFX6cmBGI8UaOOI/9+XFb4Gi+DHaHVWVVHTd14MoqNK1bmmyTHbGIZbvK8UqgJ9+FhJX1ejx17d4KBzkZI3tYvPnVacHvaw1CIUMZ1Ini6u+UGUTnIlnQzCG0pcTKjOZXf3ih1B2CKdwyC7XeXyEJHicAIG7XfzYfYd9DYHvA+h6hrXaQcNJMW7WFNbtb3fJhtlv5P1Iw+ZEGdj15ukMI0bg2OEQA0F3jIw6QZpigSAGuai3HOY6OgoPO82d7TyTYlNhuwyutWr9izl6QMc2R7BmRfW9XQj4ICR2VWJiL9nqz+SOyqnjQiOObuw8Vywb8c36R1Ym1aaGjOw=="
+ ],
+ "priority": ["100"]
+ }
+ },
+ {
+ "id": "e586f825-a25a-4833-a38e-4c6484ad17fd",
+ "name": "rsa-enc-generated",
+ "providerId": "rsa-enc-generated",
+ "subComponents": {},
+ "config": {
+ "privateKey": [
+ "MIIEogIBAAKCAQEAkQtefHy82e8d5dVWN00LnGI5YmBOTKh0tgqayVRjqLH6u3NfgJVVIe0tFnxa7Wka/ySHrn1KSsW52czZ4uPXLUo4sXBkQxyyFXeZiWN8H+9WiUQ+0hefZF4es5ZPhY2VpeMK9XAnphC362LFLVycXulkpJcQ+4DjI99To4LLyJmjQvsVaJ7amoVJ5xd62eUv+D7f2+jwuaTwjGE3+MWZADXjVxsUY1qJuGLGKnLkNNxJNMDhvnKYw+aa3Z4V90fQVyjN1Volgw3DdA59o4wrWEy+2xHc6j2ESi8+cM60fWzZU9sp2XkyJoCnV7nmwk7pZkDy3zvAkeOWzrr3OWeR3wIDAQABAoIBACWMcet8R0+L7YuATQ+H7IeRjhV/pQWHXp9541RXem1DlgtM9N5Oynk78z4s90Uavphqlo1/deohgdl2hLmODjh1THPzCqGtHhUcnyzICmwiA58JgdHVt7e9/eiz8uY6HxGQ01dyr3D4RwSyzyTNItYXSayqRwU0+phgykA8LhFCAQM/UkRXDf6UCFKBhDyE7VPBaDv0xyxNb7dKtE7C6Qo5t5D40xCfQ8ni8OcD5RvshQq5xOWcw7igxAhlmXCu1fuO2CDiSiqXLMENs4NlwilQ3caMXAIzUiblaKwCrrK2noBoitx6vuOR2tKmIZSlTyDAG4vLQQtOHk53hBoupGECgYEAx4jSmLM9uUzNwNY1zfs8iNswxbU3YibNe2Q+IFmOQofvTaq1jBBxdPWX5ifIbuTvOAA33pmJRh+BtWzOBBQC7Z4i9mdfvyWB6s8t9nnTnWIY5Hj+hV5gaqae59MjdudsORR887fxzPIeAwwaETfKaZnYpC6zLaE3BXwhIcjlFTcCgYEAuhcKf16JkEYNIwanVHpUXjFxwAThAogHWZAngRokmai67Iulx+rSUhhtOIXtmjj/EaObsrqo5yCKAVZ5EbPTOajdd9RtFzH6q3bRjRdp8o8ZVx4c1vMNaOnLbvK4YzJlKSZN9N7m255Mg+/ea3veKVZsSVHDMnuYmH8GjncjPJkCgYAOIUlQmPjZA3BapJDA2nbJ9kO47IFUiQzqHQotPkpNudSfemRK2+s87htoqA6Qk9PA8nsCX3sSJS8JSwA317bxXs55Bo8IOT6/AxbtKmlq7sR2gX78sNdBFjWQkyoixHasgB/tHmyYJ9kqPBQoffvuiH+H+OqlY5JC6CxseQ6H9wKBgF69Hj4MDjLiRwve9k9+2/b8azHcCgX05PEG/+WtPpbwHQIScnseJKdhAjH1lSqf+9OqHLlYaGcK3Nejg42spEvFmcLI5iUZ78lde3++PNUdX0RH81zHbrtL06MPdSojXPcfJi8VUCjdJY1CEFVeQZOACS8mrh7EZ8KzYM4k/055AoGAYqjBv3WS8ul7kAsjpZKpIw1QZZaTjBSmLpjB6X8InF+Zihjgm80Dd4RMFnMnEawhFBvnpklvyw5Ce6NSwcC137kN3NVpJypykkXuYkimg7OxgJjR7YFdbQWJWlc+1eB81WTHcEOHVI/DmeV2yVJcv6kA2iC+3/JA0VoJxvrRBKc="
+ ],
+ "keyUse": ["ENC"],
+ "certificate": [
+ "MIICnTCCAYUCBgGQBsyq0jANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdteXJlYWxtMB4XDTI0MDYxMTEwMTQ1NFoXDTM0MDYxMTEwMTYzNFowEjEQMA4GA1UEAwwHbXlyZWFsbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJELXnx8vNnvHeXVVjdNC5xiOWJgTkyodLYKmslUY6ix+rtzX4CVVSHtLRZ8Wu1pGv8kh659SkrFudnM2eLj1y1KOLFwZEMcshV3mYljfB/vVolEPtIXn2ReHrOWT4WNlaXjCvVwJ6YQt+tixS1cnF7pZKSXEPuA4yPfU6OCy8iZo0L7FWie2pqFSecXetnlL/g+39vo8Lmk8IxhN/jFmQA141cbFGNaibhixipy5DTcSTTA4b5ymMPmmt2eFfdH0FcozdVaJYMNw3QOfaOMK1hMvtsR3Oo9hEovPnDOtH1s2VPbKdl5MiaAp1e55sJO6WZA8t87wJHjls669zlnkd8CAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAD9wQ+CJ0FRgls3JrUzxwHLgrJ3Yo4+mDFpSe1rh2XYK5FEIWDWSqxaXI3p0cOZq75RZmI2xV8oaiJMUz9WMZkbNe/KtGRzHY1N9AZooicGIsnFu1t++b8taFxxpvKWZgnbOum2PZlfcNiXL0QeMv0wwhfn9zKA9W1DRcqYGbIamoyVlumvbNyIjqXJKwGYIOW6GNt7v3wJl5AJw8qAU/O/DQwWwmzcnFGNRxRxAwI7we8EiQ5JlG0Wi+nyAQn74o3RhNr3zsY0ndmFx9bFV4BBo2AiYGozCDOCCG5HvrmoDbrm//wmGRv0tCwueBzWHL2mhtbZ6sGWmMWfiTJ2HPpg=="
+ ],
+ "priority": ["100"],
+ "algorithm": ["RSA-OAEP"]
+ }
+ },
+ {
+ "id": "d85dae25-3728-46a0-980b-46171ba50cdd",
+ "name": "aes-generated",
+ "providerId": "aes-generated",
+ "subComponents": {},
+ "config": {
+ "kid": ["1c1d0c8a-6f0b-48a9-a66f-488489137d85"],
+ "secret": ["N4wzheVYYBWxFn9VGWTPQQ"],
+ "priority": ["100"]
+ }
+ },
+ {
+ "id": "8c3bb039-6f5b-4bdc-9faa-e0f6038d9e6b",
+ "name": "hmac-generated-hs512",
+ "providerId": "hmac-generated",
+ "subComponents": {},
+ "config": {
+ "kid": ["ce43821c-6cfd-4ea9-a29a-a724a37e6955"],
+ "secret": [
+ "j_8WeQHYt5R6coay0IOUeu9hGvCoJsgnENSoYm0gDlDx6IHOg-f6p17QIaesNmgrzXtJDRpYMhSjpTMHOnHCHLxwUM4eVg9TcszffndB850Yj3PHPeCc5aoHcpYzWN9NDZZ02nBYA04nfbkdlLXiGlpS3I3e502e4DX3rFtbFZ0"
+ ],
+ "priority": ["100"],
+ "algorithm": ["HS512"]
+ }
+ }
+ ]
+ },
+ "internationalizationEnabled": true,
+ "supportedLocales": ["en", "fr", "es"],
+ "defaultLocale": "en",
+ "authenticationFlows": [
+ {
+ "id": "0e1abbbe-40e3-4754-9fe2-8a7d1f82354e",
+ "alias": "Account verification options",
+ "description": "Method with which to verity the existing account",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-email-verification",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Verify Existing Account by Re-authentication",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "f279cc4d-ebed-4390-a5d4-0cbb6dd662ae",
+ "alias": "Browser - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "6926f455-0fd0-4ac6-9fc1-333b86c4150f",
+ "alias": "Direct Grant - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "b11840e7-21ec-4200-bf3c-c7853646a908",
+ "alias": "First broker login - Conditional OTP",
+ "description": "Flow to determine if the OTP is required for the authentication",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-otp-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "615b4d0e-e71e-4c96-aed3-b03b34b61808",
+ "alias": "Handle Existing Account",
+ "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-confirm-link",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Account verification options",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "36958ec5-62d7-4d51-8b30-7a6709476aec",
+ "alias": "Reset - Conditional OTP",
+ "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "conditional-user-configured",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-otp",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "aa4a7ac2-ec63-48ea-a70f-b3f18992b99a",
+ "alias": "User creation or linking",
+ "description": "Flow for the existing/non-existing user alternatives",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "create unique user config",
+ "authenticator": "idp-create-user-if-unique",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Handle Existing Account",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "dafdfc68-72eb-49b2-a8f4-495ee25fba21",
+ "alias": "Verify Existing Account by Re-authentication",
+ "description": "Reauthentication of existing account",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "idp-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "First broker login - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "6a39b6db-c81e-4de4-92a8-a9e504593f2e",
+ "alias": "browser",
+ "description": "browser based authentication",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-cookie",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "auth-spnego",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "identity-provider-redirector",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 25,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "forms",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "6fa840df-bc04-4045-9e33-8901d183b165",
+ "alias": "clients",
+ "description": "Base authentication for clients",
+ "providerId": "client-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "client-secret",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-secret-jwt",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "client-x509",
+ "authenticatorFlow": false,
+ "requirement": "ALTERNATIVE",
+ "priority": 40,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "4aa24ca0-ad09-4f30-806b-4c699724d731",
+ "alias": "direct grant",
+ "description": "OpenID Connect Resource Owner Grant",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "direct-grant-validate-username",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "direct-grant-validate-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 30,
+ "autheticatorFlow": true,
+ "flowAlias": "Direct Grant - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "0a914ba4-f662-4b85-af64-74738a222b7f",
+ "alias": "docker auth",
+ "description": "Used by Docker clients to authenticate against the IDP",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "docker-http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "9b40f15f-b690-4fe2-9fe8-07e77d965297",
+ "alias": "first broker login",
+ "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticatorConfig": "review profile config",
+ "authenticator": "idp-review-profile",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "User creation or linking",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "c8a9848f-8dd8-4e13-b521-0a537d92ec36",
+ "alias": "forms",
+ "description": "Username, password, otp and other auth forms.",
+ "providerId": "basic-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "auth-username-password-form",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 20,
+ "autheticatorFlow": true,
+ "flowAlias": "Browser - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "603957f8-b0a5-4885-aafd-e2757e431954",
+ "alias": "registration",
+ "description": "registration flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-page-form",
+ "authenticatorFlow": true,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": true,
+ "flowAlias": "registration form",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "f41632f9-7fad-427d-ae7a-78ac9b1f51d0",
+ "alias": "registration form",
+ "description": "registration form",
+ "providerId": "form-flow",
+ "topLevel": false,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "registration-user-creation",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-password-action",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 50,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-recaptcha-action",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 60,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "registration-terms-and-conditions",
+ "authenticatorFlow": false,
+ "requirement": "DISABLED",
+ "priority": 70,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "27a133ca-e05e-4c93-a3b7-ffe14b4e62ec",
+ "alias": "reset credentials",
+ "description": "Reset credentials for a user if they forgot their password or something",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "reset-credentials-choose-user",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-credential-email",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 20,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticator": "reset-password",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 30,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ },
+ {
+ "authenticatorFlow": true,
+ "requirement": "CONDITIONAL",
+ "priority": 40,
+ "autheticatorFlow": true,
+ "flowAlias": "Reset - Conditional OTP",
+ "userSetupAllowed": false
+ }
+ ]
+ },
+ {
+ "id": "06cd7382-4944-4499-94dc-9908544e291b",
+ "alias": "saml ecp",
+ "description": "SAML ECP Profile Authentication Flow",
+ "providerId": "basic-flow",
+ "topLevel": true,
+ "builtIn": true,
+ "authenticationExecutions": [
+ {
+ "authenticator": "http-basic-authenticator",
+ "authenticatorFlow": false,
+ "requirement": "REQUIRED",
+ "priority": 10,
+ "autheticatorFlow": false,
+ "userSetupAllowed": false
+ }
+ ]
+ }
+ ],
+ "authenticatorConfig": [
+ {
+ "id": "5f953def-6f7c-430f-a33f-440ec2d2dddd",
+ "alias": "create unique user config",
+ "config": {
+ "require.password.update.after.registration": "false"
+ }
+ },
+ {
+ "id": "b3dad9a1-5b82-4e91-a250-157a45694e24",
+ "alias": "review profile config",
+ "config": {
+ "update.profile.on.first.login": "missing"
+ }
+ }
+ ],
+ "requiredActions": [
+ {
+ "alias": "CONFIGURE_TOTP",
+ "name": "Configure OTP",
+ "providerId": "CONFIGURE_TOTP",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 10,
+ "config": {}
+ },
+ {
+ "alias": "TERMS_AND_CONDITIONS",
+ "name": "Terms and Conditions",
+ "providerId": "TERMS_AND_CONDITIONS",
+ "enabled": true,
+ "defaultAction": true,
+ "priority": 20,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PASSWORD",
+ "name": "Update Password",
+ "providerId": "UPDATE_PASSWORD",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 30,
+ "config": {}
+ },
+ {
+ "alias": "UPDATE_PROFILE",
+ "name": "Update Profile",
+ "providerId": "UPDATE_PROFILE",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 40,
+ "config": {}
+ },
+ {
+ "alias": "VERIFY_EMAIL",
+ "name": "Verify Email",
+ "providerId": "VERIFY_EMAIL",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 50,
+ "config": {}
+ },
+ {
+ "alias": "delete_account",
+ "name": "Delete Account",
+ "providerId": "delete_account",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 60,
+ "config": {}
+ },
+ {
+ "alias": "webauthn-register",
+ "name": "Webauthn Register",
+ "providerId": "webauthn-register",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 70,
+ "config": {}
+ },
+ {
+ "alias": "webauthn-register-passwordless",
+ "name": "Webauthn Register Passwordless",
+ "providerId": "webauthn-register-passwordless",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 80,
+ "config": {}
+ },
+ {
+ "alias": "VERIFY_PROFILE",
+ "name": "Verify Profile",
+ "providerId": "VERIFY_PROFILE",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 90,
+ "config": {}
+ },
+ {
+ "alias": "delete_credential",
+ "name": "Delete Credential",
+ "providerId": "delete_credential",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 100,
+ "config": {}
+ },
+ {
+ "alias": "update_user_locale",
+ "name": "Update User Locale",
+ "providerId": "update_user_locale",
+ "enabled": true,
+ "defaultAction": false,
+ "priority": 1000,
+ "config": {}
+ }
+ ],
+ "browserFlow": "browser",
+ "registrationFlow": "registration",
+ "directGrantFlow": "direct grant",
+ "resetCredentialsFlow": "reset credentials",
+ "clientAuthenticationFlow": "clients",
+ "dockerAuthenticationFlow": "docker auth",
+ "firstBrokerLoginFlow": "first broker login",
+ "attributes": {
+ "cibaBackchannelTokenDeliveryMode": "poll",
+ "cibaAuthRequestedUserHint": "login_hint",
+ "clientOfflineSessionMaxLifespan": "0",
+ "oauth2DevicePollingInterval": "5",
+ "clientSessionIdleTimeout": "0",
+ "clientOfflineSessionIdleTimeout": "0",
+ "cibaInterval": "5",
+ "realmReusableOtpCode": "false",
+ "cibaExpiresIn": "120",
+ "oauth2DeviceCodeLifespan": "600",
+ "parRequestUriLifespan": "60",
+ "clientSessionMaxLifespan": "0",
+ "organizationsEnabled": "false"
+ },
+ "keycloakVersion": "25.0.0",
+ "userManagedAccessAllowed": false,
+ "organizationsEnabled": false,
+ "clientProfiles": {
+ "profiles": []
+ },
+ "clientPolicies": {
+ "policies": []
+ }
+}
From e573aff6ae9462e8e50113405bf78cd10ed26dd6 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:17:54 +0200
Subject: [PATCH 25/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index ff7fbe2a..fe130725 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.8",
+ "version": "11.2.9-rc.0",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 4de9e059e97a590c09c7ceb522daf3c167b39c56 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:44:03 +0200
Subject: [PATCH 26/82] Aditional context exclusion
---
.../keycloakify/generateFtl/kcContextDeclarationTemplate.ftl | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
index 5cfc2755..4139df7c 100644
--- a/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
+++ b/src/bin/keycloakify/generateFtl/kcContextDeclarationTemplate.ftl
@@ -235,6 +235,9 @@ function decodeHtmlEntities(htmlStr){
"identityFederationEnabled",
"userManagedAccessAllowed"
]?seq_contains(key)
+ ) || (
+ ["flowContext", "session", "realm"]?seq_contains(key) &&
+ areSamePath(path, ["social"])
)
>
<#-- <#local outSeq += ["/*" + path?join(".") + "." + key + " excluded*/"]> -->
From d5519dbb550f024866d4255954e344f4f4eec846 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:44:22 +0200
Subject: [PATCH 27/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index fe130725..f6a41f99 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.9-rc.0",
+ "version": "11.2.9-rc.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 290ad8b5929a2a54681961fd24ae09d890b24aff Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:58:31 +0200
Subject: [PATCH 28/82] Update version ranges for Multi-Page account theme
---
.../buildJars/getKeycloakVersionRangeForJar.ts | 4 ++--
src/bin/shared/KeycloakVersionRange.ts | 2 +-
src/bin/shared/buildContext.ts | 12 +++++++++---
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts b/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
index 6c301223..89c42ef7 100644
--- a/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
+++ b/src/bin/keycloakify/buildJars/getKeycloakVersionRangeForJar.ts
@@ -52,9 +52,9 @@ export function getKeycloakVersionRangeForJar(params: {
case "0.6":
switch (keycloakThemeAdditionalInfoExtensionVersion) {
case null:
- return undefined;
+ return "26-and-above" as const;
case "1.1.5":
- return "25-and-above" as const;
+ return "25" as const;
}
}
assert>(false);
diff --git a/src/bin/shared/KeycloakVersionRange.ts b/src/bin/shared/KeycloakVersionRange.ts
index 1e118ea1..860f42f0 100644
--- a/src/bin/shared/KeycloakVersionRange.ts
+++ b/src/bin/shared/KeycloakVersionRange.ts
@@ -5,5 +5,5 @@ export type KeycloakVersionRange =
export namespace KeycloakVersionRange {
export type WithoutAccountV1Theme = "22-to-25" | "all-other-versions";
- export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25-and-above";
+ export type WithAccountV1Theme = "21-and-below" | "23" | "24" | "25" | "26-and-above";
}
diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts
index 852bd1fd..5fc09c3a 100644
--- a/src/bin/shared/buildContext.ts
+++ b/src/bin/shared/buildContext.ts
@@ -277,7 +277,8 @@ export function getBuildContext(params: {
"21-and-below": z.union([z.boolean(), z.string()]),
"23": z.union([z.boolean(), z.string()]),
"24": z.union([z.boolean(), z.string()]),
- "25-and-above": z.union([z.boolean(), z.string()])
+ "25": z.union([z.boolean(), z.string()]),
+ "26-and-above": z.union([z.boolean(), z.string()])
})
.optional()
});
@@ -767,7 +768,11 @@ export function getBuildContext(params: {
return "24" as const;
}
- return "25-and-above" as const;
+ if (buildForKeycloakMajorVersionNumber === 25) {
+ return "25" as const;
+ }
+
+ return "26-and-above" as const;
})();
assert<
@@ -855,7 +860,8 @@ export function getBuildContext(params: {
"21-and-below",
"23",
"24",
- "25-and-above"
+ "25",
+ "26-and-above"
] as const) {
assert<
Equals<
From 22b0b95e544e2fbd300b522b0a235bab1a661d0c Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 12:59:56 +0200
Subject: [PATCH 29/82] Update readme, support keycloak 26
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 78872df3..2c133223 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,7 @@
-Keycloakify is fully compatible with Keycloak from version 11 to 25...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
+Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
## Sponsors
From ab43bb73d7c58c8fe295584670abe2ee1d73f4ff Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 13:00:15 +0200
Subject: [PATCH 30/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index f6a41f99..9a704e0e 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.9-rc.1",
+ "version": "11.2.9",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From dfe2e1562a1e01a472008c970c06d495b174d93d Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 16:56:02 +0200
Subject: [PATCH 31/82] Fix cache issue
---
src/bin/keycloakify/buildJars/buildJar.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/bin/keycloakify/buildJars/buildJar.ts b/src/bin/keycloakify/buildJars/buildJar.ts
index cdbc3f22..4a952343 100644
--- a/src/bin/keycloakify/buildJars/buildJar.ts
+++ b/src/bin/keycloakify/buildJars/buildJar.ts
@@ -197,7 +197,7 @@ export async function buildJar(params: {
await new Promise((resolve, reject) =>
child_process.exec(
- `mvn install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
+ `mvn clean install -Dmaven.repo.local="${pathJoin(keycloakifyBuildCacheDirPath, ".m2")}"`,
{ cwd: keycloakifyBuildCacheDirPath },
error => {
if (error !== null) {
From ca6accc8891daa16a897d236326693fecd694d85 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 4 Oct 2024 16:56:17 +0200
Subject: [PATCH 32/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 9a704e0e..0564ddf0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.9",
+ "version": "11.2.10",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 9e41868e0d79a3c7ecc31ce6be2fc411f1c79672 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 20:30:09 +0200
Subject: [PATCH 33/82] Implement custom handler cli hook
---
package.json | 1 +
src/bin/add-story.ts | 11 +-
src/bin/copy-keycloak-resources-to-public.ts | 9 +-
src/bin/eject-page.ts | 11 +-
.../initialize-account-theme.ts | 9 +-
src/bin/initialize-email-theme.ts | 9 +-
src/bin/keycloakify/keycloakify.ts | 9 +-
src/bin/main.ts | 297 +++++++++++-------
src/bin/shared/buildContext.ts | 9 +-
src/bin/shared/constants.ts | 5 +
src/bin/shared/customHandler.ts | 35 +++
src/bin/shared/customHandler_caller.ts | 47 +++
src/bin/start-keycloak/start-keycloak.ts | 22 +-
src/bin/update-kc-gen.ts | 11 +-
src/vite-plugin/vite-plugin.ts | 4 +-
15 files changed, 300 insertions(+), 189 deletions(-)
create mode 100644 src/bin/shared/customHandler.ts
create mode 100644 src/bin/shared/customHandler_caller.ts
diff --git a/package.json b/package.json
index 0564ddf0..70ca8dae 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"dist/bin/*.index.js",
"dist/bin/*.node",
"dist/bin/shared/constants.js",
+ "dist/bin/shared/customHandler.js",
"dist/bin/shared/*.d.ts",
"dist/bin/shared/*.js.map",
"!dist/vite-plugin/",
diff --git a/src/bin/add-story.ts b/src/bin/add-story.ts
index b3ed5be7..b2be6d65 100644
--- a/src/bin/add-story.ts
+++ b/src/bin/add-story.ts
@@ -13,16 +13,11 @@ import * as fs from "fs";
import { join as pathJoin, relative as pathRelative, dirname as pathDirname } from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
-import type { CliCommandOptions } from "./main";
-import { getBuildContext } from "./shared/buildContext";
+import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({
- cliCommandOptions
- });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
console.log(chalk.cyan("Theme type:"));
diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts
index b245076a..ba944068 100644
--- a/src/bin/copy-keycloak-resources-to-public.ts
+++ b/src/bin/copy-keycloak-resources-to-public.ts
@@ -1,11 +1,8 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
-import { getBuildContext } from "./shared/buildContext";
-import type { CliCommandOptions } from "./main";
+import type { BuildContext } from "./shared/buildContext";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({ cliCommandOptions });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
copyKeycloakResourcesToPublic({
buildContext
diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts
index 404483ec..8874fd99 100644
--- a/src/bin/eject-page.ts
+++ b/src/bin/eject-page.ts
@@ -20,16 +20,11 @@ import {
} from "path";
import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
-import type { CliCommandOptions } from "./main";
-import { getBuildContext } from "./shared/buildContext";
+import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({
- cliCommandOptions
- });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
console.log(chalk.cyan("Theme type:"));
diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts
index e15b95ba..e29b6733 100644
--- a/src/bin/initialize-account-theme/initialize-account-theme.ts
+++ b/src/bin/initialize-account-theme/initialize-account-theme.ts
@@ -1,5 +1,4 @@
-import { getBuildContext } from "../shared/buildContext";
-import type { CliCommandOptions } from "../main";
+import type { BuildContext } from "../shared/buildContext";
import cliSelect from "cli-select";
import child_process from "child_process";
import chalk from "chalk";
@@ -8,10 +7,8 @@ import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { generateKcGenTs } from "../shared/generateKcGenTs";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({ cliCommandOptions });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index f2520645..dfa0287c 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -1,15 +1,12 @@
import { join as pathJoin, relative as pathRelative } from "path";
import { transformCodebase } from "./tools/transformCodebase";
import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
-import { getBuildContext } from "./shared/buildContext";
+import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
-import type { CliCommandOptions } from "./main";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({ cliCommandOptions });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
diff --git a/src/bin/keycloakify/keycloakify.ts b/src/bin/keycloakify/keycloakify.ts
index b39dcad8..0d027103 100644
--- a/src/bin/keycloakify/keycloakify.ts
+++ b/src/bin/keycloakify/keycloakify.ts
@@ -2,19 +2,16 @@ import { generateResources } from "./generateResources";
import { join as pathJoin, relative as pathRelative, sep as pathSep } from "path";
import * as child_process from "child_process";
import * as fs from "fs";
-import { getBuildContext } from "../shared/buildContext";
+import type { BuildContext } from "../shared/buildContext";
import { VITE_PLUGIN_SUB_SCRIPTS_ENV_NAMES } from "../shared/constants";
import { buildJars } from "./buildJars";
-import type { CliCommandOptions } from "../main";
import chalk from "chalk";
import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
import * as os from "os";
import { rmSync } from "../tools/fs.rmSync";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({ cliCommandOptions });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
exit_if_maven_not_installed: {
let commandOutput: Buffer | undefined = undefined;
diff --git a/src/bin/main.ts b/src/bin/main.ts
index 9f54242f..7ef2a9b8 100644
--- a/src/bin/main.ts
+++ b/src/bin/main.ts
@@ -4,8 +4,10 @@ import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
+import { callHandlerIfAny } from "./shared/customHandler_caller";
+import { getBuildContext } from "./shared/buildContext";
-export type CliCommandOptions = {
+type CliCommandOptions = {
projectDirPath: string | undefined;
};
@@ -69,115 +71,154 @@ program
})
.task({
skip,
- handler: async cliCommandOptions => {
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
+
const { command } = await import("./keycloakify");
- await command({ cliCommandOptions });
+ await command({ buildContext });
}
});
-program
- .command<{
- port: number | undefined;
- keycloakVersion: string | undefined;
- realmJsonFilePath: string | undefined;
- }>({
- name: "start-keycloak",
- description:
- "Spin up a pre configured Docker image of Keycloak to test your theme."
- })
- .option({
- key: "port",
- name: (() => {
- const name = "port";
+{
+ const commandName = "start-keycloak";
- optionsKeys.push(name);
+ program
+ .command<{
+ port: number | undefined;
+ keycloakVersion: string | undefined;
+ realmJsonFilePath: string | undefined;
+ }>({
+ name: commandName,
+ description:
+ "Spin up a pre configured Docker image of Keycloak to test your theme."
+ })
+ .option({
+ key: "port",
+ name: (() => {
+ const name = "port";
- return name;
- })(),
- description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
- defaultValue: undefined
- })
- .option({
- key: "keycloakVersion",
- name: (() => {
- const name = "keycloak-version";
+ optionsKeys.push(name);
- optionsKeys.push(name);
+ return name;
+ })(),
+ description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
+ defaultValue: undefined
+ })
+ .option({
+ key: "keycloakVersion",
+ name: (() => {
+ const name = "keycloak-version";
- return name;
- })(),
- description: [
- "Use a specific version of Keycloak.",
- "Example `--keycloak-version 21.1.1`"
- ].join(" "),
- defaultValue: undefined
- })
- .option({
- key: "realmJsonFilePath",
- name: (() => {
- const name = "import";
+ optionsKeys.push(name);
- optionsKeys.push(name);
+ return name;
+ })(),
+ description: [
+ "Use a specific version of Keycloak.",
+ "Example `--keycloak-version 21.1.1`"
+ ].join(" "),
+ defaultValue: undefined
+ })
+ .option({
+ key: "realmJsonFilePath",
+ name: (() => {
+ const name = "import";
- return name;
- })(),
- defaultValue: undefined,
- description: [
- "Import your own realm configuration file",
- "Example `--import path/to/myrealm-realm.json`"
- ].join(" ")
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./start-keycloak");
+ optionsKeys.push(name);
- await command({ cliCommandOptions });
- }
- });
+ return name;
+ })(),
+ defaultValue: undefined,
+ description: [
+ "Import your own realm configuration file",
+ "Example `--import path/to/myrealm-realm.json`"
+ ].join(" ")
+ })
+ .task({
+ skip,
+ handler: async ({
+ projectDirPath,
+ keycloakVersion,
+ port,
+ realmJsonFilePath
+ }) => {
+ const buildContext = getBuildContext({ projectDirPath });
-program
- .command({
- name: "eject-page",
- description: "Eject a Keycloak page."
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./eject-page");
+ const { command } = await import("./start-keycloak");
- await command({ cliCommandOptions });
- }
- });
+ await command({
+ buildContext,
+ cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
+ });
+ }
+ });
+}
-program
- .command({
- name: "add-story",
- description: "Add *.stories.tsx file for a specific page to in your Storybook."
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./add-story");
+{
+ const commandName = "eject-page";
- await command({ cliCommandOptions });
- }
- });
+ program
+ .command({
+ name: commandName,
+ description: "Eject a Keycloak page."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
-program
- .command({
- name: "initialize-email-theme",
- description: "Initialize an email theme."
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./initialize-email-theme");
+ callHandlerIfAny({ buildContext, commandName });
- await command({ cliCommandOptions });
- }
- });
+ const { command } = await import("./eject-page");
+
+ await command({ buildContext });
+ }
+ });
+}
+
+{
+ const commandName = "add-story";
+
+ program
+ .command({
+ name: commandName,
+ description:
+ "Add *.stories.tsx file for a specific page to in your Storybook."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
+
+ callHandlerIfAny({ buildContext, commandName });
+
+ const { command } = await import("./add-story");
+
+ await command({ buildContext });
+ }
+ });
+}
+
+{
+ const comandName = "initialize-login-theme";
+
+ program
+ .command({
+ name: comandName,
+ description: "Initialize an email theme."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
+
+ const { command } = await import("./initialize-email-theme");
+
+ await command({ buildContext });
+ }
+ });
+}
program
.command({
@@ -186,42 +227,58 @@ program
})
.task({
skip,
- handler: async cliCommandOptions => {
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
+
const { command } = await import("./initialize-account-theme");
- await command({ cliCommandOptions });
+ await command({ buildContext });
}
});
-program
- .command({
- name: "copy-keycloak-resources-to-public",
- description:
- "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./copy-keycloak-resources-to-public");
+{
+ const commandName = "copy-keycloak-resources-to-public";
- await command({ cliCommandOptions });
- }
- });
+ program
+ .command({
+ name: commandName,
+ description:
+ "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
-program
- .command({
- name: "update-kc-gen",
- description:
- "(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
- })
- .task({
- skip,
- handler: async cliCommandOptions => {
- const { command } = await import("./update-kc-gen");
+ const { command } = await import("./copy-keycloak-resources-to-public");
- await command({ cliCommandOptions });
- }
- });
+ await command({ buildContext });
+ }
+ });
+}
+
+{
+ const commandName = "update-kc-gen";
+
+ program
+ .command({
+ name: commandName,
+ description:
+ "(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const buildContext = getBuildContext({ projectDirPath });
+
+ callHandlerIfAny({ buildContext, commandName });
+
+ const { command } = await import("./update-kc-gen");
+
+ await command({ buildContext });
+ }
+ });
+}
// Fallback to build command if no command is provided
{
diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts
index 5fc09c3a..fa658097 100644
--- a/src/bin/shared/buildContext.ts
+++ b/src/bin/shared/buildContext.ts
@@ -7,7 +7,6 @@ import {
dirname as pathDirname
} from "path";
import { getAbsoluteAndInOsFormatPath } from "../tools/getAbsoluteAndInOsFormatPath";
-import type { CliCommandOptions } from "../main";
import { z } from "zod";
import * as fs from "fs";
import { assert, type Equals } from "tsafe/assert";
@@ -129,14 +128,12 @@ export type ResolvedViteConfig = {
};
export function getBuildContext(params: {
- cliCommandOptions: CliCommandOptions;
+ projectDirPath: string | undefined;
}): BuildContext {
- const { cliCommandOptions } = params;
-
const projectDirPath =
- cliCommandOptions.projectDirPath !== undefined
+ params.projectDirPath !== undefined
? getAbsoluteAndInOsFormatPath({
- pathIsh: cliCommandOptions.projectDirPath,
+ pathIsh: params.projectDirPath,
cwd: process.cwd()
})
: process.cwd();
diff --git a/src/bin/shared/constants.ts b/src/bin/shared/constants.ts
index d0707a90..d2137b14 100644
--- a/src/bin/shared/constants.ts
+++ b/src/bin/shared/constants.ts
@@ -71,3 +71,8 @@ export type AccountThemePageId = (typeof ACCOUNT_THEME_PAGE_IDS)[number];
export const CONTAINER_NAME = "keycloak-keycloakify";
export const FALLBACK_LANGUAGE_TAG = "en";
+
+export const CUSTOM_HANDLER_ENV_NAMES = {
+ COMMAND_NAME: "KEYCLOAKIFY_COMMAND_NAME",
+ BUILD_CONTEXT: "KEYCLOAKIFY_BUILD_CONTEXT"
+};
diff --git a/src/bin/shared/customHandler.ts b/src/bin/shared/customHandler.ts
new file mode 100644
index 00000000..d858aaa5
--- /dev/null
+++ b/src/bin/shared/customHandler.ts
@@ -0,0 +1,35 @@
+import { assert } from "tsafe/assert";
+import type { BuildContext } from "./buildContext";
+import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
+
+export const BIN_NAME = "_keycloakify-custom-handler";
+
+export const NOT_IMPLEMENTED_EXIT_CODE = 78;
+
+export type CommandName = "update-kc-gen" | "eject-page" | "add-story";
+
+export type ApiVersion = "v1";
+
+export function readParams(params: { apiVersion: ApiVersion }) {
+ const { apiVersion } = params;
+
+ assert(apiVersion === "v1");
+
+ const commandName = (() => {
+ const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME];
+
+ assert(envValue !== undefined);
+
+ return envValue as CommandName;
+ })();
+
+ const buildContext = (() => {
+ const envValue = process.env[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT];
+
+ assert(envValue !== undefined);
+
+ return JSON.parse(envValue) as BuildContext;
+ })();
+
+ return { commandName, buildContext };
+}
diff --git a/src/bin/shared/customHandler_caller.ts b/src/bin/shared/customHandler_caller.ts
new file mode 100644
index 00000000..38f25972
--- /dev/null
+++ b/src/bin/shared/customHandler_caller.ts
@@ -0,0 +1,47 @@
+import { assert, type Equals } from "tsafe/assert";
+import type { BuildContext } from "./buildContext";
+import { CUSTOM_HANDLER_ENV_NAMES } from "./constants";
+import {
+ NOT_IMPLEMENTED_EXIT_CODE,
+ type CommandName,
+ BIN_NAME,
+ ApiVersion
+} from "./customHandler";
+import * as child_process from "child_process";
+import { is } from "tsafe/is";
+import { dirname as pathDirname } from "path";
+import * as fs from "fs";
+
+assert>();
+
+export function callHandlerIfAny(params: {
+ commandName: CommandName;
+ buildContext: BuildContext;
+}) {
+ const { commandName, buildContext } = params;
+
+ if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
+ return;
+ }
+
+ try {
+ child_process.execSync(`npx ${BIN_NAME}`, {
+ stdio: "inherit",
+ env: {
+ ...process.env,
+ [CUSTOM_HANDLER_ENV_NAMES.COMMAND_NAME]: commandName,
+ [CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
+ }
+ });
+ } catch (error) {
+ assert(is(error));
+
+ if (error.code === NOT_IMPLEMENTED_EXIT_CODE) {
+ return;
+ }
+
+ process.exit(error.code);
+ }
+
+ process.exit(0);
+}
diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts
index c85b09dc..e243fe2b 100644
--- a/src/bin/start-keycloak/start-keycloak.ts
+++ b/src/bin/start-keycloak/start-keycloak.ts
@@ -1,6 +1,5 @@
-import { getBuildContext } from "../shared/buildContext";
+import type { BuildContext } from "../shared/buildContext";
import { exclude } from "tsafe/exclude";
-import type { CliCommandOptions as CliCommandOptions_common } from "../main";
import { promptKeycloakVersion } from "../shared/promptKeycloakVersion";
import { CONTAINER_NAME } from "../shared/constants";
import { SemVer } from "../tools/SemVer";
@@ -29,13 +28,14 @@ import { existsAsync } from "../tools/fs.existsAsync";
import { rm } from "../tools/fs.rm";
import { downloadAndExtractArchive } from "../tools/downloadAndExtractArchive";
-export type CliCommandOptions = CliCommandOptions_common & {
- port: number | undefined;
- keycloakVersion: string | undefined;
- realmJsonFilePath: string | undefined;
-};
-
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
+export async function command(params: {
+ buildContext: BuildContext;
+ cliCommandOptions: {
+ port: number | undefined;
+ keycloakVersion: string | undefined;
+ realmJsonFilePath: string | undefined;
+ };
+}) {
exit_if_docker_not_installed: {
let commandOutput: string | undefined = undefined;
@@ -88,9 +88,7 @@ export async function command(params: { cliCommandOptions: CliCommandOptions })
process.exit(1);
}
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({ cliCommandOptions });
+ const { cliCommandOptions, buildContext } = params;
const { dockerImageTag } = await (async () => {
if (cliCommandOptions.keycloakVersion !== undefined) {
diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts
index 585b0f22..b3d9ce15 100644
--- a/src/bin/update-kc-gen.ts
+++ b/src/bin/update-kc-gen.ts
@@ -1,13 +1,8 @@
-import type { CliCommandOptions } from "./main";
-import { getBuildContext } from "./shared/buildContext";
+import type { BuildContext } from "./shared/buildContext";
import { generateKcGenTs } from "./shared/generateKcGenTs";
-export async function command(params: { cliCommandOptions: CliCommandOptions }) {
- const { cliCommandOptions } = params;
-
- const buildContext = getBuildContext({
- cliCommandOptions
- });
+export async function command(params: { buildContext: BuildContext }) {
+ const { buildContext } = params;
await generateKcGenTs({ buildContext });
}
diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts
index f22cb62a..3a7f3d26 100644
--- a/src/vite-plugin/vite-plugin.ts
+++ b/src/vite-plugin/vite-plugin.ts
@@ -122,9 +122,7 @@ export function keycloakify(params: keycloakify.Params) {
}
const buildContext = getBuildContext({
- cliCommandOptions: {
- projectDirPath
- }
+ projectDirPath
});
copyKeycloakResourcesToPublic({ buildContext }),
From ffd405c6dbc378ddf2d992d73d9d3a3caaec8959 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 20:31:41 +0200
Subject: [PATCH 34/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 70ca8dae..bebb3ed3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.2.10",
+ "version": "11.3.0-rc.0",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From fe65ddb5f89dc8e60765653dec1521d32eb44218 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 21:22:00 +0200
Subject: [PATCH 35/82] Fix missing exports
---
package.json | 2 +-
src/bin/shared/buildContext.ts | 4 ++--
src/bin/tools/fetchProxyOptions.ts | 14 ++++++++------
3 files changed, 11 insertions(+), 9 deletions(-)
diff --git a/package.json b/package.json
index bebb3ed3..9a799b8f 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"dist/bin/*.node",
"dist/bin/shared/constants.js",
"dist/bin/shared/customHandler.js",
- "dist/bin/shared/*.d.ts",
+ "dist/bin/*.d.ts",
"dist/bin/shared/*.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.js",
diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts
index fa658097..26c746b8 100644
--- a/src/bin/shared/buildContext.ts
+++ b/src/bin/shared/buildContext.ts
@@ -23,7 +23,7 @@ import { objectEntries } from "tsafe/objectEntries";
import { type ThemeType } from "./constants";
import { id } from "tsafe/id";
import chalk from "chalk";
-import { getProxyFetchOptions, type ProxyFetchOptions } from "../tools/fetchProxyOptions";
+import { getProxyFetchOptions, type FetchOptionsLike } from "../tools/fetchProxyOptions";
import { is } from "tsafe/is";
export type BuildContext = {
@@ -42,7 +42,7 @@ export type BuildContext = {
* In this case the urlPathname will be "/my-app/" */
urlPathname: string | undefined;
assetsDirPath: string;
- fetchOptions: ProxyFetchOptions;
+ fetchOptions: FetchOptionsLike;
kcContextExclusionsFtlCode: string | undefined;
environmentVariables: { name: string; default: string }[];
themeSrcDirPath: string;
diff --git a/src/bin/tools/fetchProxyOptions.ts b/src/bin/tools/fetchProxyOptions.ts
index e6f8f497..c607feb9 100644
--- a/src/bin/tools/fetchProxyOptions.ts
+++ b/src/bin/tools/fetchProxyOptions.ts
@@ -1,16 +1,18 @@
-import { type FetchOptions } from "make-fetch-happen";
import * as child_process from "child_process";
import * as fs from "fs";
import { exclude } from "tsafe/exclude";
-export type ProxyFetchOptions = Pick<
- FetchOptions,
- "proxy" | "noProxy" | "strictSSL" | "cert" | "ca"
->;
+export type FetchOptionsLike = {
+ proxy: string | undefined;
+ noProxy: string | string[];
+ strictSSL: boolean;
+ cert: string | string[] | undefined;
+ ca: string[] | undefined;
+};
export function getProxyFetchOptions(params: {
npmConfigGetCwd: string;
-}): ProxyFetchOptions {
+}): FetchOptionsLike {
const { npmConfigGetCwd } = params;
const cfg = (() => {
From e92562fd44db2524a496a7972a810c8877c6f8d9 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 21:23:57 +0200
Subject: [PATCH 36/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 9a799b8f..39c31f79 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.0",
+ "version": "11.3.0-rc.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From ef6f5a4c23861df297812fe3533770eebd28b353 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 21:39:14 +0200
Subject: [PATCH 37/82] Add other missing declaration files
---
package.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index 39c31f79..542b7aff 100644
--- a/package.json
+++ b/package.json
@@ -38,13 +38,14 @@
"dist/",
"!dist/tsconfig.tsbuildinfo",
"!dist/bin/",
+ "dist/bin/**/*.d.ts",
"dist/bin/main.js",
"dist/bin/*.index.js",
"dist/bin/*.node",
"dist/bin/shared/constants.js",
+ "dist/bin/shared/constants.js.map",
"dist/bin/shared/customHandler.js",
- "dist/bin/*.d.ts",
- "dist/bin/shared/*.js.map",
+ "dist/bin/shared/customHandler.js.map",
"!dist/vite-plugin/",
"dist/vite-plugin/index.js",
"dist/vite-plugin/index.d.ts",
From 49b064b5f294b2dfb6781f58928f9dfb5711b172 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 21:39:32 +0200
Subject: [PATCH 38/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 542b7aff..a472aa99 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.1",
+ "version": "11.3.0-rc.2",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 9e9ffcd586601ba5927d404c5cb3768cf9156c51 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 22:28:36 +0200
Subject: [PATCH 39/82] add debug logs
---
src/bin/main.ts | 4 ++++
src/bin/shared/customHandler_caller.ts | 5 ++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/bin/main.ts b/src/bin/main.ts
index 7ef2a9b8..836bda9b 100644
--- a/src/bin/main.ts
+++ b/src/bin/main.ts
@@ -168,8 +168,12 @@ program
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
+ console.log("before callHandlerIfAny");
+
callHandlerIfAny({ buildContext, commandName });
+ console.log("after callHandlerIfAny");
+
const { command } = await import("./eject-page");
await command({ buildContext });
diff --git a/src/bin/shared/customHandler_caller.ts b/src/bin/shared/customHandler_caller.ts
index 38f25972..c4a49e7a 100644
--- a/src/bin/shared/customHandler_caller.ts
+++ b/src/bin/shared/customHandler_caller.ts
@@ -33,7 +33,10 @@ export function callHandlerIfAny(params: {
[CUSTOM_HANDLER_ENV_NAMES.BUILD_CONTEXT]: JSON.stringify(buildContext)
}
});
- } catch (error) {
+ } catch (error: any) {
+ console.log(error.message);
+ console.log(error.status);
+
assert(is(error));
if (error.code === NOT_IMPLEMENTED_EXIT_CODE) {
From 8fc307bd8d6c85737c48b14db72e3e2ad479b4d8 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 5 Oct 2024 22:29:13 +0200
Subject: [PATCH 40/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index a472aa99..d6ffdaf8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.2",
+ "version": "11.3.0-rc.3",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From c4ee6cd85c98472b13f7306e02dedfd8199e66a4 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 06:41:51 +0200
Subject: [PATCH 41/82] Fix not handling correctly exit cause
---
src/bin/main.ts | 4 ----
src/bin/shared/customHandler_caller.ts | 10 +++-------
2 files changed, 3 insertions(+), 11 deletions(-)
diff --git a/src/bin/main.ts b/src/bin/main.ts
index 836bda9b..7ef2a9b8 100644
--- a/src/bin/main.ts
+++ b/src/bin/main.ts
@@ -168,12 +168,8 @@ program
handler: async ({ projectDirPath }) => {
const buildContext = getBuildContext({ projectDirPath });
- console.log("before callHandlerIfAny");
-
callHandlerIfAny({ buildContext, commandName });
- console.log("after callHandlerIfAny");
-
const { command } = await import("./eject-page");
await command({ buildContext });
diff --git a/src/bin/shared/customHandler_caller.ts b/src/bin/shared/customHandler_caller.ts
index c4a49e7a..88d5a41c 100644
--- a/src/bin/shared/customHandler_caller.ts
+++ b/src/bin/shared/customHandler_caller.ts
@@ -8,7 +8,6 @@ import {
ApiVersion
} from "./customHandler";
import * as child_process from "child_process";
-import { is } from "tsafe/is";
import { dirname as pathDirname } from "path";
import * as fs from "fs";
@@ -34,16 +33,13 @@ export function callHandlerIfAny(params: {
}
});
} catch (error: any) {
- console.log(error.message);
- console.log(error.status);
+ const status = error.status;
- assert(is(error));
-
- if (error.code === NOT_IMPLEMENTED_EXIT_CODE) {
+ if (status === NOT_IMPLEMENTED_EXIT_CODE) {
return;
}
- process.exit(error.code);
+ process.exit(status);
}
process.exit(0);
From fed6af4dfed482e67e5df36a387be526af3ba60c Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 06:42:04 +0200
Subject: [PATCH 42/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index d6ffdaf8..eb36617a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.3",
+ "version": "11.3.0-rc.4",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 2dfb4eda9d34109114870ac2392d0c2fca5b5185 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 06:44:53 +0200
Subject: [PATCH 43/82] No need to handle non react environement with custom
handler support
---
src/bin/shared/generateKcGenTs.ts | 104 ++++++++----------------------
1 file changed, 28 insertions(+), 76 deletions(-)
diff --git a/src/bin/shared/generateKcGenTs.ts b/src/bin/shared/generateKcGenTs.ts
index 7191a55c..e5bb7e86 100644
--- a/src/bin/shared/generateKcGenTs.ts
+++ b/src/bin/shared/generateKcGenTs.ts
@@ -1,10 +1,8 @@
-import { assert, type Equals } from "tsafe/assert";
-import { id } from "tsafe/id";
+import { assert } from "tsafe/assert";
import type { BuildContext } from "./buildContext";
import * as fs from "fs/promises";
import { join as pathJoin } from "path";
import { existsAsync } from "../tools/fs.existsAsync";
-import { z } from "zod";
export type BuildContextLike = {
projectDirPath: string;
@@ -25,45 +23,7 @@ export async function generateKcGenTs(params: {
}): Promise {
const { buildContext } = params;
- const isReactProject: boolean = await (async () => {
- const parsedPackageJson = await (async () => {
- type ParsedPackageJson = {
- dependencies?: Record;
- devDependencies?: Record;
- };
-
- const zParsedPackageJson = (() => {
- type TargetType = ParsedPackageJson;
-
- const zTargetType = z.object({
- dependencies: z.record(z.string()).optional(),
- devDependencies: z.record(z.string()).optional()
- });
-
- assert, TargetType>>();
-
- return id>(zTargetType);
- })();
-
- return zParsedPackageJson.parse(
- JSON.parse(
- (await fs.readFile(buildContext.packageJsonFilePath)).toString("utf8")
- )
- );
- })();
-
- return (
- {
- ...parsedPackageJson.dependencies,
- ...parsedPackageJson.devDependencies
- }.react !== undefined
- );
- })();
-
- const filePath = pathJoin(
- buildContext.themeSrcDirPath,
- `kc.gen.ts${isReactProject ? "x" : ""}`
- );
+ const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))
? await fs.readFile(filePath)
@@ -84,7 +44,7 @@ export async function generateKcGenTs(params: {
``,
`// This file is auto-generated by Keycloakify`,
``,
- isReactProject && `import { lazy, Suspense, type ReactNode } from "react";`,
+ `import { lazy, Suspense, type ReactNode } from "react";`,
``,
`export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
``,
@@ -115,35 +75,31 @@ export async function generateKcGenTs(params: {
` }`,
`}`,
``,
- ...(!isReactProject
- ? []
- : [
- hasLoginTheme &&
- `export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
- hasAccountTheme &&
- `export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
- ``,
- `export function KcPage(`,
- ` props: {`,
- ` kcContext: KcContext;`,
- ` fallback?: ReactNode;`,
- ` }`,
- `) {`,
- ` const { kcContext, fallback } = props;`,
- ` return (`,
- ` `,
- ` {(() => {`,
- ` switch (kcContext.themeType) {`,
- hasLoginTheme &&
- ` case "login": return ;`,
- hasAccountTheme &&
- ` case "account": return ;`,
- ` }`,
- ` })()}`,
- ` `,
- ` );`,
- `}`
- ]),
+ hasLoginTheme &&
+ `export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
+ hasAccountTheme &&
+ `export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
+ ``,
+ `export function KcPage(`,
+ ` props: {`,
+ ` kcContext: KcContext;`,
+ ` fallback?: ReactNode;`,
+ ` }`,
+ `) {`,
+ ` const { kcContext, fallback } = props;`,
+ ` return (`,
+ ` `,
+ ` {(() => {`,
+ ` switch (kcContext.themeType) {`,
+ hasLoginTheme &&
+ ` case "login": return ;`,
+ hasAccountTheme &&
+ ` case "account": return ;`,
+ ` }`,
+ ` })()}`,
+ ` `,
+ ` );`,
+ `}`,
``,
`/* prettier-ignore-end */`,
``
@@ -160,10 +116,6 @@ export async function generateKcGenTs(params: {
await fs.writeFile(filePath, newContent);
delete_legacy_file: {
- if (!isReactProject) {
- break delete_legacy_file;
- }
-
const legacyFilePath = filePath.replace(/tsx$/, "ts");
if (!(await existsAsync(legacyFilePath))) {
From 128b27221ad8be85186b06c3df3be005c267dbf2 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 06:45:06 +0200
Subject: [PATCH 44/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index eb36617a..133ddec1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.4",
+ "version": "11.3.0-rc.5",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From bc586eceef4f8002835d63258eaf8403a3bb6ea5 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 09:03:15 +0200
Subject: [PATCH 45/82] Make sure the update-kc-gen command is delegated when
building with vite
---
.../initialize-account-theme.ts | 4 +-
src/bin/main.ts | 293 +++++++-----------
...er_caller.ts => customHandler_delegate.ts} | 2 +-
src/bin/shared/generateKcGenTs.ts | 127 --------
src/bin/update-kc-gen.ts | 112 ++++++-
src/vite-plugin/vite-plugin.ts | 7 +-
stories/login/pages/Register.stories.tsx | 6 +-
7 files changed, 239 insertions(+), 312 deletions(-)
rename src/bin/shared/{customHandler_caller.ts => customHandler_delegate.ts} (95%)
delete mode 100644 src/bin/shared/generateKcGenTs.ts
diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts
index e29b6733..3caed327 100644
--- a/src/bin/initialize-account-theme/initialize-account-theme.ts
+++ b/src/bin/initialize-account-theme/initialize-account-theme.ts
@@ -5,7 +5,7 @@ import chalk from "chalk";
import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
-import { generateKcGenTs } from "../shared/generateKcGenTs";
+import { command as updateKcGenCommand } from "../update-kc-gen";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@@ -94,7 +94,7 @@ export async function command(params: { buildContext: BuildContext }) {
updateAccountThemeImplementationInConfig({ buildContext, accountThemeType });
- await generateKcGenTs({
+ await updateKcGenCommand({
buildContext: {
...buildContext,
implementedThemeTypes: {
diff --git a/src/bin/main.ts b/src/bin/main.ts
index 7ef2a9b8..89b03d7c 100644
--- a/src/bin/main.ts
+++ b/src/bin/main.ts
@@ -4,7 +4,6 @@ import { termost } from "termost";
import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
import * as child_process from "child_process";
import { assertNoPnpmDlx } from "./tools/assertNoPnpmDlx";
-import { callHandlerIfAny } from "./shared/customHandler_caller";
import { getBuildContext } from "./shared/buildContext";
type CliCommandOptions = {
@@ -72,153 +71,117 @@ program
.task({
skip,
handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
-
const { command } = await import("./keycloakify");
- await command({ buildContext });
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
-{
- const commandName = "start-keycloak";
+program
+ .command<{
+ port: number | undefined;
+ keycloakVersion: string | undefined;
+ realmJsonFilePath: string | undefined;
+ }>({
+ name: "start-keycloak",
+ description:
+ "Spin up a pre configured Docker image of Keycloak to test your theme."
+ })
+ .option({
+ key: "port",
+ name: (() => {
+ const name = "port";
- program
- .command<{
- port: number | undefined;
- keycloakVersion: string | undefined;
- realmJsonFilePath: string | undefined;
- }>({
- name: commandName,
- description:
- "Spin up a pre configured Docker image of Keycloak to test your theme."
- })
- .option({
- key: "port",
- name: (() => {
- const name = "port";
+ optionsKeys.push(name);
- optionsKeys.push(name);
+ return name;
+ })(),
+ description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
+ defaultValue: undefined
+ })
+ .option({
+ key: "keycloakVersion",
+ name: (() => {
+ const name = "keycloak-version";
- return name;
- })(),
- description: ["Keycloak server port.", "Example `--port 8085`"].join(" "),
- defaultValue: undefined
- })
- .option({
- key: "keycloakVersion",
- name: (() => {
- const name = "keycloak-version";
+ optionsKeys.push(name);
- optionsKeys.push(name);
+ return name;
+ })(),
+ description: [
+ "Use a specific version of Keycloak.",
+ "Example `--keycloak-version 21.1.1`"
+ ].join(" "),
+ defaultValue: undefined
+ })
+ .option({
+ key: "realmJsonFilePath",
+ name: (() => {
+ const name = "import";
- return name;
- })(),
- description: [
- "Use a specific version of Keycloak.",
- "Example `--keycloak-version 21.1.1`"
- ].join(" "),
- defaultValue: undefined
- })
- .option({
- key: "realmJsonFilePath",
- name: (() => {
- const name = "import";
+ optionsKeys.push(name);
- optionsKeys.push(name);
+ return name;
+ })(),
+ defaultValue: undefined,
+ description: [
+ "Import your own realm configuration file",
+ "Example `--import path/to/myrealm-realm.json`"
+ ].join(" ")
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath, keycloakVersion, port, realmJsonFilePath }) => {
+ const { command } = await import("./start-keycloak");
- return name;
- })(),
- defaultValue: undefined,
- description: [
- "Import your own realm configuration file",
- "Example `--import path/to/myrealm-realm.json`"
- ].join(" ")
- })
- .task({
- skip,
- handler: async ({
- projectDirPath,
- keycloakVersion,
- port,
- realmJsonFilePath
- }) => {
- const buildContext = getBuildContext({ projectDirPath });
+ await command({
+ buildContext: getBuildContext({ projectDirPath }),
+ cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
+ });
+ }
+ });
- const { command } = await import("./start-keycloak");
+program
+ .command({
+ name: "eject-page",
+ description: "Eject a Keycloak page."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const { command } = await import("./eject-page");
- await command({
- buildContext,
- cliCommandOptions: { keycloakVersion, port, realmJsonFilePath }
- });
- }
- });
-}
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
+ }
+ });
-{
- const commandName = "eject-page";
+program
+ .command({
+ name: "add-story",
+ description: "Add *.stories.tsx file for a specific page to in your Storybook."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const { command } = await import("./add-story");
- program
- .command({
- name: commandName,
- description: "Eject a Keycloak page."
- })
- .task({
- skip,
- handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
+ }
+ });
- callHandlerIfAny({ buildContext, commandName });
+program
+ .command({
+ name: "initialize-login-theme",
+ description: "Initialize an email theme."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const { command } = await import("./initialize-email-theme");
- const { command } = await import("./eject-page");
-
- await command({ buildContext });
- }
- });
-}
-
-{
- const commandName = "add-story";
-
- program
- .command({
- name: commandName,
- description:
- "Add *.stories.tsx file for a specific page to in your Storybook."
- })
- .task({
- skip,
- handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
-
- callHandlerIfAny({ buildContext, commandName });
-
- const { command } = await import("./add-story");
-
- await command({ buildContext });
- }
- });
-}
-
-{
- const comandName = "initialize-login-theme";
-
- program
- .command({
- name: comandName,
- description: "Initialize an email theme."
- })
- .task({
- skip,
- handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
-
- const { command } = await import("./initialize-email-theme");
-
- await command({ buildContext });
- }
- });
-}
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
+ }
+ });
program
.command({
@@ -228,57 +191,41 @@ program
.task({
skip,
handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
-
const { command } = await import("./initialize-account-theme");
- await command({ buildContext });
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
}
});
-{
- const commandName = "copy-keycloak-resources-to-public";
+program
+ .command({
+ name: "copy-keycloak-resources-to-public",
+ description:
+ "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const { command } = await import("./copy-keycloak-resources-to-public");
- program
- .command({
- name: commandName,
- description:
- "(Webpack/Create-React-App only) Copy Keycloak default theme resources to the public directory."
- })
- .task({
- skip,
- handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
+ }
+ });
- const { command } = await import("./copy-keycloak-resources-to-public");
+program
+ .command({
+ name: "update-kc-gen",
+ description:
+ "(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
+ })
+ .task({
+ skip,
+ handler: async ({ projectDirPath }) => {
+ const { command } = await import("./update-kc-gen");
- await command({ buildContext });
- }
- });
-}
-
-{
- const commandName = "update-kc-gen";
-
- program
- .command({
- name: commandName,
- description:
- "(Webpack/Create-React-App only) Create/update the kc.gen.ts file in your project."
- })
- .task({
- skip,
- handler: async ({ projectDirPath }) => {
- const buildContext = getBuildContext({ projectDirPath });
-
- callHandlerIfAny({ buildContext, commandName });
-
- const { command } = await import("./update-kc-gen");
-
- await command({ buildContext });
- }
- });
-}
+ await command({ buildContext: getBuildContext({ projectDirPath }) });
+ }
+ });
// Fallback to build command if no command is provided
{
diff --git a/src/bin/shared/customHandler_caller.ts b/src/bin/shared/customHandler_delegate.ts
similarity index 95%
rename from src/bin/shared/customHandler_caller.ts
rename to src/bin/shared/customHandler_delegate.ts
index 88d5a41c..a41cbc7b 100644
--- a/src/bin/shared/customHandler_caller.ts
+++ b/src/bin/shared/customHandler_delegate.ts
@@ -13,7 +13,7 @@ import * as fs from "fs";
assert>();
-export function callHandlerIfAny(params: {
+export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
}) {
diff --git a/src/bin/shared/generateKcGenTs.ts b/src/bin/shared/generateKcGenTs.ts
deleted file mode 100644
index e5bb7e86..00000000
--- a/src/bin/shared/generateKcGenTs.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { assert } from "tsafe/assert";
-import type { BuildContext } from "./buildContext";
-import * as fs from "fs/promises";
-import { join as pathJoin } from "path";
-import { existsAsync } from "../tools/fs.existsAsync";
-
-export type BuildContextLike = {
- projectDirPath: string;
- themeNames: string[];
- environmentVariables: { name: string; default: string }[];
- themeSrcDirPath: string;
- implementedThemeTypes: Pick<
- BuildContext["implementedThemeTypes"],
- "login" | "account"
- >;
- packageJsonFilePath: string;
-};
-
-assert();
-
-export async function generateKcGenTs(params: {
- buildContext: BuildContextLike;
-}): Promise {
- const { buildContext } = params;
-
- const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
-
- const currentContent = (await existsAsync(filePath))
- ? await fs.readFile(filePath)
- : undefined;
-
- const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
- const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
-
- const newContent = Buffer.from(
- [
- `/* prettier-ignore-start */`,
- ``,
- `/* eslint-disable */`,
- ``,
- `// @ts-nocheck`,
- ``,
- `// noinspection JSUnusedGlobalSymbols`,
- ``,
- `// This file is auto-generated by Keycloakify`,
- ``,
- `import { lazy, Suspense, type ReactNode } from "react";`,
- ``,
- `export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
- ``,
- `export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
- ``,
- `export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
- ``,
- `export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
- ``,
- `export const kcEnvDefaults: Record = ${JSON.stringify(
- Object.fromEntries(
- buildContext.environmentVariables.map(
- ({ name, default: defaultValue }) => [name, defaultValue]
- )
- ),
- null,
- 2
- )};`,
- ``,
- `export type KcContext =`,
- hasLoginTheme && ` | import("./login/KcContext").KcContext`,
- hasAccountTheme && ` | import("./account/KcContext").KcContext`,
- ` ;`,
- ``,
- `declare global {`,
- ` interface Window {`,
- ` kcContext?: KcContext;`,
- ` }`,
- `}`,
- ``,
- hasLoginTheme &&
- `export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
- hasAccountTheme &&
- `export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
- ``,
- `export function KcPage(`,
- ` props: {`,
- ` kcContext: KcContext;`,
- ` fallback?: ReactNode;`,
- ` }`,
- `) {`,
- ` const { kcContext, fallback } = props;`,
- ` return (`,
- ` `,
- ` {(() => {`,
- ` switch (kcContext.themeType) {`,
- hasLoginTheme &&
- ` case "login": return ;`,
- hasAccountTheme &&
- ` case "account": return ;`,
- ` }`,
- ` })()}`,
- ` `,
- ` );`,
- `}`,
- ``,
- `/* prettier-ignore-end */`,
- ``
- ]
- .filter(item => typeof item === "string")
- .join("\n"),
- "utf8"
- );
-
- if (currentContent !== undefined && currentContent.equals(newContent)) {
- return;
- }
-
- await fs.writeFile(filePath, newContent);
-
- delete_legacy_file: {
- const legacyFilePath = filePath.replace(/tsx$/, "ts");
-
- if (!(await existsAsync(legacyFilePath))) {
- break delete_legacy_file;
- }
-
- await fs.unlink(legacyFilePath);
- }
-}
diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts
index b3d9ce15..f126bcb1 100644
--- a/src/bin/update-kc-gen.ts
+++ b/src/bin/update-kc-gen.ts
@@ -1,8 +1,116 @@
import type { BuildContext } from "./shared/buildContext";
-import { generateKcGenTs } from "./shared/generateKcGenTs";
+import * as fs from "fs/promises";
+import { join as pathJoin } from "path";
+import { existsAsync } from "./tools/fs.existsAsync";
+import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- await generateKcGenTs({ buildContext });
+ maybeDelegateCommandToCustomHandler({
+ commandName: "update-kc-gen",
+ buildContext
+ });
+
+ const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
+
+ const currentContent = (await existsAsync(filePath))
+ ? await fs.readFile(filePath)
+ : undefined;
+
+ const hasLoginTheme = buildContext.implementedThemeTypes.login.isImplemented;
+ const hasAccountTheme = buildContext.implementedThemeTypes.account.isImplemented;
+
+ const newContent = Buffer.from(
+ [
+ `/* prettier-ignore-start */`,
+ ``,
+ `/* eslint-disable */`,
+ ``,
+ `// @ts-nocheck`,
+ ``,
+ `// noinspection JSUnusedGlobalSymbols`,
+ ``,
+ `// This file is auto-generated by Keycloakify`,
+ ``,
+ `import { lazy, Suspense, type ReactNode } from "react";`,
+ ``,
+ `export type ThemeName = ${buildContext.themeNames.map(themeName => `"${themeName}"`).join(" | ")};`,
+ ``,
+ `export const themeNames: ThemeName[] = [${buildContext.themeNames.map(themeName => `"${themeName}"`).join(", ")}];`,
+ ``,
+ `export type KcEnvName = ${buildContext.environmentVariables.length === 0 ? "never" : buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(" | ")};`,
+ ``,
+ `export const kcEnvNames: KcEnvName[] = [${buildContext.environmentVariables.map(({ name }) => `"${name}"`).join(", ")}];`,
+ ``,
+ `export const kcEnvDefaults: Record = ${JSON.stringify(
+ Object.fromEntries(
+ buildContext.environmentVariables.map(
+ ({ name, default: defaultValue }) => [name, defaultValue]
+ )
+ ),
+ null,
+ 2
+ )};`,
+ ``,
+ `export type KcContext =`,
+ hasLoginTheme && ` | import("./login/KcContext").KcContext`,
+ hasAccountTheme && ` | import("./account/KcContext").KcContext`,
+ ` ;`,
+ ``,
+ `declare global {`,
+ ` interface Window {`,
+ ` kcContext?: KcContext;`,
+ ` }`,
+ `}`,
+ ``,
+ hasLoginTheme &&
+ `export const KcLoginPage = lazy(() => import("./login/KcPage"));`,
+ hasAccountTheme &&
+ `export const KcAccountPage = lazy(() => import("./account/KcPage"));`,
+ ``,
+ `export function KcPage(`,
+ ` props: {`,
+ ` kcContext: KcContext;`,
+ ` fallback?: ReactNode;`,
+ ` }`,
+ `) {`,
+ ` const { kcContext, fallback } = props;`,
+ ` return (`,
+ ` `,
+ ` {(() => {`,
+ ` switch (kcContext.themeType) {`,
+ hasLoginTheme &&
+ ` case "login": return ;`,
+ hasAccountTheme &&
+ ` case "account": return ;`,
+ ` }`,
+ ` })()}`,
+ ` `,
+ ` );`,
+ `}`,
+ ``,
+ `/* prettier-ignore-end */`,
+ ``
+ ]
+ .filter(item => typeof item === "string")
+ .join("\n"),
+ "utf8"
+ );
+
+ if (currentContent !== undefined && currentContent.equals(newContent)) {
+ return;
+ }
+
+ await fs.writeFile(filePath, newContent);
+
+ delete_legacy_file: {
+ const legacyFilePath = filePath.replace(/tsx$/, "ts");
+
+ if (!(await existsAsync(legacyFilePath))) {
+ break delete_legacy_file;
+ }
+
+ await fs.unlink(legacyFilePath);
+ }
}
diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts
index 3a7f3d26..e6dfb054 100644
--- a/src/vite-plugin/vite-plugin.ts
+++ b/src/vite-plugin/vite-plugin.ts
@@ -15,7 +15,7 @@ import {
type ResolvedViteConfig
} from "../bin/shared/buildContext";
import MagicString from "magic-string";
-import { generateKcGenTs } from "../bin/shared/generateKcGenTs";
+import { command as updateKcGenCommand } from "../bin/update-kc-gen";
export namespace keycloakify {
export type Params = BuildOptions & {
@@ -125,8 +125,9 @@ export function keycloakify(params: keycloakify.Params) {
projectDirPath
});
- copyKeycloakResourcesToPublic({ buildContext }),
- await generateKcGenTs({ buildContext });
+ copyKeycloakResourcesToPublic({ buildContext });
+
+ await updateKcGenCommand({ buildContext });
},
transform: (code, id) => {
assert(command !== undefined);
diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx
index f25568cc..09cdf249 100644
--- a/stories/login/pages/Register.stories.tsx
+++ b/stories/login/pages/Register.stories.tsx
@@ -115,7 +115,6 @@ export const WithFavoritePet: Story = {
)
};
-
export const WithNewsletter: Story = {
render: () => (
)
};
-
export const WithEmailAsUsername: Story = {
render: () => (
Date: Sun, 6 Oct 2024 09:07:10 +0200
Subject: [PATCH 46/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 133ddec1..791e13e6 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.5",
+ "version": "11.3.0-rc.6",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 182fb430f1151e9b988c16f312682ee60d14da6e Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 12:44:46 +0200
Subject: [PATCH 47/82] Fix dead code
---
.../updateAccountThemeImplementationInConfig.ts | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts b/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
index 976beb9a..1c60ef03 100644
--- a/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
+++ b/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
@@ -8,12 +8,14 @@ import { id } from "tsafe/id";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
+ projectDirPath: string;
+ packageJsonFilePath: string;
};
assert();
export function updateAccountThemeImplementationInConfig(params: {
- buildContext: BuildContext;
+ buildContext: BuildContextLike;
accountThemeType: "Single-Page" | "Multi-Page";
}) {
const { buildContext, accountThemeType } = params;
From 9910762abc4069a85446549bdc66128a51646940 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 13:18:12 +0200
Subject: [PATCH 48/82] Add initialize-email-theme, initialize-account-theme
and copy-keycloak-resources-to-public to commands that can be delegated to a
custom handler
---
src/bin/copy-keycloak-resources-to-public.ts | 6 ++++++
.../initialize-account-theme/initialize-account-theme.ts | 6 ++++++
src/bin/initialize-email-theme.ts | 6 ++++++
src/bin/shared/customHandler.ts | 8 +++++++-
4 files changed, 25 insertions(+), 1 deletion(-)
diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts
index ba944068..b48f9d0e 100644
--- a/src/bin/copy-keycloak-resources-to-public.ts
+++ b/src/bin/copy-keycloak-resources-to-public.ts
@@ -1,9 +1,15 @@
import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
import type { BuildContext } from "./shared/buildContext";
+import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
+ maybeDelegateCommandToCustomHandler({
+ commandName: "copy-keycloak-resources-to-public",
+ buildContext
+ });
+
copyKeycloakResourcesToPublic({
buildContext
});
diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts
index 3caed327..e7ae9223 100644
--- a/src/bin/initialize-account-theme/initialize-account-theme.ts
+++ b/src/bin/initialize-account-theme/initialize-account-theme.ts
@@ -6,10 +6,16 @@ import { join as pathJoin, relative as pathRelative } from "path";
import * as fs from "fs";
import { updateAccountThemeImplementationInConfig } from "./updateAccountThemeImplementationInConfig";
import { command as updateKcGenCommand } from "../update-kc-gen";
+import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
+ maybeDelegateCommandToCustomHandler({
+ commandName: "initialize-account-theme",
+ buildContext
+ });
+
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index dfa0287c..d5ce0fd8 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -4,10 +4,16 @@ import { promptKeycloakVersion } from "./shared/promptKeycloakVersion";
import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
+import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
+ maybeDelegateCommandToCustomHandler({
+ commandName: "initialize-email-theme",
+ buildContext
+ });
+
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if (
diff --git a/src/bin/shared/customHandler.ts b/src/bin/shared/customHandler.ts
index d858aaa5..7c0b9e1c 100644
--- a/src/bin/shared/customHandler.ts
+++ b/src/bin/shared/customHandler.ts
@@ -6,7 +6,13 @@ export const BIN_NAME = "_keycloakify-custom-handler";
export const NOT_IMPLEMENTED_EXIT_CODE = 78;
-export type CommandName = "update-kc-gen" | "eject-page" | "add-story";
+export type CommandName =
+ | "update-kc-gen"
+ | "eject-page"
+ | "add-story"
+ | "initialize-account-theme"
+ | "initialize-email-theme"
+ | "copy-keycloak-resources-to-public";
export type ApiVersion = "v1";
From 6aa60e685bac743686c1fb5a7d350e4415942f3a Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 13:19:12 +0200
Subject: [PATCH 49/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 791e13e6..ae774e39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.6",
+ "version": "11.3.0-rc.7",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From d626699f08364c933e8fd13bbdb008aa0417a698 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 15:09:53 +0200
Subject: [PATCH 50/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index ae774e39..0fec7284 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0-rc.7",
+ "version": "11.3.0",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 9a6a71c8bc3ffc422ca7091d6f6621275de380e9 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 15:37:32 +0200
Subject: [PATCH 51/82] Fix litle inconsistency
---
src/bin/eject-page.ts | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts
index 8874fd99..bacafe6f 100644
--- a/src/bin/eject-page.ts
+++ b/src/bin/eject-page.ts
@@ -239,12 +239,12 @@ export async function command(params: { buildContext: BuildContext }) {
)} copy pasted from the Keycloakify source code into your project`
);
- edit_KcApp: {
+ edit_KcPage: {
if (
pageIdOrComponent !== templateValue &&
pageIdOrComponent !== userProfileFormFieldsValue
) {
- break edit_KcApp;
+ break edit_KcPage;
}
const kcAppTsxPath = pathJoin(
From 16906297174161d4e2fc7c3fb71324442f2dbe9b Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 22:08:43 +0200
Subject: [PATCH 52/82] Fix: check for delegation of the eject-page command
---
src/bin/eject-page.ts | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts
index bacafe6f..ee4a3581 100644
--- a/src/bin/eject-page.ts
+++ b/src/bin/eject-page.ts
@@ -22,10 +22,16 @@ import { kebabCaseToCamelCase } from "./tools/kebabCaseToSnakeCase";
import { assert, Equals } from "tsafe/assert";
import type { BuildContext } from "./shared/buildContext";
import chalk from "chalk";
+import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
+ maybeDelegateCommandToCustomHandler({
+ commandName: "eject-page",
+ buildContext
+ });
+
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
From a40810b3641fab4e5cd775c0d407bfaebf8bf4bb Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 22:09:21 +0200
Subject: [PATCH 53/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 0fec7284..0ac79b18 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.0",
+ "version": "11.3.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From dc4f386e7afce2993cdf05331c71e14e17c6dca7 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 22:55:18 +0200
Subject: [PATCH 54/82] Fix vite quitting if custom handler implemented
---
src/bin/copy-keycloak-resources-to-public.ts | 90 +++++++++++++++++-
src/bin/eject-page.ts | 6 +-
.../initialize-account-theme.ts | 6 +-
src/bin/initialize-email-theme.ts | 6 +-
.../shared/copyKeycloakResourcesToPublic.ts | 95 -------------------
src/bin/shared/customHandler_delegate.ts | 8 +-
src/bin/update-kc-gen.ts | 6 +-
src/vite-plugin/vite-plugin.ts | 4 +-
8 files changed, 111 insertions(+), 110 deletions(-)
delete mode 100644 src/bin/shared/copyKeycloakResourcesToPublic.ts
diff --git a/src/bin/copy-keycloak-resources-to-public.ts b/src/bin/copy-keycloak-resources-to-public.ts
index b48f9d0e..1925860d 100644
--- a/src/bin/copy-keycloak-resources-to-public.ts
+++ b/src/bin/copy-keycloak-resources-to-public.ts
@@ -1,16 +1,96 @@
-import { copyKeycloakResourcesToPublic } from "./shared/copyKeycloakResourcesToPublic";
-import type { BuildContext } from "./shared/buildContext";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
+import { join as pathJoin, dirname as pathDirname } from "path";
+import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "./shared/constants";
+import { readThisNpmPackageVersion } from "./tools/readThisNpmPackageVersion";
+import * as fs from "fs";
+import { rmSync } from "./tools/fs.rmSync";
+import type { BuildContext } from "./shared/buildContext";
+import { transformCodebase } from "./tools/transformCodebase";
+import { getThisCodebaseRootDirPath } from "./tools/getThisCodebaseRootDirPath";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- maybeDelegateCommandToCustomHandler({
+ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "copy-keycloak-resources-to-public",
buildContext
});
- copyKeycloakResourcesToPublic({
- buildContext
+ if (hasBeenHandled) {
+ return;
+ }
+
+ const destDirPath = pathJoin(
+ buildContext.publicDirPath,
+ WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
+ );
+
+ const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
+
+ const keycloakifyBuildinfoRaw = JSON.stringify(
+ {
+ keycloakifyVersion: readThisNpmPackageVersion()
+ },
+ null,
+ 2
+ );
+
+ skip_if_already_done: {
+ if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
+ break skip_if_already_done;
+ }
+
+ const keycloakifyBuildinfoRaw_previousRun = fs
+ .readFileSync(keycloakifyBuildinfoFilePath)
+ .toString("utf8");
+
+ if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
+ break skip_if_already_done;
+ }
+
+ return;
+ }
+
+ rmSync(destDirPath, { force: true, recursive: true });
+
+ // NOTE: To remove in a while, remove the legacy keycloak-resources directory
+ rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
+ force: true,
+ recursive: true
});
+ rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
+ force: true,
+ recursive: true
+ });
+
+ fs.mkdirSync(destDirPath, { recursive: true });
+
+ fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
+
+ transformCodebase({
+ srcDirPath: pathJoin(
+ getThisCodebaseRootDirPath(),
+ "res",
+ "public",
+ WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
+ ),
+ destDirPath
+ });
+
+ fs.writeFileSync(
+ pathJoin(destDirPath, "README.txt"),
+ Buffer.from(
+ // prettier-ignore
+ [
+ "This directory is only used in dev mode by Keycloakify",
+ "It won't be included in your final build.",
+ "Do not modify anything in this directory.",
+ ].join("\n")
+ )
+ );
+
+ fs.writeFileSync(
+ keycloakifyBuildinfoFilePath,
+ Buffer.from(keycloakifyBuildinfoRaw, "utf8")
+ );
}
diff --git a/src/bin/eject-page.ts b/src/bin/eject-page.ts
index ee4a3581..e982c2f7 100644
--- a/src/bin/eject-page.ts
+++ b/src/bin/eject-page.ts
@@ -27,11 +27,15 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- maybeDelegateCommandToCustomHandler({
+ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "eject-page",
buildContext
});
+ if (hasBeenHandled) {
+ return;
+ }
+
console.log(chalk.cyan("Theme type:"));
const themeType = await (async () => {
diff --git a/src/bin/initialize-account-theme/initialize-account-theme.ts b/src/bin/initialize-account-theme/initialize-account-theme.ts
index e7ae9223..dd7c61c8 100644
--- a/src/bin/initialize-account-theme/initialize-account-theme.ts
+++ b/src/bin/initialize-account-theme/initialize-account-theme.ts
@@ -11,11 +11,15 @@ import { maybeDelegateCommandToCustomHandler } from "../shared/customHandler_del
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- maybeDelegateCommandToCustomHandler({
+ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-account-theme",
buildContext
});
+ if (hasBeenHandled) {
+ return;
+ }
+
const accountThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "account");
if (
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index d5ce0fd8..7a81205b 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -9,11 +9,15 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- maybeDelegateCommandToCustomHandler({
+ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "initialize-email-theme",
buildContext
});
+ if (hasBeenHandled) {
+ return;
+ }
+
const emailThemeSrcDirPath = pathJoin(buildContext.themeSrcDirPath, "email");
if (
diff --git a/src/bin/shared/copyKeycloakResourcesToPublic.ts b/src/bin/shared/copyKeycloakResourcesToPublic.ts
deleted file mode 100644
index db78d313..00000000
--- a/src/bin/shared/copyKeycloakResourcesToPublic.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { join as pathJoin, dirname as pathDirname } from "path";
-import { WELL_KNOWN_DIRECTORY_BASE_NAME } from "../shared/constants";
-import { readThisNpmPackageVersion } from "../tools/readThisNpmPackageVersion";
-import { assert } from "tsafe/assert";
-import * as fs from "fs";
-import { rmSync } from "../tools/fs.rmSync";
-import type { BuildContext } from "./buildContext";
-import { transformCodebase } from "../tools/transformCodebase";
-import { getThisCodebaseRootDirPath } from "../tools/getThisCodebaseRootDirPath";
-
-export type BuildContextLike = {
- publicDirPath: string;
-};
-
-assert();
-
-export function copyKeycloakResourcesToPublic(params: {
- buildContext: BuildContextLike;
-}) {
- const { buildContext } = params;
-
- const destDirPath = pathJoin(
- buildContext.publicDirPath,
- WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
- );
-
- const keycloakifyBuildinfoFilePath = pathJoin(destDirPath, "keycloakify.buildinfo");
-
- const keycloakifyBuildinfoRaw = JSON.stringify(
- {
- keycloakifyVersion: readThisNpmPackageVersion()
- },
- null,
- 2
- );
-
- skip_if_already_done: {
- if (!fs.existsSync(keycloakifyBuildinfoFilePath)) {
- break skip_if_already_done;
- }
-
- const keycloakifyBuildinfoRaw_previousRun = fs
- .readFileSync(keycloakifyBuildinfoFilePath)
- .toString("utf8");
-
- if (keycloakifyBuildinfoRaw_previousRun !== keycloakifyBuildinfoRaw) {
- break skip_if_already_done;
- }
-
- return;
- }
-
- rmSync(destDirPath, { force: true, recursive: true });
-
- // NOTE: To remove in a while, remove the legacy keycloak-resources directory
- rmSync(pathJoin(pathDirname(destDirPath), "keycloak-resources"), {
- force: true,
- recursive: true
- });
- rmSync(pathJoin(pathDirname(destDirPath), ".keycloakify"), {
- force: true,
- recursive: true
- });
-
- fs.mkdirSync(destDirPath, { recursive: true });
-
- fs.writeFileSync(pathJoin(destDirPath, ".gitignore"), Buffer.from("*", "utf8"));
-
- transformCodebase({
- srcDirPath: pathJoin(
- getThisCodebaseRootDirPath(),
- "res",
- "public",
- WELL_KNOWN_DIRECTORY_BASE_NAME.KEYCLOAKIFY_DEV_RESOURCES
- ),
- destDirPath
- });
-
- fs.writeFileSync(
- pathJoin(destDirPath, "README.txt"),
- Buffer.from(
- // prettier-ignore
- [
- "This directory is only used in dev mode by Keycloakify",
- "It won't be included in your final build.",
- "Do not modify anything in this directory.",
- ].join("\n")
- )
- );
-
- fs.writeFileSync(
- keycloakifyBuildinfoFilePath,
- Buffer.from(keycloakifyBuildinfoRaw, "utf8")
- );
-}
diff --git a/src/bin/shared/customHandler_delegate.ts b/src/bin/shared/customHandler_delegate.ts
index a41cbc7b..691989df 100644
--- a/src/bin/shared/customHandler_delegate.ts
+++ b/src/bin/shared/customHandler_delegate.ts
@@ -16,11 +16,11 @@ assert>();
export function maybeDelegateCommandToCustomHandler(params: {
commandName: CommandName;
buildContext: BuildContext;
-}) {
+}): { hasBeenHandled: boolean } {
const { commandName, buildContext } = params;
if (!fs.readdirSync(pathDirname(process.argv[1])).includes(BIN_NAME)) {
- return;
+ return { hasBeenHandled: false };
}
try {
@@ -36,11 +36,11 @@ export function maybeDelegateCommandToCustomHandler(params: {
const status = error.status;
if (status === NOT_IMPLEMENTED_EXIT_CODE) {
- return;
+ return { hasBeenHandled: false };
}
process.exit(status);
}
- process.exit(0);
+ return { hasBeenHandled: true };
}
diff --git a/src/bin/update-kc-gen.ts b/src/bin/update-kc-gen.ts
index f126bcb1..e1366fd4 100644
--- a/src/bin/update-kc-gen.ts
+++ b/src/bin/update-kc-gen.ts
@@ -7,11 +7,15 @@ import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_dele
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
- maybeDelegateCommandToCustomHandler({
+ const { hasBeenHandled } = maybeDelegateCommandToCustomHandler({
commandName: "update-kc-gen",
buildContext
});
+ if (hasBeenHandled) {
+ return;
+ }
+
const filePath = pathJoin(buildContext.themeSrcDirPath, `kc.gen.tsx`);
const currentContent = (await existsAsync(filePath))
diff --git a/src/vite-plugin/vite-plugin.ts b/src/vite-plugin/vite-plugin.ts
index e6dfb054..d7ec06d5 100644
--- a/src/vite-plugin/vite-plugin.ts
+++ b/src/vite-plugin/vite-plugin.ts
@@ -6,7 +6,7 @@ import {
} from "../bin/shared/constants";
import { id } from "tsafe/id";
import { rm } from "../bin/tools/fs.rm";
-import { copyKeycloakResourcesToPublic } from "../bin/shared/copyKeycloakResourcesToPublic";
+import { command as copyKeycloakResourcesToPublicCommand } from "../bin/copy-keycloak-resources-to-public";
import { assert } from "tsafe/assert";
import {
getBuildContext,
@@ -125,7 +125,7 @@ export function keycloakify(params: keycloakify.Params) {
projectDirPath
});
- copyKeycloakResourcesToPublic({ buildContext });
+ await copyKeycloakResourcesToPublicCommand({ buildContext });
await updateKcGenCommand({ buildContext });
},
From c3e821088b4ad15c70d9336fea25550f5ac85b10 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 6 Oct 2024 22:56:42 +0200
Subject: [PATCH 55/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 0ac79b18..f2e015ac 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.1",
+ "version": "11.3.2",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From b879569b81c804623cee548eb74c71827db609f7 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 7 Oct 2024 20:56:03 +0200
Subject: [PATCH 56/82] Announcement about Keycloak 26
---
README.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/README.md b/README.md
index 2c133223..c6edf2d0 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,11 @@
Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyond](https://github.com/keycloakify/keycloakify/discussions/346#discussioncomment-5889791)
+> 📣 **Keycloakify 26 Released**
+> Themes built with Keycloakify versions **prior** to Keycloak 26 are **incompatible** with Keycloak 26.
+> To ensure compatibility, simply upgrade to the latest Keycloakify version for your major release (v10 or v11) and rebuild your theme.
+> No breaking changes have been introduced, but the target version ranges have been updated. For more details, see [this guide](https://docs.keycloakify.dev/targeting-specific-keycloak-versions).
+
## Sponsors
Friends for the project, we trust and recommend their services.
From 987335399008e2ba57288c0cf765f458ca0965d2 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 7 Oct 2024 21:02:51 +0200
Subject: [PATCH 57/82] Fix initialize-email-theme
---
src/bin/main.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/bin/main.ts b/src/bin/main.ts
index 89b03d7c..a6abe1f5 100644
--- a/src/bin/main.ts
+++ b/src/bin/main.ts
@@ -171,7 +171,7 @@ program
program
.command({
- name: "initialize-login-theme",
+ name: "initialize-email-theme",
description: "Initialize an email theme."
})
.task({
From f1cb165bddbbceba77640bca24b7b71f9a87610b Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 7 Oct 2024 21:03:04 +0200
Subject: [PATCH 58/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index f2e015ac..cf03259f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.2",
+ "version": "11.3.3",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From ddeade97756b37ca68b0a8a2e0aefbe718f55b28 Mon Sep 17 00:00:00 2001
From: Nima Shkouhfar
Date: Sun, 29 Sep 2024 04:35:02 -0400
Subject: [PATCH 59/82] Changes: - First draft of test coverage improvement for
storybooks - code's page html rendering issue fixed
---
src/login/pages/Code.tsx | 10 +-
stories/login/pages/Code.stories.tsx | 39 ++++++
.../pages/DeleteAccountConfirm.stories.tsx | 30 ++++
.../login/pages/DeleteCredential.stories.tsx | 10 ++
stories/login/pages/Error.stories.tsx | 35 +++++
.../pages/FrontchannelLogout.stories.tsx | 11 ++
.../pages/IdpReviewUserProfile.stories.tsx | 44 ++++++
stories/login/pages/Info.stories.tsx | 39 ++++++
stories/login/pages/Login.stories.tsx | 128 ++++++++++++++++++
.../login/pages/LoginConfigTotp.stories.tsx | 21 +++
.../login/pages/LoginIdpLinkEmail.stories.tsx | 46 +++++++
stories/login/pages/Register.stories.tsx | 62 +++++++++
stories/login/pages/Terms.stories.tsx | 29 ++++
13 files changed, 503 insertions(+), 1 deletion(-)
diff --git a/src/login/pages/Code.tsx b/src/login/pages/Code.tsx
index 576a2ccd..4bca62f6 100644
--- a/src/login/pages/Code.tsx
+++ b/src/login/pages/Code.tsx
@@ -2,6 +2,7 @@ import { getKcClsx } from "keycloakify/login/lib/kcClsx";
import type { PageProps } from "keycloakify/login/pages/PageProps";
import type { KcContext } from "../KcContext";
import type { I18n } from "../i18n";
+import { kcSanitize } from "keycloakify/lib/kcSanitize";
export default function Code(props: PageProps, I18n>) {
const { kcContext, i18n, doUseDefaultCss, Template, classes } = props;
@@ -30,7 +31,14 @@ export default function Code(props: PageProps
>
) : (
- {code.error}
+ code.error && (
+
+ )
)}
diff --git a/stories/login/pages/Code.stories.tsx b/stories/login/pages/Code.stories.tsx
index 0fcdf87f..b93125bf 100644
--- a/stories/login/pages/Code.stories.tsx
+++ b/stories/login/pages/Code.stories.tsx
@@ -16,3 +16,42 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithErrorCode: Story = {
+ render: () => (
+
+ )
+};
+export const WithFrenchLanguage: Story = {
+ render: () => (
+
+ )
+};
+export const WithHtmlErrorMessage: Story = {
+ render: () => (
+ Try again"
+ }
+ }}
+ />
+ )
+};
diff --git a/stories/login/pages/DeleteAccountConfirm.stories.tsx b/stories/login/pages/DeleteAccountConfirm.stories.tsx
index 501fa0c4..0d865dfe 100644
--- a/stories/login/pages/DeleteAccountConfirm.stories.tsx
+++ b/stories/login/pages/DeleteAccountConfirm.stories.tsx
@@ -16,3 +16,33 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithAIAFlow: Story = {
+ render: () => (
+
+ )
+};
+export const WithoutAIAFlow: Story = {
+ render: () => (
+
+ )
+};
+export const WithCustomButtonStyle: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/DeleteCredential.stories.tsx b/stories/login/pages/DeleteCredential.stories.tsx
index 7b211a52..38619d5b 100644
--- a/stories/login/pages/DeleteCredential.stories.tsx
+++ b/stories/login/pages/DeleteCredential.stories.tsx
@@ -16,3 +16,13 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithCustomCredentialLabel: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/Error.stories.tsx b/stories/login/pages/Error.stories.tsx
index 7b12410e..e64ea3f8 100644
--- a/stories/login/pages/Error.stories.tsx
+++ b/stories/login/pages/Error.stories.tsx
@@ -26,3 +26,38 @@ export const WithAnotherMessage: Story = {
/>
)
};
+
+export const WithHtmlErrorMessage: Story = {
+ render: () => (
+ Error: Something went wrong. Go back"
+ }
+ }}
+ />
+ )
+};
+export const FrenchError: Story = {
+ render: () => (
+
+ )
+};
+export const WithSkipLink: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/FrontchannelLogout.stories.tsx b/stories/login/pages/FrontchannelLogout.stories.tsx
index 9dacf0b0..cdef7c2c 100644
--- a/stories/login/pages/FrontchannelLogout.stories.tsx
+++ b/stories/login/pages/FrontchannelLogout.stories.tsx
@@ -16,3 +16,14 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithoutRedirectUrl: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/IdpReviewUserProfile.stories.tsx b/stories/login/pages/IdpReviewUserProfile.stories.tsx
index 6778e77d..93eb5b30 100644
--- a/stories/login/pages/IdpReviewUserProfile.stories.tsx
+++ b/stories/login/pages/IdpReviewUserProfile.stories.tsx
@@ -16,3 +16,47 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithFormValidationErrors: Story = {
+ render: () => (
+ ["email", "firstName"].includes(fieldName),
+ get: (fieldName: string) => {
+ if (fieldName === "email") return "Invalid email format.";
+ if (fieldName === "firstName") return "First name is required.";
+ }
+ }
+ }}
+ />
+ )
+};
+export const WithReadOnlyFields: Story = {
+ render: () => (
+
+ )
+};
+export const WithPrefilledFormFields: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/Info.stories.tsx b/stories/login/pages/Info.stories.tsx
index 195061f3..81884dce 100644
--- a/stories/login/pages/Info.stories.tsx
+++ b/stories/login/pages/Info.stories.tsx
@@ -55,3 +55,42 @@ export const WithRequiredActions: Story = {
/>
)
};
+export const WithPageRedirect: Story = {
+ render: () => (
+
+ )
+};
+export const WithoutClientBaseUrl: Story = {
+ render: () => (
+
+ )
+};
+export const WithMessageHeader: Story = {
+ render: () => (
+
+ )
+};
+export const WithAdvancedMessage: Story = {
+ render: () => (
+ important information." }
+ }}
+ />
+ )
+};
diff --git a/stories/login/pages/Login.stories.tsx b/stories/login/pages/Login.stories.tsx
index 075d1f98..a3743adb 100644
--- a/stories/login/pages/Login.stories.tsx
+++ b/stories/login/pages/Login.stories.tsx
@@ -231,3 +231,131 @@ export const WithErrorMessage: Story = {
/>
)
};
+
+export const WithOneSocialProvider: Story = {
+ render: args => (
+
+ )
+};
+
+export const WithTwoSocialProviders: Story = {
+ render: args => (
+
+ )
+};
+export const WithNoSocialProviders: Story = {
+ render: args => (
+
+ )
+};
+export const WithMoreThanTwoSocialProviders: Story = {
+ render: args => (
+
+ )
+};
+export const WithSocialProvidersAndWithoutRememberMe: Story = {
+ render: args => (
+
+ )
+};
diff --git a/stories/login/pages/LoginConfigTotp.stories.tsx b/stories/login/pages/LoginConfigTotp.stories.tsx
index 5d38df9b..743d38c2 100644
--- a/stories/login/pages/LoginConfigTotp.stories.tsx
+++ b/stories/login/pages/LoginConfigTotp.stories.tsx
@@ -41,3 +41,24 @@ export const WithError: Story = {
/>
)
};
+export const WithAppInitiatedAction: Story = {
+ render: () => (
+
+ )
+};
+
+export const WithPreFilledUserLabel: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/LoginIdpLinkEmail.stories.tsx b/stories/login/pages/LoginIdpLinkEmail.stories.tsx
index a58ae2d3..e855eed5 100644
--- a/stories/login/pages/LoginIdpLinkEmail.stories.tsx
+++ b/stories/login/pages/LoginIdpLinkEmail.stories.tsx
@@ -16,3 +16,49 @@ type Story = StoryObj;
export const Default: Story = {
render: () =>
};
+export const WithIdpAlias: Story = {
+ render: () => (
+
+ )
+};
+export const WithoutIdpAlias: Story = {
+ render: () => (
+
+ )
+};
+
+export const WithCustomRealmDisplayName: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx
index 09cdf249..8ab6c16d 100644
--- a/stories/login/pages/Register.stories.tsx
+++ b/stories/login/pages/Register.stories.tsx
@@ -215,3 +215,65 @@ export const WithTermsAcceptance: Story = {
/>
)
};
+export const WithTermsNotAccepted: Story = {
+ render: args => (
+ fieldName === "termsAccepted",
+ get: (fieldName: string) => (fieldName === "termsAccepted" ? "You must accept the terms." : undefined)
+ }
+ }}
+ />
+ )
+};
+export const WithFieldErrors: Story = {
+ render: () => (
+ ["username", "email"].includes(fieldName),
+ get: fieldName => {
+ if (fieldName === "username") return "Username is required.";
+ if (fieldName === "email") return "Invalid email format.";
+ }
+ }
+ }}
+ />
+ )
+};
+export const WithReadOnlyFields: Story = {
+ render: () => (
+
+ )
+};
+export const WithAutoGeneratedUsername: Story = {
+ render: () => (
+
+ )
+};
diff --git a/stories/login/pages/Terms.stories.tsx b/stories/login/pages/Terms.stories.tsx
index f5837b07..631fe685 100644
--- a/stories/login/pages/Terms.stories.tsx
+++ b/stories/login/pages/Terms.stories.tsx
@@ -45,3 +45,32 @@ export const French: Story = {
/>
)
};
+export const WithErrorMessage: Story = {
+ render: () => (
+ true,
+ get: () => "An error occurred while processing your request."
+ }
+ }}
+ />
+ )
+};
+
+export const Spanish: Story = {
+ render: () => (
+ Mis términos en Español"
+ }
+ }
+ }}
+ />
+ )
+};
From 22241fd7adef1b7e0a209a9dbd9548c348fab7c7 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 21:47:10 +0000
Subject: [PATCH 60/82] docs: update README.md [skip ci]
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index c6edf2d0..11eff6fe 100644
--- a/README.md
+++ b/README.md
@@ -141,6 +141,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Omid ⚠️ 💻 |
 Katharina Eiserfey 💻 ⚠️ 📖 |
 Luca Peruzzo 💻 ⚠️ |
+  Nima Shokouhfar 💻 ⚠️ |
From 5332001ff42b1c45ca7f922cc027e77bc9bf046f Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Mon, 7 Oct 2024 21:47:11 +0000
Subject: [PATCH 61/82] docs: update .all-contributorsrc [skip ci]
---
.all-contributorsrc | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index fdede216..f7d39edd 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -290,6 +290,16 @@
"code",
"test"
]
+ },
+ {
+ "login": "nima70",
+ "name": "Nima Shokouhfar",
+ "avatar_url": "https://avatars.githubusercontent.com/u/5094767?v=4",
+ "profile": "https://github.com/nima70",
+ "contributions": [
+ "code",
+ "test"
+ ]
}
],
"contributorsPerLine": 7,
From 1f4d4473e4d98da052efd77d8d036e060f1ecc7b Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Mon, 7 Oct 2024 23:53:05 +0200
Subject: [PATCH 62/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index cf03259f..9be229b0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.3",
+ "version": "11.3.4",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 0f99bb5bdc245fc325d63bb71c4eafcf99a6f3b0 Mon Sep 17 00:00:00 2001
From: Liam Lowsley-Williams
Date: Tue, 8 Oct 2024 16:50:11 -0500
Subject: [PATCH 63/82] fix: added parameter type for story context on register
page
---
stories/login/pages/Register.stories.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/stories/login/pages/Register.stories.tsx b/stories/login/pages/Register.stories.tsx
index 8ab6c16d..3250f927 100644
--- a/stories/login/pages/Register.stories.tsx
+++ b/stories/login/pages/Register.stories.tsx
@@ -240,8 +240,8 @@ export const WithFieldErrors: Story = {
}
},
messagesPerField: {
- existsError: fieldName => ["username", "email"].includes(fieldName),
- get: fieldName => {
+ existsError: (fieldName: string) => ["username", "email"].includes(fieldName),
+ get: (fieldName: string) => {
if (fieldName === "username") return "Username is required.";
if (fieldName === "email") return "Invalid email format.";
}
From 9ed90995e4cb82a8b2f151762cd349d01aad7be8 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Fri, 11 Oct 2024 23:55:04 +0200
Subject: [PATCH 64/82] typesafety fix
---
.../updateAccountThemeImplementationInConfig.ts | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts b/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
index 1c60ef03..65b3ba39 100644
--- a/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
+++ b/src/bin/initialize-account-theme/updateAccountThemeImplementationInConfig.ts
@@ -5,6 +5,7 @@ import * as fs from "fs";
import chalk from "chalk";
import { z } from "zod";
import { id } from "tsafe/id";
+import { is } from "tsafe/is";
export type BuildContextLike = {
bundler: BuildContext["bundler"];
@@ -83,6 +84,8 @@ export function updateAccountThemeImplementationInConfig(params: {
zParsedPackageJson.parse(parsedPackageJson);
+ assert(is(parsedPackageJson));
+
return parsedPackageJson;
})();
From 2917719315176751248b193997a1281893da4897 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 12 Oct 2024 17:30:30 +0200
Subject: [PATCH 65/82] Add dir=rtl attribut to html when using a RTL language
---
src/login/KcContext/KcContext.ts | 1 +
src/login/i18n/noJsx/getI18n.tsx | 44 ++++++++++++++++++++++++++++++++
2 files changed, 45 insertions(+)
diff --git a/src/login/KcContext/KcContext.ts b/src/login/KcContext/KcContext.ts
index 14d1169f..161e45de 100644
--- a/src/login/KcContext/KcContext.ts
+++ b/src/login/KcContext/KcContext.ts
@@ -94,6 +94,7 @@ export declare namespace KcContext {
languageTag: string;
}[];
currentLanguageTag: string;
+ rtl?: boolean;
};
auth?: {
showUsername?: boolean;
diff --git a/src/login/i18n/noJsx/getI18n.tsx b/src/login/i18n/noJsx/getI18n.tsx
index e560e2e3..f619a303 100644
--- a/src/login/i18n/noJsx/getI18n.tsx
+++ b/src/login/i18n/noJsx/getI18n.tsx
@@ -19,6 +19,7 @@ export type KcContextLike = {
locale?: {
currentLanguageTag: string;
supported: { languageTag: string; url: string; label: string }[];
+ rtl?: boolean;
};
"x-keycloakify": {
messages: Record;
@@ -95,6 +96,49 @@ export function createGetI18n<
const html = document.querySelector("html");
assert(html !== null);
html.lang = currentLanguageTag;
+
+ const isRtl = (() => {
+ const { rtl } = kcContext.locale ?? {};
+
+ if (rtl !== undefined) {
+ return rtl;
+ }
+
+ return [
+ /* spell-checker: disable */
+ // Common RTL languages
+ "ar", // Arabic
+ "fa", // Persian (Farsi)
+ "he", // Hebrew
+ "ur", // Urdu
+ "ps", // Pashto
+ "syr", // Syriac
+ "dv", // Divehi (Maldivian)
+ "ku", // Kurdish (Sorani)
+ "ug", // Uighur
+ "az", // Azerbaijani (Arabic script)
+ "sd", // Sindhi
+
+ // Less common RTL languages
+ "yi", // Yiddish
+ "ha", // Hausa (when written in Arabic script)
+ "ks", // Kashmiri (written in the Perso-Arabic script)
+ "bal", // Balochi (when written in Arabic script)
+ "khw", // Khowar (Chitrali)
+ "brh", // Brahui (when written in Arabic script)
+ "tmh", // Tamashek (some dialects use Arabic script)
+ "bgn", // Western Balochi
+ "arc", // Aramaic
+ "sam", // Samaritan Aramaic
+ "prd", // Parsi-Dari (a dialect of Persian)
+ "huz", // Hazaragi (a dialect of Persian)
+ "gbz", // Zaza (written in Arabic script in some areas)
+ "urj" // Urdu in Romanized script (not always RTL, but to account for edge cases)
+ /* spell-checker: enable */
+ ].includes(currentLanguageTag);
+ })();
+
+ html.dir = isRtl ? "rtl" : "ltr";
}
const getLanguageLabel = (languageTag: LanguageTag) => {
From e498fb784ba12e5e6be10ca35b95b871edc66bd7 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 12 Oct 2024 17:33:44 +0200
Subject: [PATCH 66/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 9be229b0..cfa9d0cd 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.4",
+ "version": "11.3.5",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 2c1cca168f0748498a94d9467a6bbc22130da676 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 13 Oct 2024 00:55:06 +0200
Subject: [PATCH 67/82] Resolve package.json path relative to the package.json
---
src/bin/shared/buildContext.ts | 21 +++++++++++++++------
1 file changed, 15 insertions(+), 6 deletions(-)
diff --git a/src/bin/shared/buildContext.ts b/src/bin/shared/buildContext.ts
index 26c746b8..89afa6f9 100644
--- a/src/bin/shared/buildContext.ts
+++ b/src/bin/shared/buildContext.ts
@@ -508,6 +508,15 @@ export function getBuildContext(params: {
return themeNames;
})();
+ const relativePathsCwd = (() => {
+ switch (bundler) {
+ case "vite":
+ return projectDirPath;
+ case "webpack":
+ return pathDirname(packageJsonFilePath);
+ }
+ })();
+
const projectBuildDirPath = (() => {
webpack: {
if (bundler !== "webpack") {
@@ -519,7 +528,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.projectBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.projectBuildDirPath,
- cwd: projectDirPath
+ cwd: relativePathsCwd
});
}
@@ -563,7 +572,7 @@ export function getBuildContext(params: {
if (buildOptions.keycloakifyBuildDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.keycloakifyBuildDirPath,
- cwd: projectDirPath
+ cwd: relativePathsCwd
});
}
@@ -592,7 +601,7 @@ export function getBuildContext(params: {
if (parsedPackageJson.keycloakify.publicDirPath !== undefined) {
return getAbsoluteAndInOsFormatPath({
pathIsh: parsedPackageJson.keycloakify.publicDirPath,
- cwd: projectDirPath
+ cwd: relativePathsCwd
});
}
@@ -664,7 +673,7 @@ export function getBuildContext(params: {
pathIsh:
parsedPackageJson.keycloakify
.staticDirPathInProjectBuildDirPath,
- cwd: projectBuildDirPath
+ cwd: relativePathsCwd
});
}
@@ -992,7 +1001,7 @@ export function getBuildContext(params: {
type: "path",
path: getAbsoluteAndInOsFormatPath({
pathIsh: urlOrPath,
- cwd: projectDirPath
+ cwd: relativePathsCwd
})
};
}
@@ -1002,7 +1011,7 @@ export function getBuildContext(params: {
? undefined
: getAbsoluteAndInOsFormatPath({
pathIsh: buildOptions.startKeycloakOptions.realmJsonFilePath,
- cwd: projectDirPath
+ cwd: relativePathsCwd
}),
port: buildOptions.startKeycloakOptions?.port
}
From 27da57844662ea0ad0499daee6c1068b8f179552 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sun, 13 Oct 2024 00:55:23 +0200
Subject: [PATCH 68/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index cfa9d0cd..e6259f12 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.5",
+ "version": "11.3.6",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 831326952bcb0fe925e71b249d9340b931e77967 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 16 Oct 2024 03:37:00 +0200
Subject: [PATCH 69/82] Resize zone2 logo
---
README.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 11eff6fe..2b09cd04 100644
--- a/README.md
+++ b/README.md
@@ -50,19 +50,19 @@ Keycloakify is fully compatible with Keycloak from version 11 to 26...[and beyon
## Sponsors
-Friends for the project, we trust and recommend their services.
+Project backers, we trust and recommend their services.
-
+
-
+
From 8decf4a3c943cfd5cb54129455e39d3e1a11adea Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 16 Oct 2024 04:10:17 +0200
Subject: [PATCH 70/82] Add phaseTwo as sponsor
---
README.md | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2b09cd04..6f3eb47a 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,28 @@ Project backers, we trust and recommend their services.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keycloak on Steroids as a Service - Keycloak community contributors of popular extensions providing free and dedicated Keycloak hosting and enterprise Keycloak support to businesses of all sizes.
+
+
+
+
+
+
+
+

@@ -87,7 +109,7 @@ Project backers, we trust and recommend their services.
-Managed Keycloak Provider - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence.
+Managed Keycloak Provider - With Cloud-IAM powering your Keycloak clusters, you can sleep easy knowing you've got the software and the experts you need for operational excellence. Cloud IAM is a french company.
Use code keycloakify5
at checkout for a 5% discount.
From de620dca5685e5790da27be3fd4cee6b6b1e2be8 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Wed, 16 Oct 2024 05:13:52 +0200
Subject: [PATCH 71/82] Fix light mode rendering
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 6f3eb47a..af64954c 100644
--- a/README.md
+++ b/README.md
@@ -56,7 +56,7 @@ Project backers, we trust and recommend their services.
-
+
@@ -78,7 +78,7 @@ Project backers, we trust and recommend their services.
-
+
From f5b15a5ef698f7a624527abc97ef194e2917ccce Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Thu, 17 Oct 2024 19:54:14 +0200
Subject: [PATCH 72/82] Fix Phase two links
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index af64954c..81228618 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ Project backers, we trust and recommend their services.
- Keycloak on Steroids as a Service - Keycloak community contributors of popular extensions providing free and dedicated Keycloak hosting and enterprise Keycloak support to businesses of all sizes.
+ Keycloak as a Service - Keycloak community contributors of popular extensions providing free and dedicated Keycloak hosting and enterprise Keycloak support to businesses of all sizes.
From cacd0172448f875098f6da14c5d0e5713ec65237 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Thu, 17 Oct 2024 23:23:26 +0200
Subject: [PATCH 73/82] #696
---
src/bin/initialize-email-theme.ts | 31 +++++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/src/bin/initialize-email-theme.ts b/src/bin/initialize-email-theme.ts
index 7a81205b..62a5566a 100644
--- a/src/bin/initialize-email-theme.ts
+++ b/src/bin/initialize-email-theme.ts
@@ -5,6 +5,9 @@ import type { BuildContext } from "./shared/buildContext";
import * as fs from "fs";
import { downloadAndExtractArchive } from "./tools/downloadAndExtractArchive";
import { maybeDelegateCommandToCustomHandler } from "./shared/customHandler_delegate";
+import fetch from "make-fetch-happen";
+import { SemVer } from "./tools/SemVer";
+import { assert } from "tsafe/assert";
export async function command(params: { buildContext: BuildContext }) {
const { buildContext } = params;
@@ -36,7 +39,7 @@ export async function command(params: { buildContext: BuildContext }) {
console.log("Initialize with the base email theme from which version of Keycloak?");
- const { keycloakVersion } = await promptKeycloakVersion({
+ let { keycloakVersion } = await promptKeycloakVersion({
// NOTE: This is arbitrary
startingFromMajor: 17,
excludeMajorVersions: [],
@@ -44,8 +47,32 @@ export async function command(params: { buildContext: BuildContext }) {
buildContext
});
+ const getUrl = (keycloakVersion: string) => {
+ return `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`;
+ };
+
+ keycloakVersion = await (async () => {
+ const keycloakVersionParsed = SemVer.parse(keycloakVersion);
+
+ while (true) {
+ const url = getUrl(SemVer.stringify(keycloakVersionParsed));
+
+ const response = await fetch(url, buildContext.fetchOptions);
+
+ if (response.ok) {
+ break;
+ }
+
+ assert(keycloakVersionParsed.patch !== 0);
+
+ keycloakVersionParsed.patch--;
+ }
+
+ return SemVer.stringify(keycloakVersionParsed);
+ })();
+
const { extractedDirPath } = await downloadAndExtractArchive({
- url: `https://repo1.maven.org/maven2/org/keycloak/keycloak-themes/${keycloakVersion}/keycloak-themes-${keycloakVersion}.jar`,
+ url: getUrl(keycloakVersion),
cacheDirPath: buildContext.cacheDirPath,
fetchOptions: buildContext.fetchOptions,
uniqueIdOfOnArchiveFile: "extractOnlyEmailTheme",
From f3a97b253869cb082b9b5ceb6cbaec0e14dd1d44 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Thu, 17 Oct 2024 23:23:52 +0200
Subject: [PATCH 74/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index e6259f12..ccb76d98 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.6",
+ "version": "11.3.7",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From d2e518d96ba09a058eb4923604d1e0fb88cdc2b8 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 02:19:41 +0200
Subject: [PATCH 75/82] #693 #692
---
...skeysConditionalAuthenticate.useScript.tsx | 9 +++++-
...LoginRecoveryAuthnCodeConfig.useScript.tsx | 9 +++++-
.../pages/WebauthnAuthenticate.useScript.tsx | 9 +++++-
.../pages/WebauthnRegister.useScript.tsx | 9 +++++-
src/tools/waitForElementMountedOnDom.ts | 30 +++++++++++++++++++
5 files changed, 62 insertions(+), 4 deletions(-)
create mode 100644 src/tools/waitForElementMountedOnDom.ts
diff --git a/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx b/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx
index 9cd1c867..4807c6c1 100644
--- a/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx
+++ b/src/login/pages/LoginPasskeysConditionalAuthenticate.useScript.tsx
@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
+import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@@ -67,6 +68,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
- insertScriptTags();
+ (async () => {
+ await waitForElementMountedOnDom({
+ elementId: authButtonId
+ });
+
+ insertScriptTags();
+ })();
}, [isFetchingTranslations]);
}
diff --git a/src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx b/src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx
index 8a5f9586..ef882462 100644
--- a/src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx
+++ b/src/login/pages/LoginRecoveryAuthnCodeConfig.useScript.tsx
@@ -1,5 +1,6 @@
import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
+import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type I18nLike = {
msgStr: (key: "recovery-codes-download-file-header" | "recovery-codes-download-file-description" | "recovery-codes-download-file-date") => string;
@@ -137,6 +138,12 @@ export function useScript(params: { olRecoveryCodesListId: string; i18n: I18nLik
return;
}
- insertScriptTags();
+ (async () => {
+ await waitForElementMountedOnDom({
+ elementId: olRecoveryCodesListId
+ });
+
+ insertScriptTags();
+ })();
}, [isFetchingTranslations]);
}
diff --git a/src/login/pages/WebauthnAuthenticate.useScript.tsx b/src/login/pages/WebauthnAuthenticate.useScript.tsx
index bc8fde5a..0ffde595 100644
--- a/src/login/pages/WebauthnAuthenticate.useScript.tsx
+++ b/src/login/pages/WebauthnAuthenticate.useScript.tsx
@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
+import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@@ -59,6 +60,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
- insertScriptTags();
+ (async () => {
+ await waitForElementMountedOnDom({
+ elementId: authButtonId
+ });
+
+ insertScriptTags();
+ })();
}, [isFetchingTranslations]);
}
diff --git a/src/login/pages/WebauthnRegister.useScript.tsx b/src/login/pages/WebauthnRegister.useScript.tsx
index c1d8cd66..98f3dfa7 100644
--- a/src/login/pages/WebauthnRegister.useScript.tsx
+++ b/src/login/pages/WebauthnRegister.useScript.tsx
@@ -2,6 +2,7 @@ import { useEffect } from "react";
import { useInsertScriptTags } from "keycloakify/tools/useInsertScriptTags";
import { assert } from "keycloakify/tools/assert";
import { KcContext } from "keycloakify/login/KcContext/KcContext";
+import { waitForElementMountedOnDom } from "keycloakify/tools/waitForElementMountedOnDom";
type KcContextLike = {
url: {
@@ -88,6 +89,12 @@ export function useScript(params: { authButtonId: string; kcContext: KcContextLi
return;
}
- insertScriptTags();
+ (async () => {
+ await waitForElementMountedOnDom({
+ elementId: authButtonId
+ });
+
+ insertScriptTags();
+ })();
}, [isFetchingTranslations]);
}
diff --git a/src/tools/waitForElementMountedOnDom.ts b/src/tools/waitForElementMountedOnDom.ts
new file mode 100644
index 00000000..08934f74
--- /dev/null
+++ b/src/tools/waitForElementMountedOnDom.ts
@@ -0,0 +1,30 @@
+export async function waitForElementMountedOnDom(params: {
+ elementId: string;
+}): Promise {
+ const { elementId } = params;
+
+ const getElement = () => document.getElementById(elementId);
+
+ const element = getElement();
+
+ if (element === null) {
+ let prElementPresentInTheDom_resolve: () => void;
+ const prElementPresentInTheDom = new Promise(
+ resolve => (prElementPresentInTheDom_resolve = resolve)
+ );
+
+ // Observe the dom for the element to be added
+ const observer = new MutationObserver(() => {
+ const element = getElement();
+ if (element === null) {
+ return;
+ }
+ observer.disconnect();
+ prElementPresentInTheDom_resolve();
+ });
+
+ observer.observe(document.body, { childList: true, subtree: true });
+
+ await prElementPresentInTheDom;
+ }
+}
From dda77952a0b585bbe8bfdf46c8c59d8957494d35 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 02:28:11 +0200
Subject: [PATCH 76/82] #694 Probably some shell handle double quote
differently
---
src/bin/start-keycloak/start-keycloak.ts | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/bin/start-keycloak/start-keycloak.ts b/src/bin/start-keycloak/start-keycloak.ts
index e243fe2b..4e74bed6 100644
--- a/src/bin/start-keycloak/start-keycloak.ts
+++ b/src/bin/start-keycloak/start-keycloak.ts
@@ -396,12 +396,12 @@ export async function command(params: {
...(realmJsonFilePath === undefined
? []
: [
- `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), realmJsonFilePath)}":/opt/keycloak/data/import/myrealm-realm.json`
+ `-v${SPACE_PLACEHOLDER}"${realmJsonFilePath}":/opt/keycloak/data/import/myrealm-realm.json`
]),
- `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath_cacheDir)}":/opt/keycloak/providers/keycloak-theme.jar`,
+ `-v${SPACE_PLACEHOLDER}"${jarFilePath_cacheDir}":/opt/keycloak/providers/keycloak-theme.jar`,
...extensionJarFilePaths.map(
jarFilePath =>
- `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), jarFilePath)}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
+ `-v${SPACE_PLACEHOLDER}"${jarFilePath}":/opt/keycloak/providers/${pathBasename(jarFilePath)}`
),
...(keycloakMajorVersionNumber <= 20
? [`-e${SPACE_PLACEHOLDER}JAVA_OPTS=-Dkeycloak.profile=preview`]
@@ -424,7 +424,7 @@ export async function command(params: {
}))
.map(
({ localDirPath, containerDirPath }) =>
- `-v${SPACE_PLACEHOLDER}".${pathSep}${pathRelative(process.cwd(), localDirPath)}":${containerDirPath}:rw`
+ `-v${SPACE_PLACEHOLDER}"${localDirPath}":${containerDirPath}:rw`
),
...buildContext.environmentVariables
.map(({ name }) => ({ name, envValue: process.env[name] }))
From 3a2fe597bad224d08056e9412ca7552bec6f2c05 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 02:28:32 +0200
Subject: [PATCH 77/82] Bump version
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index ccb76d98..67c0865f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.7",
+ "version": "11.3.8",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From ba0532c95daf7e0bf21ccd71158b8775f86c72c5 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sat, 19 Oct 2024 00:24:36 +0000
Subject: [PATCH 78/82] docs: update README.md [skip ci]
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 81228618..9894e35a 100644
--- a/README.md
+++ b/README.md
@@ -164,6 +164,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Katharina Eiserfey 💻 ⚠️ 📖 |
 Luca Peruzzo 💻 ⚠️ |
 Nima Shokouhfar 💻 ⚠️ |
+  Marvin A. Ruder 🐛 |
From 4273322ed59873d46494c7d3d4c091d134f284d0 Mon Sep 17 00:00:00 2001
From: "allcontributors[bot]"
<46447321+allcontributors[bot]@users.noreply.github.com>
Date: Sat, 19 Oct 2024 00:24:37 +0000
Subject: [PATCH 79/82] docs: update .all-contributorsrc [skip ci]
---
.all-contributorsrc | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/.all-contributorsrc b/.all-contributorsrc
index f7d39edd..64b2924f 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -300,6 +300,15 @@
"code",
"test"
]
+ },
+ {
+ "login": "marvinruder",
+ "name": "Marvin A. Ruder",
+ "avatar_url": "https://avatars.githubusercontent.com/u/18495294?v=4",
+ "profile": "https://mruder.dev",
+ "contributions": [
+ "bug"
+ ]
}
],
"contributorsPerLine": 7,
From 4185188a5b4fabe60d5966471fab5b46906f57eb Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 22:28:10 +0200
Subject: [PATCH 80/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 67c0865f..e6a3de65 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.8",
+ "version": "11.3.9-rc.0",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",
From 19da96113f832d087c1d0f361a4f34ed8d3e88f5 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 22:33:08 +0200
Subject: [PATCH 81/82] Don't export internals
---
src/login/lib/getUserProfileApi/getUserProfileApi.ts | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/src/login/lib/getUserProfileApi/getUserProfileApi.ts b/src/login/lib/getUserProfileApi/getUserProfileApi.ts
index 31230ad3..0a695217 100644
--- a/src/login/lib/getUserProfileApi/getUserProfileApi.ts
+++ b/src/login/lib/getUserProfileApi/getUserProfileApi.ts
@@ -145,9 +145,7 @@ namespace internal {
};
}
-export function getUserProfileApi_noCache(
- params: ParamsOfGetUserProfileApi
-): UserProfileApi {
+function getUserProfileApi_noCache(params: ParamsOfGetUserProfileApi): UserProfileApi {
const { kcContext, doMakeUserConfirmPassword } = params;
unFormatNumberOnSubmit();
From 25920c208d6e9b5ab267039a51298f403ea3cbb5 Mon Sep 17 00:00:00 2001
From: Joseph Garrone
Date: Sat, 19 Oct 2024 22:33:24 +0200
Subject: [PATCH 82/82] Release candidate
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index e6a3de65..a692db5a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "keycloakify",
- "version": "11.3.9-rc.0",
+ "version": "11.3.9-rc.1",
"description": "Framework to create custom Keycloak UIs",
"repository": {
"type": "git",