Compare commits

..

24 Commits

Author SHA1 Message Date
4f7837c88e Update changelog v4.2.8 2021-11-10 19:25:10 +00:00
fd8e06f1dd Bump version (changelog ignore) 2021-11-10 20:20:48 +01:00
b01a351eaa Update doc about pattern that can be used for user attributes #50 2021-11-10 20:00:53 +01:00
604655c02d Bring back Safari compat 2021-11-10 19:48:18 +01:00
6603ac4389 Update changelog v4.2.7 2021-11-09 00:52:25 +00:00
cca6f952ee Bump version (changelog ignore) 2021-11-09 01:49:15 +01:00
df94a6322d Fix useFormValidationSlice 2021-11-09 01:48:50 +01:00
73e7f64860 Update changelog v4.2.6 2021-11-08 18:38:37 +00:00
e17e1650d5 Bump version (changelog ignore) 2021-11-08 19:33:27 +01:00
3ecb63d500 Fix deepClone so we can overwrite with undefined in when we mock kcContext 2021-11-08 19:33:06 +01:00
41ee7e90ef Update changelog v4.2.5 2021-11-07 19:21:35 +00:00
c70bba727e Bump version (changelog ignore) 2021-11-07 20:17:39 +01:00
747248454d Better debugging experience with user profile 2021-11-07 20:17:14 +01:00
59386241b4 Update changelog v4.2.4 2021-11-01 22:21:39 +00:00
c70b9b0dd1 Bump version (changelog ignore) 2021-11-01 23:15:02 +01:00
2ee00ed919 Better autoComplete typings 2021-11-01 22:28:53 +01:00
cbfc271da5 Update changelog v4.2.3 2021-11-01 21:22:58 +00:00
d45b492837 Bump version (changelog ignore) 2021-11-01 22:16:16 +01:00
ed54c145b7 Make it more easy to understand that error in the log are expected 2021-11-01 22:15:56 +01:00
64ed9a6044 Update changelog v4.2.2 2021-10-27 09:01:53 +00:00
75267abd91 Merge branch 'main' of https://github.com/garronej/keycloakify into main 2021-10-27 10:58:43 +02:00
ba9a3992b7 Bump version (changelog ignore) 2021-10-27 10:58:33 +02:00
a74c32ed6d Update changelog v4.2.1 2021-10-27 10:58:33 +02:00
c5f9812acc Replace 'path' by 'browserify-path' #47 2021-10-27 10:58:10 +02:00
17 changed files with 220 additions and 96 deletions

View File

@ -1,3 +1,32 @@
### **4.2.8** (2021-11-10)
- Update doc about pattern that can be used for user attributes #50
- Bring back Safari compat
### **4.2.7** (2021-11-09)
- Fix useFormValidationSlice
### **4.2.6** (2021-11-08)
- Fix deepClone so we can overwrite with undefined in when we mock kcContext
### **4.2.5** (2021-11-07)
- Better debugging experience with user profile
### **4.2.4** (2021-11-01)
- Better autoComplete typings
### **4.2.3** (2021-11-01)
- Make it more easy to understand that error in the log are expected
### **4.2.2** (2021-10-27)
- Replace 'path' by 'browserify-path' #47
### **4.2.1** (2021-10-26)
- useFormValidationSlice: update when params have changed

View File

