Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
bb37ce9cef | |||
77ff33570d | |||
20383d60a9 | |||
79aa5ac5f2 | |||
8be6c0d1d2 | |||
7f5a9e77de | |||
ff19ab8b08 | |||
63dcb2ad39 | |||
795e8ed0e5 | |||
bccb56ed61 | |||
02e2ad89ec |
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,3 +1,17 @@
|
|||||||
|
### **4.7.2** (2022-04-06)
|
||||||
|
|
||||||
|
- #43: M1 Mac support
|
||||||
|
|
||||||
|
### **4.7.1** (2022-03-30)
|
||||||
|
|
||||||
|
- Improve browser autofill
|
||||||
|
- factorization
|
||||||
|
|
||||||
|
## **4.7.0** (2022-03-17)
|
||||||
|
|
||||||
|
- Add support for options validator
|
||||||
|
- remove duplicate dependency
|
||||||
|
|
||||||
## **4.6.0** (2022-03-07)
|
## **4.6.0** (2022-03-07)
|
||||||
|
|
||||||
- Remove powerhooks as dev dependency
|
- Remove powerhooks as dev dependency
|
||||||
|
14
README.md
14
README.md
@ -30,6 +30,10 @@
|
|||||||
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
<img src="https://user-images.githubusercontent.com/6702424/110260457-a1c3d380-7fac-11eb-853a-80459b65626b.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> New with v4.7.2: **M1 Mac** support (for testing locally with a dockerized Keycloak).
|
||||||
|
> Thanks goes to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
|
||||||
|
> Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||||
|
|
||||||
# Motivations
|
# Motivations
|
||||||
|
|
||||||
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
|
Keycloak provides [theme support](https://www.keycloak.org/docs/latest/server_development/#_themes) for web pages. This allows customizing the look and feel of end-user facing pages so they can be integrated with your applications.
|
||||||
@ -474,6 +478,16 @@ and `kcRegisterContext["authorizedMailDomains"]` to validate on.
|
|||||||
|
|
||||||
# Changelog highlights
|
# Changelog highlights
|
||||||
|
|
||||||
|
# v4.7.2
|
||||||
|
|
||||||
|
Testing with local Keycloak container working with M1 Mac. Thanks to [@eduardosanzb](https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658).
|
||||||
|
Be aware: When running M1s you are testing with Keycloak v15 else the local container spun will be a Keycloak v16.1.0.
|
||||||
|
|
||||||
|
# v4.7.0
|
||||||
|
|
||||||
|
Register with user profile enabled: Out of the box `options` validator support.
|
||||||
|
[Example](https://user-images.githubusercontent.com/6702424/158911163-81e6bbe8-feb0-4dc8-abff-de199d7a678e.mov)
|
||||||
|
|
||||||
# v4.6.0
|
# v4.6.0
|
||||||
|
|
||||||
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
|
`tss-react` and `powerhooks` are no longer peer dependencies of `keycloakify`.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "keycloakify",
|
"name": "keycloakify",
|
||||||
"version": "4.6.0",
|
"version": "4.7.2",
|
||||||
"description": "Keycloak theme generator for Reacts app",
|
"description": "Keycloak theme generator for Reacts app",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@ -65,7 +65,6 @@
|
|||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"husky": "^4.3.8",
|
"husky": "^4.3.8",
|
||||||
"lint-staged": "^11.0.0",
|
"lint-staged": "^11.0.0",
|
||||||
"powerhooks": "^0.11.0",
|
|
||||||
"prettier": "^2.3.0",
|
"prettier": "^2.3.0",
|
||||||
"properties-parser": "^0.3.1",
|
"properties-parser": "^0.3.1",
|
||||||
"react": "^17.0.1",
|
"react": "^17.0.1",
|
||||||
|
@ -5,6 +5,7 @@ import * as child_process from "child_process";
|
|||||||
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
import { generateDebugFiles, containerLaunchScriptBasename } from "./generateDebugFiles";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
|
import { getIsM1 } from "../tools/isM1";
|
||||||
|
|
||||||
type ParsedPackageJson = {
|
type ParsedPackageJson = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -92,7 +93,9 @@ export function main() {
|
|||||||
keycloakThemeBuildingDirPath,
|
keycloakThemeBuildingDirPath,
|
||||||
themeName,
|
themeName,
|
||||||
//We want, however to test in a container running the latest Keycloak version
|
//We want, however to test in a container running the latest Keycloak version
|
||||||
"keycloakVersion": "16.1.0",
|
//Except on M1 where we can't use the default image and we only have
|
||||||
|
//https://github.com/InseeFrLab/keycloakify/issues/43#issuecomment-975699658
|
||||||
|
"keycloakVersion": getIsM1() ? "15.0.2" : "16.1.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { join as pathJoin, dirname as pathDirname } from "path";
|
import { join as pathJoin, dirname as pathDirname } from "path";
|
||||||
import type { KeycloakVersion } from "../../KeycloakVersion";
|
import type { KeycloakVersion } from "../../KeycloakVersion";
|
||||||
|
import { getIsM1 } from "../../tools/isM1";
|
||||||
|
|
||||||
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
export const containerLaunchScriptBasename = "start_keycloak_testing_container.sh";
|
||||||
|
|
||||||
@ -12,7 +13,11 @@ export function generateDebugFiles(params: { keycloakVersion: KeycloakVersion; t
|
|||||||
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
pathJoin(keycloakThemeBuildingDirPath, "Dockerfile"),
|
||||||
Buffer.from(
|
Buffer.from(
|
||||||
[
|
[
|
||||||
`FROM jboss/keycloak:${keycloakVersion}`,
|
`FROM ${
|
||||||
|
getIsM1()
|
||||||
|
? "eduardosanzb/keycloak@sha256:b1f5bc674eaff6f4e7b37808b9863440310ff93c282fc9bff812377be48bf519"
|
||||||
|
: `jboss/keycloak:${keycloakVersion}`
|
||||||
|
}`,
|
||||||
"",
|
"",
|
||||||
"USER root",
|
"USER root",
|
||||||
"",
|
"",
|
||||||
|
5
src/bin/tools/isM1.ts
Normal file
5
src/bin/tools/isM1.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import * as os from "os";
|
||||||
|
|
||||||
|
export function getIsM1() {
|
||||||
|
return os.cpus()[0].model.includes("Apple M1");
|
||||||
|
}
|
@ -5,6 +5,7 @@ import type { KcContextBase } from "../getKcContext/KcContextBase";
|
|||||||
import { useKcMessage } from "../i18n/useKcMessage";
|
import { useKcMessage } from "../i18n/useKcMessage";
|
||||||
import { useCssAndCx } from "tss-react";
|
import { useCssAndCx } from "tss-react";
|
||||||
import { useConstCallback } from "powerhooks/useConstCallback";
|
import { useConstCallback } from "powerhooks/useConstCallback";
|
||||||
|
import type { FormEventHandler } from "react";
|
||||||
|
|
||||||
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
|
export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.Login } & KcProps) => {
|
||||||
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
const { social, realm, url, usernameEditDisabled, login, auth, registrationDisabled } = kcContext;
|
||||||
@ -15,7 +16,19 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.L
|
|||||||
|
|
||||||
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
const [isLoginButtonDisabled, setIsLoginButtonDisabled] = useState(false);
|
||||||
|
|
||||||
const onSubmit = useConstCallback(() => (setIsLoginButtonDisabled(true), true));
|
const onSubmit = useConstCallback<FormEventHandler<HTMLFormElement>>(e => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
setIsLoginButtonDisabled(true);
|
||||||
|
|
||||||
|
const formElement = e.target as HTMLFormElement;
|
||||||
|
|
||||||
|
//NOTE: Even if we login with email Keycloak expect username and password in
|
||||||
|
//the POST request.
|
||||||
|
formElement.querySelector("input[name='email']")?.setAttribute("name", "username");
|
||||||
|
|
||||||
|
formElement.submit();
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Template
|
<Template
|
||||||
@ -33,27 +46,40 @@ export const Login = memo(({ kcContext, ...props }: { kcContext: KcContextBase.L
|
|||||||
{realm.password && (
|
{realm.password && (
|
||||||
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
<form id="kc-form-login" onSubmit={onSubmit} action={url.loginAction} method="post">
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
<label htmlFor="username" className={cx(props.kcLabelClass)}>
|
{(() => {
|
||||||
{!realm.loginWithEmailAllowed
|
const label = !realm.loginWithEmailAllowed
|
||||||
? msg("username")
|
? "username"
|
||||||
: !realm.registrationEmailAsUsername
|
: realm.registrationEmailAsUsername
|
||||||
? msg("usernameOrEmail")
|
? "email"
|
||||||
: msg("email")}
|
: "usernameOrEmail";
|
||||||
</label>
|
|
||||||
<input
|
const autoCompleteHelper: typeof label = label === "usernameOrEmail" ? "username" : label;
|
||||||
tabIndex={1}
|
|
||||||
id="username"
|
return (
|
||||||
className={cx(props.kcInputClass)}
|
<>
|
||||||
name="username"
|
<label htmlFor={autoCompleteHelper} className={cx(props.kcLabelClass)}>
|
||||||
defaultValue={login.username ?? ""}
|
{msg(label)}
|
||||||
type="text"
|
</label>
|
||||||
{...(usernameEditDisabled
|
<input
|
||||||
? { "disabled": true }
|
tabIndex={1}
|
||||||
: {
|
id={autoCompleteHelper}
|
||||||
"autoFocus": true,
|
className={cx(props.kcInputClass)}
|
||||||
"autoComplete": "off",
|
//NOTE: This is used by Google Chrome auto fill so we use it to tell
|
||||||
})}
|
//the browser how to pre fill the form but before submit we put it back
|
||||||
/>
|
//to username because it is what keycloak expects.
|
||||||
|
name={autoCompleteHelper}
|
||||||
|
defaultValue={login.username ?? ""}
|
||||||
|
type="text"
|
||||||
|
{...(usernameEditDisabled
|
||||||
|
? { "disabled": true }
|
||||||
|
: {
|
||||||
|
"autoFocus": true,
|
||||||
|
"autoComplete": "off",
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div className={cx(props.kcFormGroupClass)}>
|
<div className={cx(props.kcFormGroupClass)}>
|
||||||
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
<label htmlFor="password" className={cx(props.kcLabelClass)}>
|
||||||
|
@ -95,7 +95,7 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
|
|||||||
{
|
{
|
||||||
target: { value },
|
target: { value },
|
||||||
},
|
},
|
||||||
]: [React.ChangeEvent<HTMLInputElement>],
|
]: [React.ChangeEvent<HTMLInputElement | HTMLSelectElement>],
|
||||||
) =>
|
) =>
|
||||||
formValidationReducer({
|
formValidationReducer({
|
||||||
"action": "update value",
|
"action": "update value",
|
||||||
@ -148,26 +148,50 @@ const UserProfileFormFields = memo(({ kcContext, onIsFormSubmittableValueChange,
|
|||||||
{attribute.required && <>*</>}
|
{attribute.required && <>*</>}
|
||||||
</div>
|
</div>
|
||||||
<div className={cx(props.kcInputWrapperClass)}>
|
<div className={cx(props.kcInputWrapperClass)}>
|
||||||
<input
|
{(() => {
|
||||||
type={(() => {
|
const { options } = attribute.validators;
|
||||||
switch (attribute.name) {
|
|
||||||
case "password-confirm":
|
if (options !== undefined) {
|
||||||
case "password":
|
return (
|
||||||
return "password";
|
<select
|
||||||
default:
|
id={attribute.name}
|
||||||
return "text";
|
name={attribute.name}
|
||||||
}
|
onChange={onChangeFactory(attribute.name)}
|
||||||
})()}
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
id={attribute.name}
|
value={value}
|
||||||
name={attribute.name}
|
>
|
||||||
value={value}
|
{options.options.map(option => (
|
||||||
onChange={onChangeFactory(attribute.name)}
|
<option key={option} value={option}>
|
||||||
className={cx(props.kcInputClass)}
|
{option}
|
||||||
aria-invalid={displayableErrors.length !== 0}
|
</option>
|
||||||
disabled={attribute.readOnly}
|
))}
|
||||||
autoComplete={attribute.autocomplete}
|
</select>
|
||||||
onBlur={onBlurFactory(attribute.name)}
|
);
|
||||||
/>
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={(() => {
|
||||||
|
switch (attribute.name) {
|
||||||
|
case "password-confirm":
|
||||||
|
case "password":
|
||||||
|
return "password";
|
||||||
|
default:
|
||||||
|
return "text";
|
||||||
|
}
|
||||||
|
})()}
|
||||||
|
id={attribute.name}
|
||||||
|
name={attribute.name}
|
||||||
|
value={value}
|
||||||
|
onChange={onChangeFactory(attribute.name)}
|
||||||
|
className={cx(props.kcInputClass)}
|
||||||
|
aria-invalid={displayableErrors.length !== 0}
|
||||||
|
disabled={attribute.readOnly}
|
||||||
|
autoComplete={attribute.autocomplete}
|
||||||
|
onBlur={onBlurFactory(attribute.name)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{displayableErrors.length !== 0 && (
|
{displayableErrors.length !== 0 && (
|
||||||
<span
|
<span
|
||||||
id={`input-error-${attribute.name}`}
|
id={`input-error-${attribute.name}`}
|
||||||
|
@ -315,6 +315,7 @@ export type Validators = Partial<{
|
|||||||
name: string;
|
name: string;
|
||||||
shouldBe: "equal" | "different";
|
shouldBe: "equal" | "different";
|
||||||
};
|
};
|
||||||
|
options: Validators.Options;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export declare namespace Validators {
|
export declare namespace Validators {
|
||||||
@ -331,6 +332,9 @@ export declare namespace Validators {
|
|||||||
min?: `${number}`;
|
min?: `${number}`;
|
||||||
max?: `${number}`;
|
max?: `${number}`;
|
||||||
};
|
};
|
||||||
|
export type Options = {
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
assert<Equals<KcContextBase["pageId"], PageId>>();
|
assert<Equals<KcContextBase["pageId"], PageId>>();
|
||||||
|
@ -10,6 +10,7 @@ const kcMessages = {
|
|||||||
"shouldBeDifferent": "{0} should be different to {1}",
|
"shouldBeDifferent": "{0} should be different to {1}",
|
||||||
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
"shouldMatchPattern": "Pattern should match: `/{0}/`",
|
||||||
"mustBeAnInteger": "Must be an integer",
|
"mustBeAnInteger": "Must be an integer",
|
||||||
|
"notAValidOption": "Not a valid option",
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
...kcMessagesBase["fr"],
|
...kcMessagesBase["fr"],
|
||||||
@ -18,6 +19,7 @@ const kcMessages = {
|
|||||||
"shouldBeDifferent": "{0} doit être différent de {1}",
|
"shouldBeDifferent": "{0} doit être différent de {1}",
|
||||||
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
"shouldMatchPattern": "Dois respecter le schéma: `/{0}/`",
|
||||||
"mustBeAnInteger": "Doit être un nombre entiers",
|
"mustBeAnInteger": "Doit être un nombre entiers",
|
||||||
|
"notAValidOption": "N'est pas une option valide",
|
||||||
/* spell-checker: enable */
|
/* spell-checker: enable */
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -213,7 +213,7 @@ export function useGetErrors(params: {
|
|||||||
break scope;
|
break scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
const msgArgs = ["invalidEmailMessage"] as const;
|
const msgArgs = [id<MessageKey>("invalidEmailMessage")] as const;
|
||||||
|
|
||||||
errors.push({
|
errors.push({
|
||||||
validatorName,
|
validatorName,
|
||||||
@ -276,6 +276,32 @@ export function useGetErrors(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
scope: {
|
||||||
|
const validatorName = "options";
|
||||||
|
|
||||||
|
const validator = validators[validatorName];
|
||||||
|
|
||||||
|
if (validator === undefined) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === "") {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validator.options.indexOf(value) >= 0) {
|
||||||
|
break scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
const msgArgs = [id<MessageKey>("notAValidOption")] as const;
|
||||||
|
|
||||||
|
errors.push({
|
||||||
|
validatorName,
|
||||||
|
"errorMessage": <Fragment key={errors.length}>{advancedMsg(...msgArgs)}</Fragment>,
|
||||||
|
"errorMessageStr": advancedMsgStr(...msgArgs),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//TODO: Implement missing validators.
|
//TODO: Implement missing validators.
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
Reference in New Issue
Block a user