@ -307,6 +307,8 @@ Checkout a complete setup [here](https://github.com/garronej/keycloakify-demo-ap
</a>
</p>
NOTE: In reality the regexp used in this gif doesn't work server side, the regexp pattern should be `^[^@]@gmail\.com$` 😬.
User Profile is a Keycloak feature that enables to
[define, from the admin console](https://user-images.githubusercontent.com/6702424/136872461-1f5b64ef-d2ef-4c6b-bb8d-07d4729552b3.png),
what information you want to collect on your users in the register page and to validate inputs
@ -444,18 +446,20 @@ The logs of your keycloak server will always show this kind of errors every time
```log
FTL stack trace ("~" means nesting-related):
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson" at line 70, column 21]
- Reached through: @compress [in template "login.ftl" in macro "objectToJson" at line 36, column 5]
- Reached through: @objectToJson object=value depth=(dep... [in template "login.ftl" in macro "objectToJson" at line 81, column 27]
- Reached through: @compress [in template "login.ftl" in macro "objectToJson" at line 36, column 5]
- Reached through: @objectToJson object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
- Failed at: #local value = object[key] [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 70, column 21]
- Reached through: @compress [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 36, column 5]
- Reached through: @objectToJson_please_ignore_errors object=value depth=(dep... [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 81, column 27]
- Reached through: @compress [in template "login.ftl" in macro "objectToJson_please_ignore_errors" at line 36, column 5]
- Reached through: @objectToJson_please_ignore_errors object=(.data_model) de... [in template "login.ftl" at line 163, column 43]
```
Theses are expected and can be safely ignored.
Theses are expected to show up in the log.
Unfortunately, there is nothing I know of that can be done to avoid them or even mute them.
They can be, however, safely ignored.
To [converts the `.ftl` values into a JavaScript object](https://github.com/InseeFrLab/keycloakify/blob/main/src/bin/build-keycloak-theme/generateFtl/common.ftl)
without making assumptions on the `.data_model` we have to do things that throws.
It's all-right though because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs.
It's all-right because every statement that can fail is inside an `<#attempt><#recorver>` block but it results in errors being printed to the logs.
# Adding custom message (to `i18n/useKcMessage.tsx`)
@ -465,6 +469,9 @@ This approach is a bit hacky as it doesn't provide type safety but it works.
# Email domain whitelist
NOTE: This have been kind of deprecated by [user attribute](#user-profile-and-frontend-form-validation) you could
use a pattern [like this one](https://github.com/InseeFrLab/onyxia-web/blob/f1206e0329b3b8d401ca7bffa95ca9c213cb190a/src/app/components/KcApp/kcContext.ts#L106) to whitelist email domains.
If you want to restrict the emails domain that can register, you can use [this plugin](https://github.com/micedre/keycloak-mail-whitelisting)
and `kcRegisterContext["authorizedMailDomains"]` to validate on.

View File

@ -1,6 +1,6 @@
{
"name": "keycloakify",
"version": "4.2.1",
"version": "4.2.8",
"description": "Keycloak theme generator for Reacts app",
"repository": {
"type": "git",
@ -79,7 +79,7 @@
"cheerio": "^1.0.0-rc.5",
"evt": "2.0.0-beta.38",
"minimal-polyfills": "^2.2.1",
"path": "^0.12.7",
"path-browserify": "^1.0.1",
"react-markdown": "^5.0.3",
"scripting-tools": "^0.19.13",
"tsafe": "^0.8.1"

View File

@ -1,5 +1,5 @@
<script>const _=
<#macro objectToJson object depth>
<#macro objectToJson_please_ignore_errors object depth>
<@compress>
<#local isHash = false>
@ -45,7 +45,7 @@
<#continue>
</#if>
"${key}": <@objectToJson object=value depth=depth+1/>,
"${key}": <@objectToJson_please_ignore_errors object=value depth=depth+1/>,
</#list>
@ -102,7 +102,7 @@
<#list object as item>
<@objectToJson object=item depth=depth+1/>,
<@objectToJson_please_ignore_errors object=item depth=depth+1/>,
</#list>
@ -194,7 +194,7 @@
Object.deepAssign(
out,
//Removing all the undefined
JSON.parse(JSON.stringify(<@objectToJson object=.data_model depth=0 />))
JSON.parse(JSON.stringify(<@objectToJson_please_ignore_errors object=.data_model depth=0 />))
);
Object.deepAssign(

View File

@ -149,15 +149,6 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
</div>
<div className={cx(props.kcInputWrapperClass)}>
<input
autoComplete={(() => {
switch (attribute.name) {
case "password-confirm":
case "password":
return "new-password";
default:
return undefined;
}
})()}
type={(() => {
switch (attribute.name) {
case "password-confirm":
@ -174,11 +165,7 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
className={cx(props.kcInputClass)}
aria-invalid={displayableErrors.length !== 0}
disabled={attribute.readOnly}
{...(attribute.autocomplete === undefined
? {}
: {
"autoComplete": attribute.autocomplete,
})}
autoComplete={attribute.autocomplete}
onBlur={onBlurFactory(attribute.name)}
/>
{displayableErrors.length !== 0 && (

View File

@ -217,10 +217,64 @@ export type Attribute = {
groupDisplayHeader?: string;
groupDisplayDescription?: string;
readOnly: boolean;
autocomplete?: string;
validators: Validators;
annotations: Record<string, string>;
groupAnnotations: Record<string, string>;
autocomplete?:
| "on"
| "off"
| "name"
| "honorific-prefix"
| "given-name"
| "additional-name"
| "family-name"
| "honorific-suffix"
| "nickname"
| "email"
| "username"
| "new-password"
| "current-password"
| "one-time-code"
| "organization-title"
| "organization"
| "street-address"
| "address-line1"
| "address-line2"
| "address-line3"
| "address-level4"
| "address-level3"
| "address-level2"
| "address-level1"
| "country"
| "country-name"
| "postal-code"
| "cc-name"
| "cc-given-name"
| "cc-additional-name"
| "cc-family-name"
| "cc-number"
| "cc-exp"
| "cc-exp-month"
| "cc-exp-year"
| "cc-csc"
| "cc-type"
| "transaction-currency"
| "transaction-amount"
| "language"
| "bday"
| "bday-day"
| "bday-month"
| "bday-year"
| "sex"
| "tel"
| "tel-country-code"
| "tel-national"
| "tel-area-code"
| "tel-local"
| "tel-extension"
| "impp"
| "url"
| "photo";
};
export type Validators = Partial<{

View File

@ -1,13 +1,12 @@
import type { KcContextBase } from "./KcContextBase";
import type { KcContextBase, Attribute } from "./KcContextBase";
import { kcContextMocks, kcContextCommonMock } from "./kcContextMocks";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import type { DeepPartial } from "../tools/DeepPartial";
import { deepAssign } from "../tools/deepAssign";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
? KcContextBase
: AndByDiscriminatingKey<"pageId", KcContextExtended & KcContextBase.Common, KcContextBase>;
import { id } from "tsafe/id";
import { exclude } from "tsafe/exclude";
import { assert } from "tsafe/assert";
import type { ExtendsKcContextBase } from "./getKcContextFromWindow";
import { getKcContextFromWindow } from "./getKcContextFromWindow";
export function getKcContext<KcContextExtended extends { pageId: string } = never>(params?: {
mockPageId?: ExtendsKcContextBase<KcContextExtended>["pageId"];
@ -44,12 +43,55 @@ export function getKcContext<KcContextExtended extends { pageId: string } = neve
"target": kcContext,
"source": partialKcContextCustomMock,
});
if (partialKcContextCustomMock.pageId === "register-user-profile.ftl") {
assert(kcContextDefaultMock?.pageId === "register-user-profile.ftl");
const { attributes } = kcContextDefaultMock.profile;
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes = [];
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName = {};
const partialAttributes = [
...((partialKcContextCustomMock as DeepPartial<KcContextBase.RegisterUserProfile>).profile?.attributes ?? []),
].filter(exclude(undefined));
attributes.forEach(attribute => {
const partialAttribute = partialAttributes.find(({ name }) => name === attribute.name);
const augmentedAttribute: Attribute = {} as any;
deepAssign({
"target": augmentedAttribute,
"source": attribute,
});
if (partialAttribute !== undefined) {
partialAttributes.splice(partialAttributes.indexOf(partialAttribute), 1);
deepAssign({
"target": augmentedAttribute,
"source": partialAttribute,
});
}
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(augmentedAttribute);
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[augmentedAttribute.name] = augmentedAttribute;
});
partialAttributes.forEach(partialAttribute => {
const { name } = partialAttribute;
assert(name !== undefined, "If you define a mock attribute it must have at least a name");
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributes.push(partialAttribute as any);
id<KcContextBase.RegisterUserProfile>(kcContext).profile.attributesByName[name] = partialAttribute as any;
});
}
}
return { kcContext };
}
return {
"kcContext": typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName],
};
return { "kcContext": getKcContextFromWindow<KcContextExtended>() };
}

View File

@ -0,0 +1,11 @@
import type { KcContextBase } from "./KcContextBase";
import type { AndByDiscriminatingKey } from "../tools/AndByDiscriminatingKey";
import { ftlValuesGlobalName } from "../../bin/build-keycloak-theme/ftlValuesGlobalName";
export type ExtendsKcContextBase<KcContextExtended extends { pageId: string }> = [KcContextExtended] extends [never]
? KcContextBase
: AndByDiscriminatingKey<"pageId", KcContextExtended & KcContextBase.Common, KcContextBase>;
export function getKcContextFromWindow<KcContextExtended extends { pageId: string } = never>(): ExtendsKcContextBase<KcContextExtended> | undefined {
return typeof window === "undefined" ? undefined : (window as any)[ftlValuesGlobalName];
}

View File

@ -1,2 +1,3 @@
export type { KcContextBase, Attribute, Validators } from "./KcContextBase";
export type { ExtendsKcContextBase } from "./getKcContextFromWindow";
export { getKcContext } from "./getKcContext";

View File

@ -278,29 +278,6 @@ export const kcContextMocks: KcContextBase[] = [
"readOnly": false,
"name": "lastName",
},
{
"validators": {
"length": {
"ignore.empty.value": true,
"min": "3",
"max": "9",
},
"up-immutable-attribute": {},
"up-attribute-required-by-metadata-value": {},
"email": {
"ignore.empty.value": true,
},
},
"displayName": "${foo}",
"annotations": {
"this_is_second_key": "this_is_second_value",
"this_is_first_key": "this_is_first_value",
},
"required": true,
"groupAnnotations": {},
"readOnly": false,
"name": "foo",
},
];
return {

View File

@ -1,5 +1,5 @@
import { createUseGlobalState } from "powerhooks/useGlobalState";
import { getKcContext } from "../getKcContext";
import { getKcContextFromWindow } from "../getKcContext/getKcContextFromWindow";
import { getBestMatchAmongKcLanguageTag } from "./KcLanguageTag";
import type { StatefulEvt } from "powerhooks";
import { KcLanguageTag } from "./KcLanguageTag";
@ -8,7 +8,7 @@ import { KcLanguageTag } from "./KcLanguageTag";
const wrap = createUseGlobalState(
"kcLanguageTag",
() => {
const { kcContext } = getKcContext();
const kcContext = getKcContextFromWindow();
const languageLike = kcContext?.locale?.current ?? (typeof navigator === "undefined" ? undefined : navigator.language);

View File

@ -27,8 +27,9 @@ function resolveMsg<Key extends string, DoRenderMarkdown extends boolean>(props:
str = (() => {
const startIndex = str
.match(/(?<={)[0-9]+(?=})/g)
?.map(g => parseInt(g))
.match(/{[0-9]+}/g)
?.map(g => g.match(/{([0-9]+)}/)![1])
.map(indexStr => parseInt(indexStr))
.sort((a, b) => a - b)[0];
if (startIndex === undefined) {

View File

@ -1,9 +1,12 @@
import { assert } from "tsafe/assert";
import { is } from "tsafe/is";
import { deepClone } from "./deepClone";
//Warning: Be mindful that because of array this is not idempotent.
export function deepAssign(params: { target: Record<string, unknown>; source: Record<string, unknown> }) {
const { target, source } = params;
const { target } = params;
const source = deepClone(params.source);
Object.keys(source).forEach(key => {
var dereferencedSource = source[key];

View File

@ -1,3 +1,17 @@
export function deepClone<T>(arg: T): T {
return JSON.parse(JSON.stringify(arg));
import "minimal-polyfills/Object.fromEntries";
export function deepClone<T>(o: T): T {
if (!(o instanceof Object)) {
return o;
}
if (typeof o === "function") {
return o;
}
if (o instanceof Array) {
return o.map(deepClone) as any;
}
return Object.fromEntries(Object.entries(o).map(([key, value]) => [key, deepClone(value)])) as any;
}

View File

@ -38,8 +38,24 @@ export function useGetErrors(params: {
const { value: defaultValue, validators } = attributes.find(attribute => attribute.name === name)!;
if (defaultValue === value && messagesPerField.existsError(value)) {
const errorMessageStr = messagesPerField.get(value);
block: {
if (defaultValue !== value) {
break block;
}
let doesErrorExist: boolean;
try {
doesErrorExist = messagesPerField.existsError(name);
} catch {
break block;
}
if (!doesErrorExist) {
break block;
}
const errorMessageStr = messagesPerField.get(name);
return [
{
@ -312,6 +328,7 @@ export function useFormValidationSlice(params: {
"validators": passwordValidators,
"annotations": {},
"groupAnnotations": {},
"autocomplete": "new-password",
}),
id<Attribute>({
"name": "password-confirm",
@ -328,6 +345,7 @@ export function useFormValidationSlice(params: {
},
"annotations": {},
"groupAnnotations": {},
"autocomplete": "new-password",
}),
]),
],

View File

@ -1,6 +1,6 @@
import { getKcContext } from "../../lib/getKcContext";
import type { KcContextBase } from "../../lib/getKcContext";
import type { ExtendsKcContextBase } from "../../lib/getKcContext/getKcContext";
import type { ExtendsKcContextBase } from "../../lib/getKcContext";
import { same } from "evt/tools/inDepth";
import { assert } from "tsafe/assert";
import type { Equals } from "tsafe";

View File

@ -771,11 +771,6 @@ inherits@2, inherits@^2.0.1, inherits@~2.0.1, inherits@~2.0.3:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
inline-style-parser@0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1"
@ -1174,6 +1169,11 @@ parse5@^6.0.1:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b"
integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==
path-browserify@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
path-exists@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
@ -1194,14 +1194,6 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
path@^0.12.7:
version "0.12.7"
resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f"
integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=
dependencies:
process "^0.11.1"
util "^0.10.3"
picomatch@^2.2.3:
version "2.3.0"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972"
@ -1241,11 +1233,6 @@ process-nextick-args@~2.0.0:
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
process@^0.11.1:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
@ -1677,13 +1664,6 @@ util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
util@^0.10.3:
version "0.10.4"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901"
integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==
dependencies:
inherits "2.0.3"
vfile-message@^2.0.0:
version "2.0.4"
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-2.0.4.tgz#5b43b88171d409eae58477d13f23dd41d52c371a"